• Home
  • Blogs
  • Laravel 12 — Custom Admin Password Reset & Account Auth
Blog Details

Laravel 12 — Custom Admin Password Reset & Account Auth

Authentication for admin panels must be separate, secure, and simple to maintain. In this tutorial you’ll learn how to implement a complete admin forgot-password & reset-password flow for a custom admins table and custom admin guard in Laravel 12. 


Table of contents

  1. Why custom admin password reset?

  2. How the flow works (theory)

  3. Database: password_reset_tokens migration

  4. Routes (admin group)

  5. Controller methods (forget, submit, show reset, submit reset)

  6. Important Blade snippets (Forgot Password, Reset Password, Logout link)

  7. Email template (HTML)

  8. Security best practices

  9. Testing & debugging checklist

  10. FAQ 


1. Why a custom admin password reset?

Most Laravel apps use the default users flow. But admin accounts should be isolated:

  • Admins have elevated privileges — losing an admin password is high risk.

  • You may want different token lifetime, separate email templates, throttling and audit logs.

  • A separate admins table and guard prevents accidental cross-authentication and lets you customize behavior for backend users.

This tutorial continues from your existing admin auth (separate admins table and admin guard) and shows a production-ready forgot/reset password flow.


2. How the flow works — theory (quick)

  1. Admin clicks Forgot password and submits email.

  2. Server checks that email exists in admins.

  3. Server creates a one-time token (we’ll store a hashed token in DB and send the plain token in the email link).

  4. Email contains link: /admin/login/reset-password/{token}.

  5. When admin opens link, server finds matching hashed token and checks expiry.

  6. Admin submits new password — server verifies token again and updates password (hashed with bcrypt).

  7. All tokens for that email are deleted.

Why hash token in DB? If your DB is leaked, hashed tokens prevent attackers from using stored tokens directly. You still send the plain token in email only. Verification uses Hash::check().

Token expiry & rate-limiting are essential to prevent abuse.


3. Database migration: password_reset_tokens

Create migration:

 
php artisan make:migration create_password_reset_tokens_table --create=password_reset_tokens

Migration file (important parts):

 
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreatePasswordResetTokensTable extends Migration
{
    public function up()
    {
        Schema::create('password_reset_tokens', function (Blueprint $table) {
            $table->id();
            $table->string('email')->index();
            $table->string('token', 128)->unique(); // we'll store hashed token
            $table->timestamp('created_at')->nullable();
        });
    }

    public function down()
    {
        Schema::dropIfExists('password_reset_tokens');
    }
}

Run:

 
php artisan migrate 

4. Routes — admin group

Add these to routes/web.php inside the admin prefix:

 
use App\Http\Controllers\Admin\AuthController;

Route::prefix('admin')->group(function () {
    Route::get('login', [AuthController::class, 'login'])->name('admin.login');
    Route::post('login/submit', [AuthController::class, 'login_submit'])->name('admin.login.submit');

    // Forgot & reset
    Route::get('login/forget-password', [AuthController::class, 'forgetpass'])->name('admin.forgetpass');
    Route::post('login/forget-password/submit', [AuthController::class, 'submitforgetpass'])->name('admin.forgetpass.submit');

    Route::get('login/reset-password/{token}', [AuthController::class, 'show_reset_pass_form'])->name('admin.reset.password.get');
    Route::post('login/reset-password', [AuthController::class, 'submit_reset_pass_form'])->name('admin.reset.password.post');

    // Logout (POST)
    Route::post('logout', [AuthController::class, 'logout'])->name('admin.logout');
});

Notes:

  • Logout must be POST for CSRF protection.

  • Reset POST accepts token via hidden input (safer pattern).


5. Controller: AuthController methods

Add these methods in App\Http\Controllers\Admin\AuthController. Required imports (top of file):

 
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Hash;
use Carbon\Carbon;
use Illuminate\Support\Facades\RateLimiter;
use App\Models\Admin;

5.1 Show forgot password view

public function forgetpass()
{
    return check_admin_auth('adminpanel.auth.forget_password'); // your helper that redirects if logged in
}

5.2 Handle forgot password submit

 
public function submitforgetpass(Request $request)
{
    $request->validate(['email' => 'required|email']);

    $email = $request->email;

    // Confirm admin exists
    $admin = DB::table('admins')->where('email', $email)->first();
    if (!$admin) {
        toastr()->error('The provided email was not found.');
        return redirect()->back()->withInput();
    }

    // Rate limit requests per email (3 attempts per 30 minutes)
    $key = 'admin-forget-password|' . $email;
    if (RateLimiter::tooManyAttempts($key, 3)) {
        toastr()->warning('Too many requests. Please wait before trying again.');
        return redirect()->back();
    }
    RateLimiter::hit($key, 60 * 30); // 30 minutes

    // Remove existing tokens for this email
    DB::table('password_reset_tokens')->where('email', $email)->delete();

    // Create token (store hashed in DB)
    $plainToken = Str::random(64);
    $hashedToken = Hash::make($plainToken);

    DB::table('password_reset_tokens')->insert([
        'email' => $email,
        'token' => $hashedToken,
        'created_at' => Carbon::now(),
    ]);

    // Send email with plain token
    Mail::send('adminpanel.emails.forget_password', ['token' => $plainToken, 'email' => $email], function ($message) use ($email) {
        $message->to($email);
        $message->subject('Reset Password');
    });

    toastr()->success('We have emailed your password reset link!');
    return redirect()->back();
}

5.3 Show reset password form

 
public function show_reset_pass_form($token)
{
    // find hashed token by checking all tokens (recent) and checking against Hash::check
    $records = DB::table('password_reset_tokens')->orderBy('created_at', 'desc')->get();

    $found = null;
    foreach ($records as $r) {
        if (Hash::check($token, $r->token)) {
            $found = $r;
            break;
        }
    }

    if (!$found) {
        toastr()->error('Token has expired or is invalid');
        return redirect()->route('admin.login');
    }

    // check expiry (60 minutes)
    if (Carbon::parse($found->created_at)->addMinutes(60)->isPast()) {
        DB::table('password_reset_tokens')->where('email', $found->email)->delete();
        toastr()->error('Token has expired. Please request a new reset link.');
        return redirect()->route('admin.forgetpass');
    }

    return view('adminpanel.auth.reset_password', ['token' => $token, 'email' => $found->email]);
}

5.4 Handle reset password submit

 
public function submit_reset_pass_form(Request $request)
{
    $request->validate([
        'email' => 'required|email|exists:admins,email',
        'token' => 'required|string',
        'password' => 'required|string|min:8|confirmed',
    ]);

    $email = $request->email;
    $plainToken = $request->token;

    // Find token record for this email
    $records = DB::table('password_reset_tokens')->where('email', $email)->get();
    $match = null;
    foreach ($records as $r) {
        if (Hash::check($plainToken, $r->token)) {
            $match = $r;
            break;
        }
    }

    if (!$match) {
        toastr()->error('Token has expired or is invalid');
        return redirect()->route('admin.forgetpass');
    }

    if (Carbon::parse($match->created_at)->addMinutes(60)->isPast()) {
        DB::table('password_reset_tokens')->where('email', $email)->delete();
        toastr()->error('Token has expired. Please request a new reset link.');
        return redirect()->route('admin.forgetpass');
    }

    // Update password
    Admin::where('email', $email)->update([
        'password' => Hash::make($request->password),
    ]);

    // Delete tokens for this email
    DB::table('password_reset_tokens')->where('email', $email)->delete();

    toastr()->success('Your password has been successfully updated.');
    return redirect()->route('admin.login');
}

5.5 Logout method (if not present)

 
public function logout()
{
    Auth::guard('admin')->logout();
    toastr()->success('Admin logged out successfully.');
    return redirect()->route('admin.login');
}

6. Blade snippets

6.1 Login password field

 
<div class="form-group">
    <div class="form-label-group">
        <label class="form-label" for="password">Password*</label>
        <a class="link link-primary link-sm" href="{{ route('admin.forgetpass') }}">Forgot password?</a>
    </div>
    <div class="form-control-wrap">
        <a href="#" class="form-icon form-icon-right passcode-switch lg" data-target="password">
            <em class="passcode-icon icon-show icon ni ni-eye"></em>
            <em class="passcode-icon icon-hide icon ni ni-eye-off"></em>
        </a>
        <input type="password" required name="password" id="password" class="form-control form-control-lg" placeholder="Enter your passcode">
    </div>
    <div class="text-danger">
        @error('password') {{ $message }} @enderror
    </div>
</div>

6.2 Forgot Password Blade (adminpanel/auth/forget_password.blade.php)

 
<form method="POST" action="{{ route('admin.forgetpass.submit') }}">
    @csrf
    <div class="form-group">
        <label for="email">Email Address*</label>
        <input id="email" class="form-control" type="email" name="email" value="{{ old('email') }}" required autofocus placeholder="Email Address*">
        @error('email') <div class="text-danger">{{ $message }}</div> @enderror
    </div>
    <div class="form-group">
        <button type="submit" class="btn btn-primary">Reset Now</button>
    </div>
</form>

6.3 Reset Password Blade (adminpanel/auth/reset_password.blade.php)

 
<form method="POST" action="{{ route('admin.reset.password.post') }}">
    @csrf
    <input type="hidden" name="token" value="{{ $token }}">
    <input type="hidden" name="email" value="{{ old('email', $email ?? '') }}">

    <div class="form-group">
        <label for="email">Email*</label>
        <input id="email" class="form-control" type="email" name="email" value="{{ old('email', $email ?? '') }}" required autofocus>
        @error('email') <div class="text-danger">{{ $message }}</div> @enderror
    </div>

    <div class="form-group">
        <label for="password">New Password*</label>
        <input id="password" class="form-control" type="password" name="password" required autocomplete="new-password" placeholder="Password">
        @error('password') <div class="text-danger">{{ $message }}</div> @enderror
    </div>

    <div class="form-group">
        <label for="password_confirmation">Confirm Password*</label>
        <input id="password_confirmation" class="form-control" type="password" name="password_confirmation" required placeholder="Confirm Password">
    </div>

    <div class="form-group">
        <button type="submit" class="btn btn-primary">Reset Now</button>
    </div>
</form>

6.4 Logout link 

 
<div class="dropdown-inner">
    <ul class="link-list">
        <li>
            <a href="#" onclick="event.preventDefault(); document.getElementById('logout-form').submit();">
                <em class="icon ni ni-signout"></em><span>Sign out</span>
            </a>
        </li>
    </ul>

    <form method="POST" action="{{ route('admin.logout') }}" id="logout-form" style="display: none;">
        @csrf
    </form>
</div>

7. Email template

resources/views/adminpanel/emails/forget_password.blade.php:

 
<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <title>Reset Password</title>
</head>
<body>
    <h2>Password Reset Request</h2>
    <p>Hello,</p>
    <p>You recently requested to reset your admin password. Click the button below to reset it. The link expires in 60 minutes.</p>

    @php
        $resetUrl = route('admin.reset.password.get', ['token' => $token]);
    @endphp

    <p>
        <a href="{{ $resetUrl }}" style="display:inline-block;padding:10px 20px;border-radius:4px;background:#22BC66;color:#fff;text-decoration:none;">
            Reset your password
        </a>
    </p>

    <p>If you did not request this, please ignore this email or contact support.</p>
    <p>Thanks,<br/>Web Tech Tutorials</p>
</body>
</html>

8. Security best practices

  • Hash tokens in DB. We store Hash::make($token) and validate with Hash::check($plainToken, $hashedToken).

  • Short expiry. 60 minutes is standard. Enforce expiry checks.

  • Rate limit resets. Use RateLimiter or throttle middleware to prevent spam.

  • Delete tokens on success. Remove all tokens for the email after successful reset.

  • Strong password rules. Minimum 8 characters; consider adding regex rules for complexity.

  • HTTPS only. Ensure APP_URL uses https:// so email links use HTTPS.

  • Audit logs. Log when reset is requested and completed (admin email, IP, timestamp).

  • Email deliverability. Use SMTP provider (SendGrid, Mailgun) and test with Mailtrap during development.

  • CSRF. Keep forms protected with @csrf.

  • Logout guard. Use Auth::guard('admin')->logout() and protect admin routes with auth:admin.


9. Testing & debugging checklist

  • Migrate password_reset_tokens and create a test admin entry.

  • Submit forget-password for the admin email; confirm DB has hashed token and created_at.

  • Receive email (use Mailtrap in dev); click link.

  • Open reset form; submit new password; verify admin can login with new password.

  • Confirm tokens cleared after reset.

  • Test invalid/expired token behavior.

  • Attempt rate-limit exceed to ensure message shows.

  • Test logout link works and returns to login.


10. FAQs

Q: Should I use Laravel Password Broker instead?
A: For long-term maintainability, use Laravel’s Password Broker configured for the admins provider — it handles many details (throttling, expiry). The custom approach above gives finer control and is fine if you need a separate flow.

Q: Why store hashed token instead of plain token?
A: Hashing tokens prevents token misuse if DB leaks. Only the plain token sent by email is usable and verified using Hash::check().

Q: How long should tokens live?
A: 60 minutes is common. Shorter lifetimes are more secure; longer are more user friendly.

Leave A Reply

Your email address will not be published. Required fields are marked

Ahmad

Ahmad Raza

Hi, I'm Ahmad Raza — a passionate Web Developer and Laravel enthusiast.