Secure Your Laravel App — 2FA By Email | Easy Email OTP For Login

Secure Your Laravel App — 2FA by Email | Easy Email OTP for Login
Avinash Chaurasiya Aug 30, 2025 86
2FA,Email

Secure Your Laravel App — 2FA by Email | Easy Email OTP for Login


Implement Email-based 2FA in Laravel

Add an extra security layer to your Laravel application by sending one-time passwords (OTPs) to users' email addresses. This guide explains step by step how to set up the migration, OTP generation, mail class, verification flow, and a simple Blade form.

Process Overview

  1. User logs in with email and password.
  2. The server generates an OTP, stores it in the email_otps table, and sends it via email.
  3. User enters the OTP on the verification page.
  4. The server validates the OTP (expiry and usage flag) and logs the user in if valid.

1. Migration for OTPs

Create a migration create_email_otps_table and run php artisan migrate.


// migration: create_email_otps_table
public function up()
{
    Schema::create('email_otps', function (Blueprint $table) {
        $table->id();
        $table->unsignedBigInteger('user_id');
        $table->string('otp');
        $table->timestamp('expires_at');
        $table->boolean('is_used')->default(false);
        $table->timestamps();

        $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
    });
}
  

2. Generate OTP on Login

After login, generate the OTP, save it, send it via email, and log the user out until verification.


use App\Models\EmailOtp;
use Illuminate\Support\Facades\Mail;
use App\Mail\SendOtpMail;
use Carbon\Carbon;

public function login(Request $request)
{
    $credentials = $request->only('email', 'password');

    if (Auth::attempt($credentials)) {
        $user = Auth::user();

        // Generate OTP
        $otp = rand(100000, 999999);

        EmailOtp::create([
            'user_id' => $user->id,
            'otp' => $otp,
            'expires_at' => Carbon::now()->addMinutes(5),
        ]);

        // Send OTP
        Mail::to($user->email)->send(new SendOtpMail($otp));

        Auth::logout();

        return redirect()->route('verify.otp')->with('info', 'OTP has been sent to your email.');
    }

    return back()->withErrors(['email' => 'Invalid credentials']);
}
  

3. Mail Class for OTP

Create a mail class with php artisan make:mail SendOtpMail and use a simple view:


public $otp;
public function __construct($otp) { $this->otp = $otp; }
public function build()
{
    return $this->subject('Your OTP Code')
                ->view('emails.otp')
                ->with(['otp' => $this->otp]);
}
  


Your OTP Code


Use the following OTP to complete login:


{{ $otp }}


This code will expire in 5 minutes.


  

4. Routes & OTP Verification

Add the following routes:


Route::get('/verify-otp', [AuthController::class, 'showOtpForm'])->name('verify.otp');
Route::post('/verify-otp', [AuthController::class, 'verifyOtp'])->name('verify.otp.submit');
  

Verification logic:


public function verifyOtp(Request $request)
{
    $request->validate(['otp' => 'required|numeric']);

    $otpRecord = EmailOtp::where('otp', $request->otp)
        ->where('expires_at', '>', now())
        ->where('is_used', false)
        ->first();

    if ($otpRecord) {
        $otpRecord->update(['is_used' => true]);
        Auth::loginUsingId($otpRecord->user_id);
        return redirect()->route('dashboard')->with('success', 'Login successful');
    }

    return back()->withErrors(['otp' => 'Invalid or expired OTP']);
}
  

5. OTP Form (Blade)



@csrf

  

Notes / Best Practices

  • OTP expiry should be between 3–10 minutes (example uses 5 minutes).
  • Throttle resend attempts (rate limiting) to prevent abuse.
  • Consider storing hashed OTPs for additional security (hash before saving and validate with Hash::check).
  • Log OTP attempts and notify users in case of multiple failed attempts.

For production, you may also want to add Resend OTP functionality, rate-limiting middleware, and secure OTP hashing.

WhatsApp Us