Add Two-Factor Verification via Email in Laravel Auth

Sumit Talwar · · 5808 Views

In this article, we are going to take a look at how we can implement Two-Factor Verification in our Laravel application in truly simple steps. Likewise, we will utilize email as our method for verification of the user’s identity. Let us dive right in.

Step 1. Writing Migration to add two new fields in Users table

Here’s our new migration:

Schema::table('users', function (Blueprint $table) {
    $table->string('two_factor_code')->nullable();
    $table->dateTime('two_factor_expires_at')->nullable();
});

Field two_factor_code will contain a random 6-digit number, and two_factor_expires_at will contain expiration at – for our case, It will expire in 10 minutes.

We also add those fields to app/User.php properties – $fillable array, and $dates:

class User extends Authenticatable
{

    protected $dates = [
        'updated_at',
        'created_at',
        'deleted_at',
        'email_verified_at',
        'two_factor_expires_at',
    ];

    protected $fillable = [
        'name',
        'email',
        'password',
        'created_at',
        'updated_at',
        'deleted_at',
        'remember_token',
        'email_verified_at',
        'two_factor_code',
        'two_factor_expires_at',
    ];

Step 2. Generate code and send it on user login

Add the following method in app/Http/Controllers/Auth/LoginController.php

protected function authenticated(Request $request, $user)
{
    $user->generateTwoFactorCode();
    $user->notify(new TwoFactorCode());
}

This way we override the authenticated() method of core Laravel, and add custom logic of what should happen after the user logs in.

Next, add following method in app/User.php, that  generate the 2FA code:

public function generateTwoFactorCode()
{
    $this->timestamps = false;
    $this->two_factor_code = rand(100000, 999999);
    $this->two_factor_expires_at = now()->addMinutes(10);
    $this->save();
}

Besides setting two-factor code and its expiration time, we additionally indicate that this update should not touch the updated_at column in the users table – we're doing $this->timestamps = false;

Looking back at LoginController above, we call $user->notify() and utilize Laravel's notification system, for that we need to make a Notification class using following command:

php artisan make:notification TwoFactorCode

Now, add following code in app/Notifications/TwoFactorCode.php

Two things to specify here:

  1. Method toMail() parameter $notifiable is automatically allocated as signed in User object, so we can get to users.two_factor_code database column by calling $notifiable->two_factor_code;

  2. We will make route('verify.index') route, which will re-send the code, a bit later.

Step 3. create verification form with Middleware

To do that, we will generate a Middleware:

php artisan make:middleware TwoFactor

Now, add the following code in the app/Http/Middleware/TwoFactor.php

class TwoFactor
{

    public function handle($request, Closure $next)
    {
        $user = auth()->user();

        if(auth()->check() && $user->two_factor_code)
        {
            if($user->two_factor_expires_at->lt(now()))
            {
                $user->resetTwoFactorCode();
                auth()->logout();

                return redirect()->route('login')
                    ->withMessage('The two factor code has expired. Please login again.');
            }

            if(!$request->is('verify*'))
            {
                return redirect()->route('verify.index');
            }
        }

        return $next($request);
    }
}

Next, we check if there is a two-factor code set. If it is, then next, check if it isn’t expired. If it has expired, we reset it and redirect it back to the login page. If it’s still active, we redirect back to the verification form.

All in all, if users.two_factor_code is unfilled or empty, at that point it's confirmed and the user can move further.

Add the following resetTwoFactorCode() in the app/User.php

public function resetTwoFactorCode()
{
    $this->timestamps = false;
    $this->two_factor_code = null;
    $this->two_factor_expires_at = null;
    $this->save();
}

Next, add the middleware class  as an “alias” name, inside app/Http/Kernel.php:

class Kernel extends HttpKernel
{
    // ...

    protected $routeMiddleware = [
        // ... more middlewares

        'twofactor'     => \App\Http\Middleware\TwoFactor::class,
    ];
}

Now, we need to assign this twofactor Middleware to some routes.

Route::group([
    'prefix' => 'admin', 
    'as' => 'admin.', 
    'namespace' => 'Admin', 
    'middleware' => ['auth', 'twofactor']
], function () {
    Route::resource('permissions', 'PermissionsController');
    Route::resource('roles', 'RolesController');
    Route::resource('users', 'UsersController');
});

Step 4. Verification page Controller/View

At this point, any request to any URL will redirect to code verification.
For that, we will have two extra public routes:

Route::get('verify/resend', 'Auth\TwoFactorController@resend')->name('verify.resend');
Route::resource('verify', 'Auth\TwoFactorController')->only(['index', 'store']);

Add the following code in the app/Http/Controllers/Auth/TwoFactorController.php

class TwoFactorController extends Controller
{
    public function index() 
    {
        return view('auth.twoFactor');
    }

    public function store(Request $request)
    {
        $request->validate([
            'two_factor_code' => 'integer|required',
        ]);

        $user = auth()->user();

        if($request->input('two_factor_code') == $user->two_factor_code)
        {
            $user->resetTwoFactorCode();

            return redirect()->route('admin.home');
        }

        return redirect()->back()
            ->withErrors(['two_factor_code' => 
                'The two factor code you have entered does not match']);
    }

    public function resend()
    {
        $user = auth()->user();
        $user->generateTwoFactorCode();
        $user->notify(new TwoFactorCode());

        return redirect()->back()->withMessage('The two factor code has been sent again');
    }
}

Here index() method returns the view for the main form. The store() method to verify the code and resend() method is for re-generating and sending new code in the email.

Add verification form – in resources/views/auth/twoFactor.blade.php:

@if(session()->has('message'))
    <p class="alert alert-info">
        {{ session()->get('message') }}
    </p>
@endif

<form method="POST" action="{{ route('verify.store') }}">
    {{ csrf_field() }}
    <h1>Two Factor Verification</h1>
    <p class="text-muted">
        You have received an email which contains two factor login code.
        If you haven't received it, press <a href="{{ route('verify.resend') }}">here</a>.
    </p>

    <div class="input-group mb-3">
        <div class="input-group-prepend">
            <span class="input-group-text">
                <i class="fa fa-lock"></i>
            </span>
        </div>
        <input name="two_factor_code" type="text" 
            class="form-control{{ $errors->has('two_factor_code') ? ' is-invalid' : '' }}" 
            required autofocus placeholder="Two Factor Code">
        @if($errors->has('two_factor_code'))
            <div class="invalid-feedback">
                {{ $errors->first('two_factor_code') }}
            </div>
        @endif
    </div>

    <div class="row">
        <div class="col-6">
            <button type="submit" class="btn btn-primary px-4">
                Verify
            </button>
        </div>
    </div>
</form>

That is it! We have our full logic to send a two-factor code through email.

1

Please login or create new account to add your comment.

1 comment
fnvn12
fnvn12 ·

missing code in "Now, add following code in app/Notifications/TwoFactorCode.php"

You may also like:

Building a Real-Time Chat App with Laravel Reverb and Nuxt 3

Building a real-time chat application is a great way to understand the power of WebSockets and real-time communication. In this tutorial, we will walk through creating a Real-Time (...)
Harish Kumar

How to Set Up Nuxt 3 Authentication with Laravel Sanctum (Step-by-Step Guide)

In modern web development, securing your application’s authentication process is a top priority. For developers building Single Page Applications (SPA) or Server-Side Rendered (...)
Harish Kumar

Laracon US 2024: Laravel 11 Minor Features That Enhance Performance

At Laracon US 2024, Taylor Otwell and the Laravel team introduced a series of "minor" features for Laravel 11 that are anything but minor. These enhancements, while not headline-grabbing (...)
Harish Kumar

PHP OPCache: The Secret Weapon for Laravel Performance Boost

OPCache, a built-in PHP opcode cache, is a powerful tool for significantly improving Laravel application speed. This guide will demonstrate how to effectively utilize OPCache to (...)
Harish Kumar

How to Use DTOs for Cleaner Code in Laravel, Best Practices and Implementation Guide

When developing APIs in Laravel, ensuring your responses are clear, concise, and consistent is crucial for creating a maintainable and scalable application. One effective way to (...)
Harish Kumar

Data Type Validation in Laravel Collections with the `ensure()` Method

Before moving on to the ensure() method, let us first know what Laravel Collections actually are. These are wrappers of PHP arrays, offering a fluent and helpful interface in interacting (...)
Harish Kumar