Users forget passwords β it happens all the time. Laravel provides a complete password reset system out of the box: send a reset link via email, verify the token, and let the user set a new password. Here's how to implement it step by step.
How Password Reset Works
- 1User clicks "Forgot Password?" and enters their email
- 2Laravel generates a unique token and stores it in the
password_reset_tokenstable - 3Laravel sends an email with a reset link containing the token
- 4User clicks the link, enters a new password
- 5Laravel verifies the token, updates the password, and deletes the token
1. Database Setup
Laravel's default migration already creates the password_reset_tokens table. Make sure you've run migrations:
Terminal
php artisan migrateThe migration looks like this
Schema::create('password_reset_tokens', function (Blueprint $table) {
$table->string('email')->primary();
$table->string('token');
$table->timestamp('created_at')->nullable();
});2. Request Password Reset Link
First, create the form where users enter their email, and the controller that sends the reset link:
routes/web.php
use App\Http\Controllers\PasswordResetController;
Route::get('/forgot-password', [PasswordResetController::class, 'showRequestForm'])
->middleware('guest')
->name('password.request');
Route::post('/forgot-password', [PasswordResetController::class, 'sendResetLink'])
->middleware('guest')
->name('password.email');resources/views/auth/forgot-password.blade.php
<h1>Forgot Password</h1>
@if (session('status'))
<div class="alert-success">${'{{'} session('status') ${'}}'}</div>
@endif
<form method="POST" action="${'{{'} route('password.email') ${'}}'}">
@csrf
<label>Email Address</label>
<input type="email" name="email" value="${'{{'} old('email') ${'}}'}" required>
@error('email')
<span>${'{{'} $message ${'}}'}</span>
@enderror
<button type="submit">Send Reset Link</button>
</form>Controller β Send the link
use Illuminate\Support\Facades\Password;
class PasswordResetController extends Controller
{
public function showRequestForm()
{
return view('auth.forgot-password');
}
public function sendResetLink(Request $request)
{
$request->validate([
'email' => 'required|email',
]);
// Send password reset link
$status = Password::sendResetLink(
$request->only('email')
);
return $status === Password::RESET_LINK_SENT
? back()->with('status', __($status))
: back()->withErrors(['email' => __($status)]);
}
}3. Reset the Password
When the user clicks the link in the email, they land on the reset form:
routes/web.php
Route::get('/reset-password/{token}', [PasswordResetController::class, 'showResetForm'])
->middleware('guest')
->name('password.reset');
Route::post('/reset-password', [PasswordResetController::class, 'resetPassword'])
->middleware('guest')
->name('password.update');resources/views/auth/reset-password.blade.php
<h1>Reset Password</h1>
<form method="POST" action="${'{{'} route('password.update') ${'}}'}">
@csrf
<input type="hidden" name="token" value="${'{{'} $token ${'}}'}">
<label>Email</label>
<input type="email" name="email" value="${'{{'} old('email', $email) ${'}}'}" required>
<label>New Password</label>
<input type="password" name="password" required>
<label>Confirm Password</label>
<input type="password" name="password_confirmation" required>
@error('email')
<span>${'{{'} $message ${'}}'}</span>
@enderror
<button type="submit">Reset Password</button>
</form>Controller β Reset password
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Support\Str;
public function showResetForm(string $token)
{
return view('auth.reset-password', ['token' => $token]);
}
public function resetPassword(Request $request)
{
$request->validate([
'token' => 'required',
'email' => 'required|email',
'password' => 'required|min:8|confirmed',
]);
$status = Password::reset(
$request->only('email', 'password', 'password_confirmation', 'token'),
function ($user, string $password) {
$user->forceFill([
'password' => Hash::make($password),
'remember_token' => Str::random(60),
])->save();
event(new PasswordReset($user));
}
);
return $status === Password::PASSWORD_RESET
? redirect()->route('login')->with('status', __($status))
: back()->withErrors(['email' => [__($status)]]);
}4. Configuration
config/auth.php
'passwords' => [
'users' => [
'provider' => 'users',
'table' => 'password_reset_tokens',
'expire' => 60, // Token expires after 60 minutes
'throttle' => 60, // Wait 60 seconds before resending
],
],expire: How many minutes a reset token is valid. After this, the user must request a new link.
throttle: How many seconds a user must wait before requesting another reset email (prevents spam).
throttle: How many seconds a user must wait before requesting another reset email (prevents spam).
5. Customize the Reset Email
app/Providers/AppServiceProvider.php
use Illuminate\Auth\Notifications\ResetPassword;
use Illuminate\Notifications\Messages\MailMessage;
public function boot(): void
{
// Customize the reset email content
ResetPassword::toMailUsing(function ($notifiable, string $token) {
$url = url("/reset-password/{$token}?email={$notifiable->email}");
return (new MailMessage)
->subject('Reset Your Password')
->greeting('Hello!')
->line('You requested a password reset for your account.')
->action('Reset Password', $url)
->line('This link expires in 60 minutes.')
->line('If you did not request this, ignore this email.');
});
// Or customize just the URL
ResetPassword::createUrlUsing(function ($user, string $token) {
return 'https://myapp.com/reset-password/' . $token
. '?email=' . $user->email;
});
}6. API Password Reset (for SPA / Mobile)
For Vue/React SPA or mobile apps, return JSON instead of redirects:
routes/api.php
Route::post('/forgot-password', function (Request $request) {
$request->validate(['email' => 'required|email']);
$status = Password::sendResetLink($request->only('email'));
if ($status === Password::RESET_LINK_SENT) {
return response()->json(['message' => 'Reset link sent to your email']);
}
return response()->json(['message' => __($status)], 400);
});
Route::post('/reset-password', function (Request $request) {
$request->validate([
'token' => 'required',
'email' => 'required|email',
'password' => 'required|min:8|confirmed',
]);
$status = Password::reset(
$request->only('email', 'password', 'password_confirmation', 'token'),
function ($user, string $password) {
$user->forceFill([
'password' => Hash::make($password),
'remember_token' => Str::random(60),
])->save();
event(new PasswordReset($user));
}
);
if ($status === Password::PASSWORD_RESET) {
return response()->json(['message' => 'Password reset successfully']);
}
return response()->json(['message' => __($status)], 400);
});7. Password Confirmation (for Sensitive Actions)
Require users to re-enter their password before performing sensitive actions (like changing email or deleting account):
routes/web.php
// These routes require password confirmation
Route::middleware(['auth', 'password.confirm'])->group(function () {
Route::get('/settings/security', [SettingsController::class, 'security']);
Route::delete('/account', [AccountController::class, 'destroy']);
});resources/views/auth/confirm-password.blade.php
<h1>Confirm Password</h1>
<p>Please confirm your password to continue.</p>
<form method="POST" action="${'{{'} route('password.confirm') ${'}}'}">
@csrf
<input type="password" name="password" required>
@error('password')
<span>${'{{'} $message ${'}}'}</span>
@enderror
<button type="submit">Confirm</button>
</form>Timeout: By default, once confirmed, the user won't be asked again for 3 hours. Configure this with
password_timeout in config/auth.php. Summary
- βPassword::sendResetLink() β sends reset email with token
- βPassword::reset() β validates token and updates password
- βToken expiry β configurable, default 60 minutes
- βCustomizable email β modify content and URL
- βAPI support β return JSON for SPA/mobile apps
- βPassword confirmation β re-verify for sensitive actions