Back to Read More
LaravelPHP

Laravel Password Reset

Nov 20, 2025

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

  1. 1User clicks "Forgot Password?" and enters their email
  2. 2Laravel generates a unique token and stores it in the password_reset_tokens table
  3. 3Laravel sends an email with a reset link containing the token
  4. 4User clicks the link, enters a new password
  5. 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 migrate
The 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).

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

Β© 2026 Koeuk KOS. All rights reserved.

Built with Nuxt.js, Vue.js & Tailwind CSS