Add Two-Factor Verification via Email in Laravel Auth

Sumit Talwar · · 1050 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\[email protected]')->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.

0

Please login or create new account to add your comment.

0 comments
You may also like:

What are Laravel Macros and How to Extending Laravel’s Core Classes using Macros with example?

Laravel Macros are a great way of expanding Laravel's core macroable classes and add additional functionality needed for your application. In simple word, Laravel Macro is an (...)
Harish Kumar

Install Laravel Valet Linux+ development environment on Ubuntu System

The official Laravel Valet development environment is great if you are an Apple user. But there is no official Valet for Linux or Window system.
Harish Kumar

Laravel Sanctum API Token Authentication Tutorial with example

Laravel Sanctum is a popular package for API Token Authentication. There are many other packages available to authenticate the APIs request in Laravel. For example, We are already (...)
Harish Kumar

Create SPA authentication Using Laravel Sanctum and Vue.js

In this guide, we will focus on SPA authentication in a simple Vue.js app using Laravel Sanctum. Laravel Sanctum provides a featherweight authentication system for SPAs (single (...)
Harish Kumar

Create API Authentication with Laravel Passport

In this article, we'll see how to implement restful API authentication using Laravel Passport. You should have experience working with Laravel as this is not an introductory tutorial. (...)
Sumit Talwar

Laravel Themer: multi-theme support for Laravel application

This Laravel Themer package adds multi-theme support to your application. This theme package improves any application while allowing the freedom to organize and maintain your app's (...)
Harish Kumar