Laravel Two Factor Authentication (2FA) via Email

Hello artisans, in this tutorial I will show you how to add Two Factor Authentication (2FA) with sending emails in Laravel. We will use google smtp for sending mails with code and later use that one for verify users. So, no more talk and dive into the topic.

Note: Tested on Laravel 8.65.

Table of Contents

  1. Install Laravel and Basic Config
  2. Setup Migrations
  3. Setup Models
  4. Create and Configure Middleware
  5. Setup Controllers
  6. Add Routes
  7. Create and Configure Mail Class
  8. Setup Blade Files
  9. Setup .env File
  10. Output

Install Laravel and Basic Config

Each Laravel project needs this thing. For that you can see the very beautiful article on this topic from here: Install Laravel and Basic Configurations.

Setup Migrations

Here we need to create a table which will store all the codes which we are to use for verify. So fire the below command in your terminal.

php artisan make:model UserCode -mcr

This command will create a model named UserCode.php, a controller called UserCodeController and a migration files called user_codes. We will now setup the migration for now. So, open up the file and paste the below code.

database/migrations/2021_11_20_103841_create_user_codes_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateUserCodesTable extends Migration
{
    public function up()
    {
        Schema::create('user_codes', function (Blueprint $table) {
            $table->id();
            $table->foreignId('user_id')->constrained()->cascadeOnDelete();
            $table->string('code');
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('user_codes');
    }
}

Now we have to run this migration. So fire the below command in your terminal.

php artisan make:model UserCode -mcr

Setup Models

First we will update our default User.php model. So open and Update the file using below code

app/Models/User.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\Mail;
use Laravel\Sanctum\HasApiTokens;
use App\Mail\SendCodeMail;

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;

    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    protected $hidden = [
        'password',
        'remember_token',
    ];

    protected $casts = [
        'email_verified_at' => 'datetime',
    ];

    public function generateCode()
    {
        $code = rand(1000, 9999);

        UserCode::updateOrCreate(
            [ 'user_id' => auth()->id() ],
            [ 'code' => $code ]
        );

        try {

            $details = [
                'title' => 'Mail from shouts.dev',
                'code' => $code
            ];

            Mail::to(auth()->user()->email)->send(new SendCodeMail($details));

        } catch (\Exception $e) {
            info("Error: ". $e->getMessage());
            dd($e);
        }
    }
}

Now, we will update the The UserCode.php which we will create at the step2. So open up the file and paste the below code

app/Models/UserCode.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class UserCode extends Model
{
    use HasFactory;

    protected $fillable = [
        'user_id',
        'code',
    ];
}

Create and Configure Middleware

First we need to create a middleware for check user has 2FA or not. So, fire the below command to create w new middleware.

php artisan make:middleware Check2FA

It will create a middleware under app\Http\Middleware. Open the file and paste the below code.

app/Http/Middleware/Check2FA.php
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Session;

class Check2FA
{
    public function handle(Request $request, Closure $next)
    {
        if (!Session::has('user_2fa')) {
            return redirect()->route('2fa.index');
        }
        return $next($request);
    }
}

Now, we need to register the middleware in kernel.php. Open the kernel.php and update its $routeMiddleware property with the following code.

app/Http/Kernel.php
protected $routeMiddleware = [
        'auth' => \App\Http\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
        'can' => \Illuminate\Auth\Middleware\Authorize::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
        'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
        'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
        '2fa' => \App\Http\Middleware\Check2FA::class,
    ];

Setup Controllers

Here, we need first modify our Default LoginController by replacing with the below code.

app/Http/Controllers/Auth/LoginController.php
<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class LoginController extends Controller
{
    use AuthenticatesUsers;

    protected $redirectTo = RouteServiceProvider::HOME;

    public function __construct()
    {
        $this->middleware('guest')->except('logout');
    }

    public function login(Request $request)
    {
        $request->validate([
            'email' => 'required',
            'password' => 'required',
        ]);

        $credentials = $request->only('email', 'password');
        if (Auth::attempt($credentials)) {

            auth()->user()->generateCode();

            return redirect()->route('2fa.index');
        }

        return redirect("login")->withSuccess('Oppes! You have entered invalid credentials');
    }
}

Now, we will update the The UserCodeController.php which we will create at the step2. So open up the file and paste the below code

app/Http/Controllers/Auth/LoginController.php
<?php

namespace App\Http\Controllers;

use App\Models\UserCode;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Session;

class UserCodeController extends Controller
{
    public function index()
    {
        return view('2fa');
    }

    public function create()
    {
        //
    }

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

        $find = UserCode::where('user_id', auth()->id())
            ->where('code', $request->code)
            ->where('updated_at', '>=', now()->subMinutes(2))
            ->first();

        if (!is_null($find)) {
            Session::put('user_2fa', auth()->id());
            return redirect()->route('home');
        }

        return back()->with('error', 'You entered wrong code.');
    }

    public function resend()
    {
        auth()->user()->generateCode();

        return back()->with('success', 'We sent you code on your email.');
    }
}

Add Routes

Hew we need to define some routes that we will use to redirect user. So paste the below code into web.php

routes/web.php
<?php

use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    return view('welcome');
});

Auth::routes();

Route::get('/home', [App\Http\Controllers\HomeController::class, 'index'])->name('home');
Route::get('2fa', [App\Http\Controllers\UserCodeController::class, 'index'])->name('2fa.index');
Route::post('2fa', [App\Http\Controllers\UserCodeController::class, 'store'])->name('2fa.post');
Route::get('2fa/reset', [App\Http\Controllers\UserCodeController::class, 'resend'])->name('2fa.resend');

Create and Configure Mail Class

At first ween need to create a Mail class by firing the below code

php artisan make:mail SendCodeMail

This will create a file under app\Mail. Open the file and paste the below code

app/Mail/SendCodeMail.php
<?php

namespace App\Mail;

use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;

class SendCodeMail extends Mailable
{
    use Queueable, SerializesModels;

    private $details;

    public function __construct($details)
    {
        $this->details = $details;
    }

    public function build()
    {
        $data = $this->details;
        return $this->subject('Mail from shouts.dev')
            ->view('emails.code',$data);
    }
}

Setup Blade Files

Create two blade files

  1. 2fa.blade.php
  2. email_code.blade.php

Now paste the below codes into their corresponding files.

resources/views/2fa.blade.php
@extends('layouts.app')

@section('content')
    <div class="container">
        <div class="row justify-content-center">
            <div class="col-md-8">
                <div class="card">
                    <div class="card-header">2FA Verification</div>

                    <div class="card-body">
                        <form method="POST" action="{{ route('2fa.post') }}">
                            @csrf

                            <p class="text-center">We sent code to email : {{ substr(auth()->user()->email, 0, 5) . '******' . substr(auth()->user()->email,  -2) }}</p>

                            @if ($message = Session::get('success'))
                                <div class="row">
                                    <div class="col-md-12">
                                        <div class="alert alert-success alert-block">
                                            <button type="button" class="close" data-dismiss="alert">×</button>
                                            <strong>{{ $message }}</strong>
                                        </div>
                                    </div>
                                </div>
                            @endif

                            @if ($message = Session::get('error'))
                                <div class="row">
                                    <div class="col-md-12">
                                        <div class="alert alert-danger alert-block">
                                            <button type="button" class="close" data-dismiss="alert">×</button>
                                            <strong>{{ $message }}</strong>
                                        </div>
                                    </div>
                                </div>
                            @endif

                            <div class="form-group row">
                                <label for="code" class="col-md-4 col-form-label text-md-right">Code</label>

                                <div class="col-md-6">
                                    <input id="code" type="number" class="form-control @error('code') is-invalid @enderror" name="code" value="{{ old('code') }}" required autocomplete="code" autofocus>

                                    @error('code')
                                    <span class="invalid-feedback" role="alert">
                                        <strong>{{ $message }}</strong>
                                    </span>
                                    @enderror
                                </div>
                            </div>

                            <div class="form-group row mb-0">
                                <div class="col-md-8 offset-md-4">
                                    <a class="btn btn-link" href="{{ route('2fa.resend') }}">Resend Code?</a>
                                </div>
                            </div>

                            <div class="form-group row mb-0">
                                <div class="col-md-8 offset-md-4">
                                    <button type="submit" class="btn btn-primary">
                                        Submit
                                    </button>

                                </div>
                            </div>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </div>
@endsection
resources/views/email_code.blade.php
<!DOCTYPE html>
<html>
<head>
    <title>shouts.dev</title>
</head>
<body>
<h1>{{ $title }}</h1>
<p>Your code is : {{ $code }}</p>

<p>Thank you</p>
</body>
</html>

Setup .env File

.env
MAIL_DRIVER=smtp
MAIL_HOST=smtp.gmail.com
MAIL_PORT=587
[email protected]
MAIL_PASSWORD=*********
MAIL_ENCRYPTION=tls
[email protected]
MAIL_FROM_NAME="${APP_NAME}"

Output

After successfully completed all the step if you run your project and try to login you will see the below output

After Login ask for a Code

Email for 2FA

That’s all for today. You can download this project from GitHub. Thanks for reading.