In this blog post, Iβll walk you through building a custom "Forgot Password" and "Reset Password" feature in Laravel β without using Laravel Breeze/Fortify's default implementation. This solution gives you full control over the password reset process, using Laravel's built-in features like mailing, routing, validation, and the password resets table.
Step 1: Create Routes
Define the following routes in your web.php:
use App\Http\Controllers\ForgotPasswordController;
Route::get('forget-password', [ForgotPasswordController::class, 'showForgetPasswordForm'])->name('forget.password.get');
Route::post('forget-password', [ForgotPasswordController::class, 'submitForgetPasswordForm'])->name('forget.password.post');
Route::get('reset-password/{token}', [ForgotPasswordController::class, 'showResetPasswordForm'])->name('reset.password.get');
Route::post('reset-password', [ForgotPasswordController::class, 'submitResetPasswordForm'])->name('reset.password.post');
π₯ Step 2: Create Controller
Create ForgotPasswordController.php inside App\Http\Controllers
php artisan make:controller Auth/ForgotPasswordController
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
use App\Models\User;
use Carbon\Carbon;
class ForgotPasswordController extends Controller
{
public function showForgetPasswordForm()
{
return view('auth.forgot-password');
}
public function submitForgetPasswordForm(Request $request)
{
$request->validate([
'email' => 'required|email|exists:users',
]);
$token = Str::random(64);
DB::table('password_resets')->insert([
'email' => $request->email,
'token' => $token,
'created_at' => Carbon::now(),
]);
Mail::send('email.forgetPassword', ['token' => $token], function ($message) use ($request) {
$message->to($request->email);
$message->subject('Reset Password');
});
return back()->with('status', 'We have e-mailed your password reset link!');
}
public function showResetPasswordForm($token)
{
return view('auth.reset-password', ['token' => $token]);
}
public function submitResetPasswordForm(Request $request)
{
$request->validate([
'email' => 'required|email|exists:users',
'password' => 'required|string|min:6|confirmed',
'password_confirmation' => 'required'
]);
$updatePassword = DB::table('password_resets')
->where(['email' => $request->email, 'token' => $request->token])
->first();
if (!$updatePassword) {
return back()->withInput()->with('status', 'Invalid token!');
}
User::where('email', $request->email)
->update(['password' => Hash::make($request->password)]);
DB::table('password_resets')->where(['email' => $request->email])->delete();
return redirect('/login')->with('status', 'Your password has been changed!');
}
}
Create "password_resets" table
php artisan make:migration create_password_resets_table
Schema::create('password_resets', function (Blueprint $table) {
$table->string('email')->index();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
php artisan migrate
βοΈ Step 3: Create Email View
Create resources/views/email/forgetPassword.blade.php:
<h2>Reset Your Password</h2>
<p>You can reset your password from the link below:</p>
<a href="{{ route('reset.password.get', $token) }}">Reset Password</a>
Create resources/views/auth/forgot-password.blade.php:
π Step 4: Forgot Password Blade View
<x-guest-layout>
<x-jet-authentication-card>
<x-slot name="logo">
<x-jet-authentication-card-logo />
</x-slot>
<div class="mb-4 text-sm text-gray-600">
{{ __('Forgot your password? No problem.') }}
</div>
@if (session('status'))
<div class="mb-4 font-medium text-sm text-green-600">
{{ session('status') }}
</div>
@endif
<x-jet-validation-errors class="mb-4" />
<form method="POST" action="{{ route('forget.password.post') }}">
@csrf
<div class="block">
<x-jet-label for="email" value="Email" />
<x-jet-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autofocus />
</div>
<div class="flex items-center justify-end mt-4">
<x-jet-button>
{{ __('Email Password Reset Link') }}
</x-jet-button>
</div>
</form>
</x-jet-authentication-card>
</x-guest-layout>
π Step 5: Reset Password Blade View
Create resources/views/auth/reset-password.blade.php:
<x-guest-layout>
<x-jet-authentication-card>
<x-slot name="logo">
<x-jet-authentication-card-logo />
</x-slot>
<x-jet-validation-errors class="mb-4" />
@if (session('status'))
<div class="mb-4 font-medium text-sm text-green-600">
{{ session('status') }}
</div>
@endif
<form method="POST" action="{{ route('reset.password.post') }}">
@csrf
<input type="hidden" name="token" value="{{ $token }}">
<div class="block">
<x-jet-label for="email" value="Email" />
<x-jet-input id="email" class="block mt-1 w-full" type="email" name="email" required autofocus />
</div>
<div class="mt-4">
<x-jet-label for="password" value="Password" />
<x-jet-input id="password" class="block mt-1 w-full" type="password" name="password" required />
</div>
<div class="mt-4">
<x-jet-label for="password_confirmation" value="Confirm Password" />
<x-jet-input id="password_confirmation" class="block mt-1 w-full" type="password" name="password_confirmation" required />
</div>
<div class="flex items-center justify-end mt-4">
<x-jet-button>
{{ __('Reset Password') }}
</x-jet-button>
</div>
</form>
</x-jet-authentication-card>
</x-guest-layout>
π Final Notes
Make sure your password_resets table exists (migrate if needed).
Configure mail settings in .env to enable email sending.
You can customize validation messages, email views, and token expiration as needed.
β
Conclusion
This custom Laravel forgot/reset password implementation is a great way to tailor the password reset process to fit your own application. Itβs lightweight, flexible, and easy to expand.
Let me know in the comments if you want a custom token expiration, rate limiting, or a job queue for sending emails!
Reset Password Mail