Package Data | |
---|---|
Maintainer Username: | michaeldzjap |
Package Create Date: | 2017-05-26 |
Package Last Update: | 2024-09-01 |
Home Page: | |
Language: | PHP |
License: | MIT |
Last Refreshed: | 2024-12-22 03:10:12 |
Package Statistics | |
---|---|
Total Downloads: | 39,184 |
Monthly Downloads: | 744 |
Daily Downloads: | 0 |
Total Stars: | 43 |
Total Watchers: | 4 |
Total Forks: | 6 |
Total Open Issues: | 0 |
A two-factor authentication package for Laravel >= 5.5
This is a two-factor authentication package for Laravel. It is heavily inspired by the Laravel Two-Factor Authentication package. The main differences between this package and the aforementioned package are:
'null'
driver that goes through all the steps of the two-factor authentication process without actually doing any real verification. This could be useful for testing purposes. You can however, specify a custom provider yourself.From Laravel 5.8 and onwards, the default is to use bigIncrements
instead of increments
for the id
column on the users
table. As such, the default for this package is to use the same convention for the user_id
column on the two_factor_auths
table. If this is not what you want, you can change this to your liking by modifying the migration files that are published for this package.
Publishing the package's migration files allows for more flexibility with regards to customising your database structure. However, it could also cause complications if you already have ran migrations as part of installing previous versions of this package. In this case you simply might want to bypass running the migrations again or only run them when in a specific environment. The Schema::hasColumn()
and Schema::hasTable()
methods should be of use here.
Versions of this package prior to v2.3.0 incorrectly created the user_id
column on the two_factor_auths
table using increments
instead of unsignedInteger
. Practically speaking, this error is of no concern. Although there is no need to have a primary key for the user_id
column, it doesn't cause any problems either. However, if for some reason you don't like this idea, it is safe to remove the primary key using a migration of the form
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class RemovePrimaryFromTwoFactorAuthsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('two_factor_auths', function (Blueprint $table) {
$table->dropForeign(['user_id']);
});
Schema::table('two_factor_auths', function (Blueprint $table) {
$table->unsignedInteger('user_id')->change();
$table->dropPrimary(['user_id']);
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('two_factor_auths', function (Blueprint $table) {
$table->dropForeign(['user_id']);
});
Schema::table('two_factor_auths', function (Blueprint $table) {
$table->increments('user_id')->change();
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
});
}
}
Note that you will need the doctrine/dbal package for this migration to work. Furthermore, if the id
column on your users
table is of type bigIncrements
you will have to change the lines $table->unsignedInteger('user_id')->change();
to $table->unsignedBigInteger('user_id')->change();
and $table->increments('user_id')->change();
to $table->bigIncrements('user_id')->change();
respectively.
composer require michaeldzjap/twofactor-auth
If you want to use MessageBird Verify as the two-factor authentication provider then you also need to install the MessageBird PHP api:
composer require messagebird/php-rest-api
and don't forget to add your MESSAGEBIRD_ACCESS_KEY
and TWO_FACTOR_AUTH_DRIVER=messagebird
variables to the .env
. If you instead wish to use the 'null'
driver (default) then do NOT define the TWO_FACTOR_AUTH_DRIVER
variable in your .env
.
'providers'
array in config/app.php
:MichaelDzjap\TwoFactorAuth\TwoFactorAuthServiceProvider::class
php artisan vendor:publish
If you want to publish only one of these file groups, for instance if you don't need the views or language files, you can append one of the following commands to the artisan command: --tag=config
, --tag=lang
or --tag-views
.
Important: Make sure you do this step before you run any migrations for this package, as otherwise it might give you unexpected results.
From Laravel 5.8 and on, the default is to use bigIncrements
instead of increments
for the id
column on the users
table. As such, the default for this package is to use the same convention for the user_id
column on the two_factor_auths
table. If this is not what you want, you can modify the published migration files for this package.
Run the following artisan command to run the database migrations
php artisan migrate
This will add a mobile
column to the users
table and create a two_factor_auths
table.
User
model:...
use MichaelDzjap\TwoFactorAuth\TwoFactorAuthenticable;
class User extends Authenticatable
{
use Notifiable, TwoFactorAuthenticable;
...
Optionally, you might want to add 'mobile'
to your $fillable
array.
The following two-factor authentication routes will be added automatically:
$router->group([
'middleware' => ['web', 'guest'],
'namespace' => 'App\Http\Controllers\Auth',
], function () use ($router) {
$router->get('/auth/token', 'TwoFactorAuthController@showTwoFactorForm')->name('auth.token');
$router->post('/auth/token', 'TwoFactorAuthController@verifyToken');
});
The first route is the route the user will be redirected to once the two-factor authentication process has been initiated. The second route is used to verify the two-factor authentication token that is to be entered by the user. The showTwoFactorForm
controller method does exactly what it says. There do exist cases where you might want to respond differently however. For instance, instead of loading a view you might just want to return a json
response. In that case you can simply overwrite showTwoFactorForm
in the TwoFactorAuthController
to be discussed below.
LoginController
:...
use MichaelDzjap\TwoFactorAuth\Contracts\TwoFactorProvider;
class LoginController extends Controller
{
...
and also add the following functions:
/**
* The user has been authenticated.
*
* @param \Illuminate\Http\Request $request
* @param mixed $user
* @return mixed
*/
protected function authenticated(Request $request, $user)
{
if (resolve(TwoFactorProvider::class)->enabled($user)) {
return self::startTwoFactorAuthProcess($request, $user);
}
return redirect()->intended($this->redirectPath());
}
and
/**
* Log out the user and start the two factor authentication state.
*
* @param \Illuminate\Http\Request $request
* @param \App\User $user
* @return \Illuminate\Http\Response
*/
private function startTwoFactorAuthProcess(Request $request, $user)
{
// Logout user, but remember user id
auth()->logout();
$request->session()->put(
'two-factor:auth', array_merge(['id' => $user->id], $request->only('email', 'remember'))
);
self::registerUserAndSendToken($user);
return redirect()->route('auth.token');
}
and lastly
/**
* Provider specific two-factor authentication logic. In the case of MessageBird
* we just want to send an authentication token via SMS.
*
* @param \App\User $user
* @return mixed
*/
private function registerUserAndSendToken(User $user)
{
// Custom, provider dependend logic for sending an authentication token
// to the user. In the case of MessageBird Verify this could simply be
// resolve(TwoFactorProvider::class)->sendSMSToken($this->user)
// Here we assume this function is called from a queue'd job
dispatch(new SendSMSToken($user));
}
You can discard the third function if you do not want to send a two-factor authentication token automatically after a successful login attempt. Instead, you might want the user to instantiate this process from the form him/herself. In that case you would have to add the required route and controller method to trigger this function yourself. The best place for this would be the TwoFactorAuthController
to be discussed next.
TwoFactorAuthController
in app/Http/Controllers/Auth
with the following content:<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use MichaelDzjap\TwoFactorAuth\Http\Controllers\TwoFactorAuthenticatesUsers;
class TwoFactorAuthController extends Controller
{
use TwoFactorAuthenticatesUsers;
/**
* The maximum number of attempts to allow.
*
* @var int
*/
protected $maxAttempts = 5;
/**
* The number of minutes to throttle for.
*
* @var int
*/
protected $decayMinutes = 1;
/**
* Where to redirect users after two-factor authentication passes.
*
* @var string
*/
protected $redirectTo = '/home';
}
resources/views/auth/login.blade.php
:...
<form class="form-horizontal" role="form" method="POST" action="{{ route('login') }}">
@csrf
{{-- Add this block to show an error message in case of an expired token or user lockout --}}
@if ($errors->has('token'))
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<strong>{{ $errors->first('token') }}</strong>
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
@endif
...
Since the v2.1.0 release it is possible to user your own custom provider. To do so your provider needs to implement MichaelDzjap\TwoFactorAuth\Contracts\TwoFactorProvider
(and possibly MichaelDzjap\TwoFactorAuth\Contracts\SMSToken
if you want to send the authentication token via SMS).
TwoFactorAuthManager
from a service provider (could be \App\Providers\AppServiceProvider
):resolve(\MichaelDzjap\TwoFactorAuth\TwoFactorAuthManager)->extend('dummy', function ($app) {
return new DummyProvider;
});
...
'dummy' => [
'driver' => 'dummy',
],
...
TWO_FACTOR_AUTH_DRIVER=dummy
Unfortunately the MessageBird php api throws rather generic exceptions when the verification of a token fails. The only way to distinguish an expired token from an invalid token is by comparing their error messages, which obviously is not a very robust mechanism. The reason this is rather unfortunate is because in the case of an invalid token we want to give the user at least a few (3) changes to re-enter the token before throttling kicks in, whereas in the case of an expired token we just want to redirect to the login screen right away.
An example project including unit and browser tests can be found here.