0

Custom guards and providers — building non-standard auth

Advanced5 min read·lv-16-005
laravel-srcframework

Concept

Password reset in Laravel is a multi-step flow: the user requests a reset (email sent), clicks the link (token verified), submits a new password (token consumed). Laravel handles all of this with built-in notification, token storage, and expiry.

Flow:

  1. User submits email on /forgot-password.
  2. Password::sendResetLink(['email' => $email]) → generates a token, stores password_resets table row (hashed token + email + created_at), sends ResetPasswordNotification.
  3. Email contains a link: /reset-password?token={token}&email={email}.
  4. User submits new password on /reset-password.
  5. Password::reset($credentials, $callback) → validates token, calls callback with user + new password, deletes the token.

Token security: The reset token is a random 64-character string. It's stored as a bcrypt hash in the database — the URL contains the plain token, the DB stores the hash. Token expires based on config('auth.passwords.users.expire') (default 60 minutes).

ResetPassword notification: Customizable. $user->sendPasswordResetNotification($token). Override on the User model to use a custom notification.

Password facade: Illuminate\Support\Facades\Password. Constants: Password::RESET_LINK_SENT, Password::INVALID_TOKEN, Password::INVALID_USER, Password::RESET_THROTTLED.

Throttling: Built-in rate limiting. Password::RESET_THROTTLED returned if the user requests too many resets.

Code Example

php
<?php
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Facades\Hash;
use Illuminate\Auth\Events\PasswordReset;

// Step 1: Request password reset email
class ForgotPasswordController extends Controller
{
    public function store(Request $request)
    {
        $request->validate(['email' => 'required|email']);

        $status = Password::sendResetLink($request->only('email'));

        return match($status) {
            Password::RESET_LINK_SENT => back()->with('status', __($status)),
            Password::INVALID_USER    => back()->withErrors(['email' => __($status)]),
            Password::RESET_THROTTLED => back()->withErrors(['email' => __($status)]),
            default                   => back()->withErrors(['email' => __($status)]),
        };
    }
}

// Step 2: Handle the reset form submission
class NewPasswordController extends Controller
{
    public function store(Request $request)
    {
        $request->validate([
            'token'                 => 'required',
            'email'                 => 'required|email',
            'password'              => 'required|min:8|confirmed',
            'password_confirmation' => 'required',
        ]);

        $status = Password::reset(
            $request->only('email', 'password', 'password_confirmation', 'token'),
            function(User $user, string $password) {
                $user->forceFill([
                    'password'       => Hash::make($password),
                    'remember_token' => \Illuminate\Support\Str::random(60),
                ])->save();

                event(new PasswordReset($user));
            }
        );

        return $status === Password::PASSWORD_RESET
            ? redirect()->route('login')->with('status', __($status))
            : back()->withErrors(['email' => __($status)]);
    }
}

// Customize reset email — on User model
public function sendPasswordResetNotification(string $token): void
{
    $this->notify(new \App\Notifications\CustomResetPasswordNotification($token));
}

// config/auth.php — customize expiry and throttle
'passwords' => [
    'users' => [
        'provider' => 'users',
        'table'    => 'password_reset_tokens',
        'expire'   => 60,    // token expires in 60 minutes
        'throttle' => 60,    // can request reset once per 60 seconds
    ],
],