Add Two-Factor Verification via Email in Laravel Auth
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:
Method
toMail()
parameter$notifiable
is automatically allocated as signed in User object, so we can get tousers.two_factor_code
database column by calling$notifiable->two_factor_code;
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.
missing code in "Now, add following code in app/Notifications/TwoFactorCode.php"