Add Two-Factor Verification via Email in Laravel Auth

Sumit Talwar · · 4688 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:

Laravel Facades: Simplifying Code and Improve Readability

As an integral part of Laravel, a renowned PHP framework, Facades provide a static interface to classes stored in the application's service container. They serve as static proxies (...)
Harish Kumar

What is Laravel’s Service Container and How to Use Dependency Injection in Laravel App

Dependency injection and inversion of control are vital in clean web development. They make writing maintainable, testable code possible. Laravel is a famous PHP framework that (...)
Harish Kumar

Secure Your SPA with Laravel Sanctum: A Step-by-Step Guide

In today's web development landscape, Single Page Applications (SPAs) are increasingly popular. But securing their interaction with backend APIs is crucial. Laravel Sanctum provides (...)
Harish Kumar

Multi-Authentication with Guards in Laravel

Laravel's robust authentication system provides a powerful mechanism for securing your application. To cater to scenarios where you need different user roles with distinct login (...)
Harish Kumar

Laravel Pint & VS Code: Automate Your Code Formatting

Laravel Pint is an opinionated PHP code style fixer built on top of PHP-CS-Fixer, designed to simplify the process of ensuring clean and consistent code style in Laravel projects. (...)
Harish Kumar

Laravel Clockwork: A Deep Dive into Debugging, Profiling Skills and Best Practices

In the world of web development, building complex applications often comes with the challenge of identifying and resolving performance bottlenecks. This is where a reliable debugging (...)
Harish Kumar