Laravel 12: Forgot Your Password Functionality
In this tutorial, we’ll walk through how to implement the "Forgot Your Password" feature in Laravel 12, where users can request a password reset link via email and set a new password securely.
Prerequisites
-
Laravel 12 project setup
-
Database connected
-
SMTP email setup (Gmail or other)
-
Authentication system (e.g., Laravel Breeze or custom)
Step 1: Create the Password Reset Table
Create a table to store reset tokens:
php artisan make:migration create_password_resets_table
In the migration file:
Schema::create('password_resets', function (Blueprint $table) {
$table->string('email')->index();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
Then run:
php artisan migrate
Step 2: Configure Mail Settings in .env
MAIL_MAILER=smtp MAIL_HOST=smtp.gmail.com MAIL_PORT=587 MAIL_USERNAME=your_email@gmail.com MAIL_PASSWORD=your_app_password MAIL_ENCRYPTION=tls MAIL_FROM_ADDRESS=your_email@gmail.com MAIL_FROM_NAME="Your App Name"
For Gmail, create an App Password if 2FA is enabled.
⚠️ If you're using Gmail, you must generate an App Password — regular Gmail passwords won't work.
👉 Go to: Google App Passwords
Step 3: Create Routes
In routes/web.php
:
// ----------------------------- Forget Password --------------------------// Route::controller(ForgotPasswordController::class)->group(function () { Route::post('forget-password', 'sendResetLinkEmail')->name('password.email'); }); // ---------------------------- Reset Password ----------------------------// Route::controller(ResetPasswordController::class)->group(function () { Route::get('reset-password/{token}', 'showResetForm')->name('password.reset'); Route::post('reset-password', 'reset')->name('password.update'); });
Step 4: Create the Controllers
ForgotPasswordController
php artisan make:controller Auth/ForgotPasswordController
In ForgotPasswordController.php
:
<?php namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\DB; use Illuminate\Http\Request; use Illuminate\Support\Str; use Illuminate\View\View; use App\Models\User; use Carbon\Carbon; class ForgotPasswordController extends Controller { /** Show the email request form */ public function showLinkRequestForm(): View { return view('auth.passwords.email'); } /** Handle the email submission */ public function sendResetLinkEmail(Request $request) { $request->validate([ 'email' => 'required|email|exists:users,email', ]); try { $token = Str::random(40); // Delete existing reset tokens DB::table('password_resets')->where('email', $request->email)->delete(); // Store new reset token (plain text) DB::table('password_resets')->insert([ 'email' => $request->email, 'token' => $token, 'created_at' => Carbon::now(), ]); // Send email Mail::send('auth.verify', ['token' => $token], function ($message) use ($request) { $message->from(config('mail.from.address'), config('mail.from.name')); $message->to($request->email)->subject('Reset Password Notification'); }); return back()->with('success', 'We have e-mailed your password reset link! :)'); } catch (\Exception $e) { \Log::error('Password Reset Email Error: '.$e->getMessage()); return back()->with('error', 'Something went wrong while sending the reset email. Please try again.'); } } }
ResetPasswordController
php artisan make:controller Auth/ResetPasswordController
In ResetPasswordController.php
:
<?php namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\DB; use Illuminate\Support\Carbon; use Illuminate\Http\Request; use App\Models\User; class ResetPasswordController extends Controller { /** * Show the reset password form. * * @param string $token * @return \Illuminate\View\View */ public function getPassword($token) { return view('auth.passwords.reset', ['token' => $token]); } /** * Handle the password reset process. * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\RedirectResponse */ public function updatePassword(Request $request) { try { // Validate the request inputs $request->validate([ 'email' => 'required|email|exists:users,email', 'password' => 'required|string|min:6|confirmed', 'token' => 'required', ]); // Find password reset record $resetRecord = DB::table('password_resets') ->where('email', $request->email) ->first(); if (!$resetRecord || !Hash::check($request->token, $resetRecord->token)) { return back()->with('error', 'Invalid token!')->withInput(); } // Check if the token has expired (valid for 60 minutes) if (Carbon::parse($resetRecord->created_at)->addMinutes(60)->isPast()) { return back()->with('error', 'The password reset link has expired.')->withInput(); } // Find the user and update the password $user = User::where('email', $request->email)->first(); if ($user) { $user->update([ 'password' => Hash::make($request->password), ]); // Delete the used password reset record DB::table('password_resets')->where('email', $request->email)->delete(); return redirect('/login')->with('success', 'Your password has been changed successfully!'); } return back()->with('error', 'User not found!')->withInput(); } catch (\Exception $e) { Log::error('Error updating password: ' . $e->getMessage()); return back()->with('error', 'Something went wrong while updating your password. Please try again later.'); } } }
Step 5: Create the Views
resources/views/auth/passwords/email.blade.php
@extends('layouts.app') @section('content') <div class="container vh-100 d-flex justify-content-center align-items-center"> <div class="row w-100 justify-content-center"> <div class="col-md-6 col-lg-5"> <div class="card shadow-sm p-4"> <div class="card-header text-center bg-primary text-white fs-4 fw-bold rounded"> Forgot Password </div> <div class="card-body"> @if (session('status')) <div class="alert alert-success alert-dismissible fade show" role="alert"> {{ session('status') }} <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> </div> @endif <form method="POST" action="{{ route('password.email') }}"> @csrf <div class="mb-3"> <label for="email" class="form-label">Email Address</label> <input id="email" type="email" class="form-control @error('email') is-invalid @enderror" name="email" value="{{ old('email') }}" placeholder="Enter your email" required autofocus> @error('email') <div class="invalid-feedback">{{ $message }}</div> @enderror </div> <div class="d-grid gap-2"> <button type="submit" class="btn btn-primary p-2"> Send Password Reset Link </button> </div> </form> <div class="text-center mt-3"> <a href="{{ route('login') }}" class="btn btn-link"> Back to Login </a> </div> </div> </div> </div> </div> </div> @endsection
resources/views/auth/verify.blade.php
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Reset Your Password</title> <style> body { font-family: Arial, sans-serif; background-color: #f4f4f4; margin: 0; padding: 20px; } .container { max-width: 600px; margin: auto; background: #ffffff; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); } .header { background-color: #4361ee; color: white; padding: 10px; text-align: center; border-radius: 8px 8px 0 0; } .content { margin: 20px 0; text-align: center; } .btn { display: inline-block; padding: 10px 20px; background-color: #4361ee; color: white; text-decoration: none; border-radius: 5px; margin-top: 10px; } .footer { text-align: center; color: #777; font-size: 14px; margin-top: 20px; } .ii a[href] { color: white !important; } </style> </head> <body> <div class="container"> <div class="header"> <h2>Reset Your Password</h2> </div> <div class="content"> <p>Click the button below to reset your password:</p> <a href="{{ url('/reset-password/'.$token) }}" class="btn">Reset Password</a> <p class="footer"> If you did not request a password reset, no further action is required. </p> </div> </div> </body> </html>
resources/views/auth/passwords/reset.blade.php
@extends('layouts.app') @section('content') <div class="container vh-100 d-flex justify-content-center align-items-center"> <div class="row w-100 justify-content-center"> <div class="col-md-6 col-lg-5"> <div class="card shadow-sm p-4"> <div class="card-header text-center bg-primary text-white fs-4 fw-bold rounded"> Reset Password </div> <div class="card-body"> @if (session('status')) <div class="alert alert-success alert-dismissible fade show" role="alert"> {{ session('status') }} <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> </div> @endif <form method="POST" action="{{ route('reset-password') }}"> @csrf <input type="hidden" name="token" value="{{ $token }}"> <div class="mb-3"> <label for="email" class="form-label">Email Address</label> <input id="email" type="email" class="form-control @error('email') is-invalid @enderror" name="email" value="{{ $email ?? old('email') }}" placeholder="Enter your email" required autofocus> @error('email') <div class="invalid-feedback">{{ $message }}</div> @enderror </div> <div class="mb-3"> <label for="password" class="form-label">New Password</label> <input id="password" type="password" class="form-control @error('password') is-invalid @enderror" name="password" placeholder="Enter new password" required> @error('password') <div class="invalid-feedback">{{ $message }}</div> @enderror </div> <div class="mb-3"> <label for="password-confirm" class="form-label">Confirm New Password</label> <input id="password-confirm" type="password" class="form-control" name="password_confirmation" placeholder="Confirm new password" required> </div> <div class="d-grid gap-2"> <button type="submit" class="btn btn-primary p-2"> Reset Password </button> </div> </form> <div class="text-center mt-3"> <a href="{{ route('login') }}" class="btn btn-link"> Back to Login </a> </div> </div> </div> </div> </div> </div> @endsection
Final Tips
-
Make sure your email template is clear and secure.
-
Handle errors properly using
@error
session flash messages. -
You can customize the token expiration duration using your own logic.