Custom guards and providers — building non-standard auth
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:
- User submits email on
/forgot-password. Password::sendResetLink(['email' => $email])→ generates a token, storespassword_resetstable row (hashed token + email + created_at), sendsResetPasswordNotification.- Email contains a link:
/reset-password?token={token}&email={email}. - User submits new password on
/reset-password. 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
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
],
],