Laravel Fortify: Setting up two factor authentication

Published October 11, 2020 (8 min read)

A tutorial on how to implement a frontend build for a user to enable and disable two factor authentication with Laravel Fortify.


This tutorial will be starting from a fresh install of Laravel 8 with our database details setup and using Vuejs 2 for the frontend (though this can be adapted to work with a Javascript framework of your choice).

If you have already completed the installation of Laravel Fortify and have added it's auth views you can skip the set up section.

Alternatively, if you just want the completed version you can find the code here

Setting up Laravel Fortify

Here we will go through the process of installing Fortify and setting up the login and home views

To get started, install Fortify using Composer:

composer require laravel/fortify

Next, publish Fortify's resources:

php artisan vendor:publish --provider="Laravel\Fortify\FortifyServiceProvider"

Add the new provider to your array of providers in config/app.php.

config/app.php
'providers' => [
    /*
     * Application Service Providers...
     */
    App\Providers\FortifyServiceProvider::class,
]

This will fix errors like the below when using Fortify's routes.

Target [Laravel\Fortify\Contracts\LoginViewResponse] is not instantiable.

Next, run the migrations:

php artisan migrate

If you haven't done already we can setup Vuejs:

npm install vue

and import it into our resources/js/app.js file.

resources/js/app.js
import Vue from 'vue'

window.app = new Vue({
    el: '#app',
})

Building the Fortify auth pages

Now we can create the views. First let's build a layout, create app.blade.php in the layouts folder.

resources/views/layouts/app.blade.php
<!DOCTYPE html>
<html>
    <body>
        <div id="app">
            @yield('content')
        </div>

        <script src="{{ mix('js/app.js') }}"></script>
    </body>
</html>

Next we can create the login and register views. Add auth folder in your resources/views and create a login.blade.php file.

resources/views/auth/login.blade.php
@extends('layouts.app')

@section('content')
<form method="POST">
    @csrf

    <label>{{ __('Email') }}</label>
    <input type="text" name="email" />

    <label>{{ __('Password') }}</label>
    <input type="password" name="password" />

    <button>
        Submit
    </button>
</form>
@endsection

Then create a register.blade.php in the same folder.

resources/views/auth/register.blade.php
@extends('layouts.app')

@section('content')
<form method="POST">
    @csrf

    <label>{{ __('Name') }}</label>
    <input type="text" name="name" />

    <label>{{ __('Email') }}</label>
    <input type="text" name="email" />

    <label>{{ __('Confirm Password') }}</label>
    <input type="password" name="password" />

    <label>{{ __('Confirm Password') }}</label>
    <input type="password" name="password_confirmation" />

    <button>
        Submit
    </button>
</form>
@endsection

We now need to tell Fortify to use these views as our new auth pages, we can do this by adding the following code to the boot method in App\Providers\FortifyServiceProvider.php

App\Providers\FortifyServiceProvider.php
Fortify::loginView(function () {
    return view('auth.login');
});

Fortify::registerView(function () {
    return view('auth.register');
});

Once a user is logged in they will be navigated to the signed in users home, the default is /home but this can be changed in App\Providers\RouteServiceProvider.php

App\Providers\RouteServiceProvider.php
public const HOME = '/home';

For now we will keep it this way and create a route and view for this url.

First let's create the home view home.blade.php

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

@section('content')
<h2>
    You are signed in as {{ auth()->user()->name }}
</h2>

<form method="POST" action="{{ route('logout') }}">
    @csrf
    
    <button>Logout</button>
</form>
@endsection

A simple view with a form allowing the use to logout.

Next, add the route to our routes/web.php

routes/web.php
Route::middleware('auth')->get('home', function() {
    return view('home');
});

Now let's set up the server:

php artisan serve

Now navigate to /register or /login if you already have a user and fill in the form. Once logged in you will now see the home template we created above.

Adding two factor authentication

To enable Fortify's two factor authentication feature we need to add it to our features array in config/fortify.php. This should be activated by default when Fortify was published but if not add it in.

config/fortify.php
'features' => [
    Features::twoFactorAuthentication([
        'confirmPassword' => true,
    ]),
]

The confirmPassword argument makes it so the user has to re-enter their password in order to enable two factor authentication.

Next, add the two factor authenticatable trait to our User model.

app\models\User.php
use Laravel\Fortify\TwoFactorAuthenticatable;

class User extends Authenticatable
{
    use TwoFactorAuthenticatable;
}

then add the two factor fields to the hidden array of the model

app\models\User.php
protected $hidden = [
    'two_factor_recovery_codes',
    'two_factor_secret',
];

Fortify generates all the routes we need in order to set the user up with two factor authentication but it's up to us to add the views to call these.

The steps (routes) we need to take are:

  1. Check if the user has confirmed their password (/user/confirmed-password-status)
  2. Make the user confirm their password (/user/confirm-password)
  3. Enable two factor authentication (/user/two-factor-authentication)
  4. Retrieve the QR code (/user/two-factor-qr-code)

First we need to create a component to check if the user has confirmed their password and if not present them with a prompt to do so. Create the confirm password vue component in resources/js/components/ConfirmPassword.vue

resources/js/components/ConfirmPassword.vue
<template>
    <div>
        <div v-if="confirmingPassword">
            <form @submit.prevent="confirmPassword">
                <input v-model="password" type="password" />

                <button>
                    Confirm
                </button>
            </form>
        </div>

        <span v-else @click="startConfirmingPassword">
            <slot />
        </span>
    </div>
</template>

<script>
export default {
    data () {
        return {
            confirmingPassword: false,
            password: ''
        }
    },

    methods: {
        startConfirmingPassword () {
            axios.get('/user/confirmed-password-status')
                .then((response) => {
                    if (response.data.confirmed) {
                        this.$emit('confirmed')
                    } else {
                        this.confirmingPassword = true
                    }
                })
        },

        confirmPassword () {
            axios.post('/user/confirm-password', {
                password: this.password,
            }).then(response => {
                this.confirmingPassword = false
                this.$emit('confirmed')
            })
        },
    }
}
</script>

Create the two factor authentication vue component in resources/js/components/TwoFactorAuth.vue and add the enable button in the slot of your newly created confirmPassword component

resources/js/components/TwoFactorAuth.vue
<template>
    <div>
        <h2>
            Two Factor Authentication
        </h2>

        <confirm-password>
            <button>
                Enable
            </button>
        </confirm-password>
    </div>
</template>

<script>
import ConfirmPassword from './ConfirmPassword'

export default {
    components: {
        ConfirmPassword
    },
}
</script>

Now we can register our component in our resources/js/app.js file

resources/js/app.js
Vue.component('two-factor-auth', require('./components/TwoFactorAuth').default)

window.app = new Vue({
    el: '#app',
})

and then add the component to our resources/views/home.blade.php file inbetween the content section.

resources/views/home.blade.php
<two-factor-auth />

Build our vue files:

npm run watch

Navigate to the /home page and we should see the enable button. Now when the user clicks the enable button they will be prompted to confirm their password if they already haven't.

Next, we need to add a call to enable two factor authentication on confirmation of password and then retrieve the QR code on a successful response. We can add two data properties, one to tell us whether two factor has been enabled and the other to store the QR code.

resources/js/components/TwoFactorAuth.vue
data () {
    return {
        twoFactorEnabled: false,
        qrCode: ''
    }
},

methods: {
    enableTwoFactorAuthentication () {
        axios.post('/user/two-factor-authentication')
            .then(() => {
                return Promise.all([
                    this.showQrCode()
                ])
            }).then(() => {
                this.twoFactorEnabled = true
            })
    },

    showQrCode () {
        return axios.get('/user/two-factor-qr-code')
            .then(response => {
                this.qrCode = response.data.svg
            })
    },
}

Note: If the confirmPassword argument has been set in the two factor authentication feature /user/two-factor-qr-code can only be called if the user has confirmed their password else a 500 error will be thrown.

We can call the enable method on confirmation by attaching it to the confirmed event. Once enabled we can hide the enable button with v-if. We can also add a contianer to show the retrieved QR code.

resources/js/components/TwoFactorAuth.vue
<div v-if="qrCode" v-html="qrCode" />

<confirm-password v-if="!twoFactorEnabled" @confirmed="enableTwoFactorAuthentication()">
    <button>
        Enable
    </button>
</confirm-password>

Now our user can enable two factor authentication and can scan the returned QR code.

To disable two factor authentication we can add a similar button and method to the component. First let's add the method.

resources/js/components/TwoFactorAuth.vue
disableTwoFactorAuthentication () {
    axios.delete('/user/two-factor-authentication')
        .then(() => {
            this.twoFactorEnabled = false
            this.qrCode = ''
        })
}

This will send a delete request and remove any two factor secrets and codes from the user.

We can add the disable button after the enable button to use Vue's if else conditional templating and call the disable method on the confirmed event

resources/js/components/TwoFactorAuth.vue
<confirm-password v-else @confirmed="disableTwoFactorAuthentication()">
    <button>
        Disable
    </button>
</confirm-password>

and now the user can disable two factor authentication.

To make sure the disable button appears if we have two factor enabled and we reload the page we can pass an enabled property to the two factor component.

First let's add a check for two factor into the User Model.

app\models\User.php
/**
 * @return bool
 */
public function twoFactorAuthEnabled()
{
    return !is_null($this->two_factor_secret);
}

Next, we need to allow the two factor component to accept the property. In resources/js/component/TwoFactorAuth.vue add the props and modify the twoFactorEnabled data variable to use it.

resources/js/component/TwoFactorAuth.vue
props: {
    enabled: {
        type: Boolean,
        default: false
    }
},

data () {
    return {
        twoFactorEnabled: this.enabled,
        qrCode: ''
    }
}

Now in our resources/views/home.blade.php we can pass in the check for the authentication.

resources/views/home.blade.php
<two-factor-auth :enabled="{{ json_encode(auth()->user()->twoFactorAuthEnabled()) }}" />

Once two factor authentication has been enabled and the QR code scanned we can log out and follow the login process by navigating to /login and filling in the form. Once submitted we will be taken to the two factor challenge page which we need to add the view for. Create resources/views/auth/two-factor-challenge.blade.php and add the following code.

resources/views/auth/two-factor-challenge.blade.php
@extends('layouts.app')

@section('content')
<form method="POST" action="/two-factor-challenge">
    @csrf

    <label>{{ __('Code') }}</label>
    <input type="text" name="code" />

    <button>
        Login
    </button>
</form>
@endsection

Now we need to tell Fortify to use this view so let's add it to boot method of app/Providers/FortifyServiceProvider.php

app/Providers/FortifyServiceProvider.php
Fortify::twoFactorChallengeView(function() {
    return view('auth.two-factor-challenge');
});

Reload the page, enter the code from your authentication app and hit submit. You will be taken to the user's home page, all done.

You have successfully set up two factor authentication with Laravel Fortify.