Free eBook: Build Your First Node App

How to Add Google's Two Factor Authentication to Laravel

Laravel is a wonderful PHP framework that makes building applications with PHP a lot of fun.

One of the nice features of Laravel is how easy it is to set up user authentication. It includes everything from registering to authentication and even password retrieval.

However, with the state of the things at the moment, the regular email and password login method is becoming less and less secure. Brute force attacks, phishing scams, data breaches, and SQL injection attacks have become so common that usernames and passwords can be easily cracked, captured, and leaked. Also, the use of weak passwords, same passwords across multiple accounts, and unsecure wifi networks, put many people in jeopardy of getting hacked.

Table of Contents

    Two factor authentication (2FA) strengthens access security by requiring two methods (also referred to as factors) to verify your identity. Two factor authentication protects against phishing, social engineering and password brute force attacks and secures your logins from attackers exploiting weak or stolen credentials.

    In this tutorial, we are going to learn how to add two factor authentication to our Laravel application. We'll be using Google Authenticator and implementing the Time-based One-time Password (TOTP) algorithm specified in RFC 6238.

    To use the two factor authentication, your user will have to install a Google Authenticator compatible app. Here are some that are currently available:

    Setting up

    Installing Laravel

    To start, we will create a fresh Laravel installation. Let's install it in a folder called laravel-2fa

    composer create-project --prefer-dist laravel/laravel laravel-2fa
    
    # set proper folder permissions
    sudo chmod -R 777 laravel-2fa/storage laravel-2fa/bootstrap/cache

    For more detailed installation instructions, visit the documentation.

    We can then start our server with the command:

    php artisan serve --port=8000

    Now our website will be available on http://localhost:8000. It should look like this.

    Laravel

    Connecting to a database

    In order to manage the user data, we need to connect to a database. We will use MySQL for this tutorial, but it works the same for any other database system.

    In the .env file, edit the following lines according to your database setup.

    # .env
    DB_CONNECTION=mysql
    DB_HOST=127.0.0.1
    DB_PORT=3306
    DB_DATABASE=homestead
    DB_USERNAME=homestead
    DB_PASSWORD=secret

    You should also update the following lines.

    # .env
    APP_NAME=Laravel 2FA Demo
    APP_URL=http://localhost:8000

    This is to give our application a name different from the default (Laravel) and also to update our base URL. You can choose any other URL depending on your setup, but take note and use the right base URL while following this tutorial.

    Setting up Laravel authentication

    Laravel ships with several pre-built authentication controllers and provides a quick way to scaffold all of the routes and views you need for authentication using one simple command:

    php artisan make:auth
    
    # create the database tables needed with
    php artisan migrate

    If we visit our site, we will now see this. Notice LOGIN and REGISTER at the top of the screen.

    Laravel Auth.

    We can then visit http://localhost:8000/register to register a new user.

    Adding two factor authentication during registration

    What we aim to achieve is this:

    1. When a new user tries to register we will generate a user secret for the authenticator.
    2. On the next request, we will use that secret to show the QR code for the user to set up their Google Authenticator.
    3. When the user clicks "OK" we will then register the user with their Google Authenticator secret.

    This way the QR code page is accessible ONLY once. This is for maximum security. If the user wants to set up the two factor authentication again, they will have to repeat the flow and invalidate the old one.

    To achieve this, we will put a step for setting up the Google Authenticator before registering the user in the database.

    To make changes to the registration flow, we have to define the register method of the RegisterController (you can find it at app/Http/Controllers/Auth/RegisterController).

    Generating and displaying the secret

    First, we need to install two packages.

    composer require pragmarx/google2fa-laravel
    composer require bacon/bacon-qr-code

    If you are using Laravel 5.4 and below, you need to add PragmaRX\Google2FALaravel\ServiceProvider::class, to your providers array, and 'Google2FA' => PragmaRX\Google2FALaravel\Facade::class, to your aliases array in app/config/app.php (Laravel 4.x) or config/app.php (Laravel 5.x).

    Next, we have to publish the config file using:

    php artisan vendor:publish --provider=PragmaRX\\Google2FALaravel\\ServiceProvider

    Next, we include request class at the top of our RegisterController. This is so we can use the Request class without using of the full namespace.

    // app/Http/Controllers/Auth/RegisterController.php
    
    use Illuminate\Http\Request;

    Then we define the register method of our RegisterController as this.

        // app/Http/Controllers/Auth/RegisterController.php
    
        public function register(Request $request)
        {
            //Validate the incoming request using the already included validator method
            $this->validator($request->all())->validate();
    
            // Initialise the 2FA class
            $google2fa = app('pragmarx.google2fa');
    
            // Save the registration data in an array
            $registration_data = $request->all();
    
            // Add the secret key to the registration data
            $registration_data["google2fa_secret"] = $google2fa->generateSecretKey();
    
            // Save the registration data to the user session for just the next request
            $request->session()->flash('registration_data', $registration_data);
    
            // Generate the QR image. This is the image the user will scan with their app
         // to set up two factor authentication
            $QR_Image = $google2fa->getQRCodeInline(
                config('app.name'),
                $registration_data['email'],
                $registration_data['google2fa_secret']
            );
    
            // Pass the QR barcode image to our view
            return view('google2fa.register', ['QR_Image' => $QR_Image, 'secret' => $registration_data['google2fa_secret']]);
        }

    We also need to create the view for displaying the QR code. Our method defines the view as google2fa.register, so in resources/views we will create a google2fa folder and a file inside it called register.blade.php.

    So the full file path will be resources/views/google2fa/register.blade.php.

    Here are the contents of the file.

    // resources/views/google2fa/register.blade.php
    
    @extends('layouts.app')
    
    @section('content')
    <div class="container">
        <div class="row">
            <div class="col-md-8 col-md-offset-2">
                <div class="panel panel-default">
                    <div class="panel-heading">Set up Google Authenticator</div>
    
                    <div class="panel-body" style="text-align: center;">
                        <p>Set up your two factor authentication by scanning the barcode below. Alternatively, you can use the code {{ $secret }}</p>
                        <div>
                            <img src="{{ $QR_Image }}">
                        </div>
                        <p>You must set up your Google Authenticator app before continuing. You will be unable to login otherwise</p>
                        <div>
                            <a href="/complete-registration"><button class="btn-primary">Complete Registration</button></a>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
    @endsection

    Now immediately after registration, the user is taken to a page with the relevant QR code and the SECRET incase they cannot scan the code themselves.

    The page should look like this. Laravel two factor authentication QR Code

    Registering the user

    Unfortunately, we get an error when the user tries to proceed beyond this point, this is because we have not set up the route and controller action to handle the proper registration.

    However, before we do that, we need to make space for the Google two factor authentication secret in the users table. For that, we create a migration.

    php artisan make:migration add_google2fa_column_to_users --table=users

    The migration file should look like this.

    // database/migrations/201X_XX_XX_XXXXXX_add_google2fa_column_to_users.php
    
    <?php
    
    use Illuminate\Support\Facades\Schema;
    use Illuminate\Database\Schema\Blueprint;
    use Illuminate\Database\Migrations\Migration;
    
    class AddGoogle2faColumnToUsers extends Migration
    {
        /**
         * Run the migrations.
         *
         * @return void
         */
        public function up()
        {
            Schema::table('users', function (Blueprint $table) {
             // add a text column in the users table for the google2fa_secret
                $table->text('google2fa_secret');
            });
        }
    
        /**
         * Reverse the migrations.
         *
         * @return void
         */
        public function down()
        {
            Schema::table('users', function (Blueprint $table) {
             // drop the column if the migration is rolledback
                $table->dropColumn('google2fa_secret');
            });
        }
    }

    The migration file tells our application to add a googel2fa_secret column to our users table when we run it and to delete that column if we rollback the migration. Now we run our migrations again.

    php artisan migrate

    In this next step of the registration, we will need to make use of the register method that we overrode, so in our RegisterController, we change this:

        // app/Http/Controllers/Auth/RegisterController.php
    
        use RegistersUsers;

    to this:

        // app/Http/Controllers/Auth/RegisterController.php
    
        use RegistersUsers {
         // change the name of the name of the trait's method in this class
         // so it does not clash with our own register method
            register as registration;
        }

    Next, we create the complete-registration route.

    In routes/web.php add the following line:

    // routes/web.php 
    
    Route::get('/complete-registration', 'Auth\RegisterController@completeRegistration');

    So, now we define the completeRegistration method in out RegisterController.

        // app/Http/Controllers/Auth/RegisterController.php
    
        public function completeRegistration(Request $request)
        {        
            // add the session data back to the request input
            $request->merge(session('registration_data'));
    
            // Call the default laravel authentication
            return $this->registration($request);
        }

    Unfortunately, the default Laravel authentication saves just the name, email and password.

    To include our google2fa_secret we modify our create method:

        // app/Http/Controllers/Auth/RegisterController.php
    
        protected function create(array $data)
        {
            return User::create([
                'name' => $data['name'],
                'email' => $data['email'],
                'password' => bcrypt($data['password']),
                'google2fa_secret' => $data['google2fa_secret'],
            ]);
        }

    We have to also modify our User model's fillable property to include the google2fa_secret and also hide it whenever we cast it to an array or JSON.

    Read about the fillable property here.

    Read about hiding attributes from casting here.

    So we modify the following lines in the app/User.php.

        // app/User.php
    
        /**
         * The attributes that are mass assignable.
         *
         * @var array
         */
        protected $fillable = [
            'name', 'email', 'password', 'google2fa_secret',
        ];
    
        /**
         * The attributes that should be hidden for arrays.
         *
         * @var array
         */
        protected $hidden = [
            'password', 'remember_token', 'google2fa_secret',
        ];

    We can proceed with this, but for extra security, let us encrypt the google2fa_secret so that our users are not compromised even if our database gets compromised.

    In our User model, we will add these extra methods.

        // app/User.php
    
        /**
         * Ecrypt the user's google_2fa secret.
         *
         * @param  string  $value
         * @return string
         */
        public function setGoogle2faSecretAttribute($value)
        {
             $this->attributes['google2fa_secret'] = encrypt($value);
        }
    
        /**
         * Decrypt the user's google_2fa secret.
         *
         * @param  string  $value
         * @return string
         */
        public function getGoogle2faSecretAttribute($value)
        {
            return decrypt($value);
        }

    To understand how the above methods work, read about Laravel accessors and mutators here.

    Finally, users can now register seamlessly! A logged in user should see this:

    Laravel logged in

    Adding two factor authentication during logging in

    Everything we have done so far will be useless if we do not use it during the login flow. Since we are still using the default login flow, users only need their email and password.

    Our aim is for users to first input their Google Authenticator code before they are allowed full access to the site.

    The best way to implement this is to use a middleware. Thankfully, the pragmarx/google2fa-laravel package ships with a middleware for this.

    To use it, first, we add this to the routeMiddleware array in app/Http/Kernel.php.

        // app/Http/Kernel.php
    
        protected $routeMiddleware = [
            ...
            '2fa' => \PragmaRX\Google2FALaravel\Middleware::class,
        ];

    With this, we can use 2fa to refer to our middleware whenever we need to. Either in our route files or inside our controller classes.

    Next, we define the view where the user enters the OTP after logging in. By default, it is configured to use the view at resources/views/google2fa/index.blade.php. So we will create the view and add the following.

    // resources/views/google2fa/index.blade.php 
    
    @extends('layouts.app')
    
    @section('content')
    <div class="container">
        <div class="row">
            <div class="col-md-8 col-md-offset-2">
                <div class="panel panel-default">
                    <div class="panel-heading">Register</div>
    
                    <div class="panel-body">
                        <form class="form-horizontal" method="POST" action="{{ route('2fa') }}">
                            {{ csrf_field() }}
    
                            <div class="form-group">
                                <label for="one_time_password" class="col-md-4 control-label">One Time Password</label>
    
                                <div class="col-md-6">
                                    <input id="one_time_password" type="number" class="form-control" name="one_time_password" required autofocus>
                                </div>
                            </div>
    
                            <div class="form-group">
                                <div class="col-md-6 col-md-offset-4">
                                    <button type="submit" class="btn btn-primary">
                                        Login
                                    </button>
                                </div>
                            </div>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </div>
    @endsection

    Next, we need a route to handle the submissions of the OTP. The middleware already checks for the OTP, so we just need a route to sit behind the middleware and redirect the user back to the original URL.

    We can do that by adding this to routes/web.php:

    // routes/web.php
    
    Route::post('/2fa', function () {
        return redirect(URL()->previous());
    })->name('2fa')->middleware('2fa');

    We created a route that responds to post requests to http://localhost:8000/2fa and redirects to the previous URL. Since we put the route behind the 2fa middleware, it will validate the OTP if it is contained in the request object.

    Now, we can use the middleware to restrict any aspect of the application that requires it.

    Read all about using middlewares here.

    For example, in our HomeController, we can change this:

        // app/Http/Controllers/HomeController.php
    
        /**
         * Create a new controller instance.
         *
         * @return void
         */
        public function __construct()
        {
            $this->middleware('auth');
        }

    to this

        // app/Http/Controllers/HomeController.php
    
        /**
         * Create a new controller instance.
         *
         * @return void
         */
        public function __construct()
        {
            $this->middleware(['auth', '2fa']);
        }

    So that after logging in, when the user is redirected to /home they have to first enter the one-time password from the google authenticator.

    They will be presented with a form like this:

    Laravel OTP

    Once they enter the OTP, they will then be fully logged in.

    That's it! We've successfully added two factor authentication with the Google Authenticator to our Laravel Application.

    BONUS: Edge cases

    Reauthentication by the User

    So you user feels like someone has access to his secret and will be able to generate the OTP, so he wants to get a new one.

    You don't want him calling you at 3 am or blaming you if something goes wrong, so you need to give them a link to re-authenticate.

    To do this, let us define the route.

    In routes/web.php we'll add:

    // routes/web.php
    
    Route::get('/re-authenticate', 'HomeController@reauthenticate');

    Next, in HomeController we'll add the reauthenticate method;

        // app/Http/Controllers/HomeController.php
    
        public function reauthenticate(Request $request)
        {
            // get the logged in user
            $user = \Auth::user();
    
            // initialise the 2FA class
            $google2fa = app('pragmarx.google2fa');
    
            // generate a new secret key for the user
            $user->google2fa_secret = $google2fa->generateSecretKey();
    
            // save the user
            $user->save();
    
            // generate the QR image
            $QR_Image = $google2fa->getQRCodeInline(
                config('app.name'),
                $user->email,
                $user->google2fa_secret
            );
    
            // Pass the QR barcode image to our view.
            return view('google2fa.register', ['QR_Image' => $QR_Image, 
                                                'secret' => $user->google2fa_secret,
                                                'reauthenticating' => true
                                            ]);
        }

    If you notice, we are using the same view as last time. So let us add some conditionals in the view to hide the registration messages.

    Edit resources/views/google2fa/register.blade.php:

    //resources/views/google2fa/register.blade.php
    
    @extends('layouts.app')
    
    @section('content')
    <div class="container">
        <div class="row">
            <div class="col-md-8 col-md-offset-2">
                <div class="panel panel-default">
                    <div class="panel-heading">Set up Google Authenticator</div>
    
                    <div class="panel-body" style="text-align: center;">
                        <p>Set up your two factor authentication by scanning the barcode below. Alternatively, you can use the code {{ $secret }}</p>
                        <div>
                            <img src="{{ $QR_Image }}">
                        </div>
                        @if (!@$reauthenticating) {{-- add this line --}}
                            <p>You must set up your Google Authenticator app before continuing. You will be unable to login otherwise</p>
                            <div>
                                <a href="/complete-registration"><button class="btn-primary">Complete Registration</button></a>
                            </div>
                        @endif {{-- and this line --}}
                    </div>
                </div>
            </div>
        </div>
    </div>
    @endsection

    Reauthentication by you the SysAdmin

    So this time your user cannot login. His phone may have been stolen, or he deleted the credentials unknowingly or some other thing.

    Bottom line is, you have to generate a new secret for him.

    I've found the best way to do this is to create an artisan command that will update the user secret and print it on the command line. We can then send the secret to the user to input in his app.

    To create the command we run this:

    php artisan make:command ReAuthenticate

    Now we can go edit our command file at app/Console/Commands/ReAuthenticate.php.

    // app/Console/Commands/ReAuthenticate.php
    
    <?php
    
    namespace App\Console\Commands;
    
    use App\User;
    use Illuminate\Console\Command;
    
    class ReAuthenticate extends Command
    {
        /**
         * The name and signature of the console command.
         *
         * @var string
         */
        protected $signature = '2fa:reauthenticate {--email= : The email of the user to reauthenticate} {--force : run without asking for confirmation}';
    
        /**
         * The console command description.
         *
         * @var string
         */
        protected $description = 'Regenerate the secret key for a user\'s two factor authentication';
    
        /**
         * Create a new command instance.
         *
         * @return void
         */
        public function __construct()
        {
            parent::__construct();
        }
    
        /**
         * Execute the console command.
         *
         * @return mixed
         */
        public function handle()
        {
            // retrieve the email from the option
            $email = $this->option('email');
    
            // if no email was passed to the option, prompt the user to enter the email
            if (!$email) $email = $this->ask('what is the user\'s email?');
    
            // retrieve the user with the specified email
            $user = User::where('email', $email)->first();
    
            if (!$user) {
                // show an error and exist if the user does not exist
                $this->error('No user with that email.');
                return;
            }
    
            // Print a warning 
            $this->info('A new secret will be generated for '.$user->email);
            $this->info('This action will invalidate the previous secret key.');
    
            // ask for confirmation if not forced
            if (!$this->option('force') && !$this->confirm('Do you wish to continue?')) return;
    
            // initialise the 2FA class
            $google2fa = app('pragmarx.google2fa');
    
            // generate a new secret key for the user
            $user->google2fa_secret = $google2fa->generateSecretKey();
    
            // save the user
            $user->save();
    
            // show the new secret key
            $this->info('A new secret has been generated for '.$user->email);
            $this->info('The new secret is: '.$user->google2fa_secret);
        }
    }
    

    I've added comments to explain what the command does.

    Read all about the creating console commands for the artisan console here.

    To use it, we go to the terminal and run

    php artisan 2fa:reauthenticate

    It will prompt for the user's email and then ask for confirmation.

    We can also pass the user's email with the command by adding the --email option.

    php artisan 2fa:reauthenticate --email johndoe@example.com

    To skip confirmation check, we can force the command using the --force option

    php artisan 2fa:reauthenticate --force

    The new secret key generated will be printed to the console, so you can copy it and send to your user so he can set up his app.

    Conclusion

    In this tutorial, we've seen how we can add two factor authentication to a Laravel application. We modified both the registration and login flow, and even dealt with a couple edge cases.

    If you want to get a Laravel 5.5 template with two factor authentication already set up, you can clone the repository(Don't forget to star it too). It was set up using the steps in this article.

    As always, if you have any questions, suggestions, or comments, please leave them below.