Introduction
Laravel provides an amazing authentication scaffold that handles registration, login, and all other authentication parts for users that are easy to use.
However, the traditional email and password are becoming less secure due to many cyber attacks like SQL injections, phishing scams and data breaches.
The concept of two-factor authentication(2FA) was created to overcome this shortcoming.
What is Two Factor Authentication (2FA)?
Two-factor authentication (2FA) strengthens access security by requiring two methods to verify users identities.
The traditional password is already one factor of authentication which is something only the user should have. Some extra form of security that a user should also have includes biometrics(fingerprint), voice pattern recognition, or iris scan which are quite expensive but awesome.
The second factor should be something that users don't readily have or aren't constant. One form of the second factor is One-Time Passwords(OTPs) which will be our focus here.
What is a One-Time Password(OTP)?
A one-time password (OTP) is an automatically generated set of characters used to authorize a user for a specific action. As the name implies, it can only be used once. It can either be counter-based or time-based.
After the correct password is provided in the login form, the user is prompted for an OTP depending on your application or preference. This can be implemented in so many ways such as:
- Hardware Tokens
- One Time Password (OTP) sent via SMS
- Google Authenticator
In this guide, you will learn how to use Google Authenticator to implement Time based One-Time Password (TOTP) specified in RFC 6238. which uses HMAC-Based One-Time Password Algorithm (HOTP) specified in RFC 4226 in building an authentication system.
We will be using this package to implement the Google Two-Factor Authentication on our Laravel Application.
Through this guide, I highlighted some issues, their solutions and modifications to help have a better experience with this packages.
Google Authentication Apps
To use the two-factor authentication, the user will have to install a Google Authenticator compatible app. Here are a few:
An advantage of using Google Authenticator is that after downloading the app to your smartphone, you can use it offline unlike other 2FA options which need to be connected to the internet to work which may be a disadvantage to users in a cut off location *for example, the basement of a building.
How Time based One-time Password works?
After the user logs in successfully, a prompt showing a QR code and alternatively a code(set of characters to manually input if the user may not be able to scan QR code).
Upon scanning or submitting the code, the server generates a secret key which is passed to the user.
The secret key is combined with the current Unix timestamp to generate a six-digit code using a message authentication code (HMAC) based algorithm.
The six-digit code is the OTP and it changes every 30 seconds.
Now let's get to coding!😎
Building the Authentication System
Create the Authentication Scaffold.
Step 1: Create a Laravel application.
You can use the laravel command or via the Composer package manager as follows:
laravel new project_name
or
composer create-project laravel/laravel project_name
Step 2: Establish a database connection.
Here is a guide I wrote in connecting your Laravel app to MySQL database. If your database is different, do well to connect it appropriately.
Step 3: Install Laravel/UI.
composer require laravel/ui
Step 4: Install Bootstrap Auth Scaffolding.
The bootstrap authentication scaffold gives a UI and basic authentication for registration and login. You can install it with this Artisan
command:
php artisan ui bootstrap --auth
Step 5: Install NPM Packages.
npm install
Step 6: Run NPM environment.
npm run dev
Step 7: Run the application.
You can serve the laravel application with the following Artisan Command
:
php artisan serve
Step 8: Now hit this URL on your browser.
http://localhost:8000/register
You should be able to view the register and login page like this:
P.S: We haven't run the migrations yet so submitting the forms will return an error message.
Adding the Two-Factor Authentication to Registration.
AIM:
Let's pause and identify what we want to achieve:
- A user's secret for authenticator will be generated when a user tries to register
- That secret will be used to show a QR Code for the user to set up their Google Authenticator upon the next request.
- The user is registered with their Google Authentication secret after submitting the correct OTP.
P.S: The QR code is accessible ONLY once for maximum security and if the user needs to set up 2FA again, they will have to repeat the process and invalidate the old one.
Section 1: Generating QR Code and Displaying secret
Step 1: Install two packages.
The first is Google Authenticator package for PHP and the second package is a QR code generator which is BaconQrCode.
composer require pragmarx/google2fa-laravel
composer require bacon/bacon-qr-code
Step 2: Publish the config file.
php artisan vendor:publish --provider="PragmaRX\Google2FALaravel\ServiceProvider"
Step 3: Update RegisterController
with register()
method.
P.S: Include the
Request
class outside the controller class.
use Illuminate\Http\Request;
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']]);
}
Step 4: Override the default register()
trait.
Since we defined our own register()
method, we need to update the trait to avoid a clash with the default register()
method from the authentication scaffold.
Instead of
use RegistersUsers;
use this:
use RegistersUsers {
// We are doing this so the predefined register method does not clash with the one we just defined.
register as registration;
}
Step 5: Create the view to display the QR code.
Our register()
method already redirects to view(google2fa.register.blade.php)
. This means we will create agoogle2fa
folder and a register.blade.php
file in it. The full path will be resources/views/google2fa/register.blade.php
.
This will be the content of the file to display the QR code:
@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
P.S: Now after submitting valid credentials at registration, the user is redirected to a page with a valid QR code and also the secret(if they cant scan the code). However, users can't complete registration yet because we are yet to set up the controllers and route to handle that.
Section 2: Registering the user.
Step 1 : Update User Migration.
Since we haven't run our migrations yet, we can add a column for Google two factor authentication secret. We will update the up()
method in database/migrations/2014_10_12_000000_create_users_table.php
like this:
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->text('google2fa_secret');
$table->timestamps();
});
}
Step 2: Run the migrations.
We can now run our migrations to the database with this Artisan
command:
php artisan run migrate
P.S: If you have run your migrations before now, Here is a guide I wrote about Adding and Removing columns from existing tables in database . (Fear not! I got you covered😎)
Step 3 : Update the RegisterController
with completeRegistration()
method.
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);
}
Step 4: Update User
model and create()
method in RegisterController
with google2fa_secret
field.
- First, we need to set the
User
model ashidden
andfillable
property so that it can be included and also hidden if we cast it to an array or JSON.
protected $fillable = [
'name',
'email',
'password',
'google2fa_secret'
];
/**
* The attributes that should be hidden for arrays.
*
* @var array
*/
protected $hidden = [
'password',
'remember_token',
'google2fa_secret'
];
- Then we also need to update the
create()
method to accept the field like this: ```php
protected function create(array $data)
{
return User::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => Hash::make($data['password']),
'google2fa_secret' => $data['google2fa_secret'],
]);
}
**Step 5: Set up routes for `complete-registeration`.**
This will be in our `routes/web.php` file like this:
```php
Route::get('/complete-registration', [App\Http\Controllers\Auth\RegisterController::class, 'completeRegistration'])->name('complete-registration');
Step 6 : Encrypt the google2fa_secret
.
We are all set😍, however, let's take an extra step for security and encrypt the google2fa_secret
in the User
model like this:
public function setGoogle2faSecretAttribute($value)
{
$this->attributes['google2fa_secret'] = encrypt($value);
}
public function getGoogle2faSecretAttribute($value)
{
return decrypt($value);
}
Now a user that successfully registers with the correct OTP should see this:
HIGHLIGHT 1
Here is an issue you're likely to face at this point depending on your Laravel version and its compatibility with these packages.
Here is one solution I figured out🤗:
The Bacon-Qr-code package seems to be most stable on version 1.0.3 so you can downgrade it with the following composer command:
composer require bacon/bacon-qr-code:~1.0.3
Now try to submit the form again, the registration should be successful!🎉👍
Adding the Two-Factor Authentication to Log in.
Now we can generate the QR code as well as the secret, thereafter successfully register a user.
But we want to prompt users for their OTP before granting them access to any part of the app.🤔
Step 1: Set up Route Middleware
The pragmarx/google2fa-laravel
package provides a middleware to prevent users from accessing the app unless OTP has been provided.
First, we need to add this to the routeMiddleware
array in app/Http/Kernel.php
before we can use it.
protected $routeMiddleware = [
'2fa' => \PragmaRX\Google2FALaravel\Middleware::class,
];
This will restrict any user that hasn't submitted a valid OTP from access to the home page. It will keep redirecting back to resources/views/google2fa/index.blade.php
prompting for the OTP until a valid OTP is sent.
USAGE
After scanning the QR code or inputting Secret on the Google Authenticator app, it automatically generates an OTP on the Authenticator App.
Click on Complete Registration then the user is prompted for the OTP and if OTP submitted is valid(being careful that the OTP on the app refreshes every 30 seconds, so the user must input the current OTP) then, the user is redirected to home page else user will keep being prompted for the OTP until the submission is valid.
The OTP page which is resources/views/google2fa/index.blade.php
looks like this:
- A user that has successfully logged in , validating both forms of authentication will see the home page:
HIGHLIGHT 2
I realized that it wasn't user-friendly to keep redirecting users back to OTP page for a valid OTP if the submission was invalid. So I decided to return an error message when redirecting to inform users that the OTP entered was wrong.
Update the resources/views/google2fa/index.blade.php
like this:
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row justify-content-center align-items-center " style="height: 70vh;S">
<div class="col-md-8 col-md-offset-2">
<div class="panel panel-default">
<div class="panel-heading font-weight-bold">Register</div>
<hr>
@if($errors->any())
<b style="color: red">{{$errors->first()}}</b>
@endif
<div class="panel-body">
<form class="form-horizontal" method="POST" action="{{ route('2fa') }}">
{{ csrf_field() }}
<div class="form-group">
<p>Please enter the <strong>OTP</strong> generated on your Authenticator App. <br> Ensure you submit the current one because it refreshes every 30 seconds.</p>
<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
Here is what the result will be if the submitted OTP is wrong:
Conclusion
Wow!🎉🤗 You have built an authentication system with Google's Two Factor Authentication💪💪.
Guess what😎? I made the entire code open-source here on Github 🤗. I am open to conversations, questions or contributions, especially concerning the highlights. I would love to know better ways to achieve these things.
You can drop your comments or reach out to me on Twitter.
Thanks for reading.🤝
Top comments (10)
Hi, I have the same problem.
But I think that is the solution. In routes/web.php I add this:
Route::middleware(['2fa'])->group(function () {
Route::get('/home', [App\Http\Controllers\HomeController::class, 'index'],)->name('home');
Route::post('/2fa', function () {
return redirect(route('home'));
})->name('2fa');
});
and run correctly for me.
;-)
Thanks for adding this, it helped me a lot!
Hello. I have an issue that you also have what I can see from your sceenshots. The user is logged in when the OTP screen is up. But the user has never submited his OTP code. In my case he can click on buttons on the navbar and go to that page without filled in the OTP. How can I fix that?
hello, did you manage to solve your problem ?
Hello ! Great tuto, big thanks. It is very comprehensive and easy to follow.
I just tried to implement this google2fa and it went very well on registration. But the login keep sending me directly to the dashboard without asking me for the secret code.
Please do you have any idea of what can cause this ?
@habibx @roxie
Hi, I am facing the same issue.
Did you find any solution ?
Hi,
I followed all the steps, but currently, I am stuck with this error:
Did someone come across this error?
Thanks You try your best but it's not complete dude.
You did't explain what happend with 2fa route and controller
and How to turn off 2fa ?
Thanks
Thank you so much for this, I am doing an improved version of this article soon and it will include all that, so please watch out for updates. I appreciate the suggestion
This is a great tutorial! Thanks for making it! Very easy to follow and understand. Appreciate you making it!