Add Two-Factor Verification via Email in Laravel Auth

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

Part #3: Rule objects based custom validation in Laravel

Laravel comes with multiple ways to add custom validation rules to validate form request inputs. I have already explained some of the ways in the following article links:
Harish Kumar

Part #2: How to use Laravel's Validator::extend method for custom validation

Validation is important in any application as it validates a form before performing actions on it. It allows the user to know their input is accurate and confident about the operation (...)
Harish Kumar

Part #1: Closure-based Custom Laravel Validation

While I was working with Laravel, validation using closure came to my mind, and I know it will be helpful to you. This tutorial assists you with all what is the difference between (...)
Harish Kumar

How to use the enumerations(Enums) of PHP 8.1 in Laravel?

The release of PHP 8.1 brings native enumerations to PHP. There is no more requirement for custom solutions in your Laravel projects since the Laravel v8.69 release has you back. (...)
Harish Kumar

Mobile App Development Process

With businesses adopting a mobile-first approach and the growing number of mobile apps, successful mobile app development seems like a quest. But it’s the process that determines (...)
Narola Infotech

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