Double Authentification Laravel : Google Authenticator

La double authentification se répand assez vite, et c’est une bonne chose. La confirmation par SMS est une très bonne chose, mais cette méthode a montrée ses faiblesses.
Dans ce tutoriel, on va voir comment configurer une double authentification sur une application Laravel, via Google Authenticator !

Initialiser le projet

A grands coupe de


composer create-project --prefer-dist laravel/laravel secure-me

On se déplace dans le dossier du projet, et on va demander à composer d’installer 2 dépendances : Une lib qui gère la double authentification avec Laravel, et une autre qui va servir à encoder en Base32.


composer require pragmarx/google2fa
composer require paragonie/constant_time_encoding

google2fa nécessite d’enregistrer le service provider, et on va ajouter la facade pour plus de facilité.
Donc on ouvre config/app.php et on ajoute ces lignes dans les bonnes sections :

//côté service provider
PragmaRX\Google2FA\Vendor\Laravel\ServiceProvider::class
//côté Facade
'Google2FA' => PragmaRX\Google2FA\Vendor\Laravel\Facade::class

Et enfin, on va utiliser cet outil démentiel de Laravel, qui vous permet de scafolder une auth en moins d’une seconde (accrochez-vous, ça va trembler) :

php artisan make:auth

Structurer la base de donnée

On va aller modifier la méthode up() du fichier de migration de la table users, avant de la lancer, comme ceci :

Schema::create('users', function (Blueprint $table) {
    $table->increments('id');
    $table->string('name');
    $table->string('email')->unique();
    $table->string('password');
    $table->rememberToken();
    $table->string('google2fa_secret')->nullable();
    $table->timestamps();
});

Maintenant, il faut modifier le modèle App\User pour ajouter ce champ google2fa_secret à la propriété fillable, pour pouvoir modifier le champ, et en hidden, pour que sa valeur ne soit jamais affichée quand vous retournez l’objet complet, qui sera casté en JSON (on ne sait jamais).

Les controllers

Et hop, dans la console :

php artisan make:controller GoogleAuthController

Et on va tout de suite aller y coller ce code :

<?php

namespace App\Http\Controllers;

use App\Http\Requests\ActivateSecretValidation;
use Illuminate\Http\Request;
use Base32\Base32;

/**
 * Class GoogleAuthController
 * @package App\Http\Controllers
 */
class GoogleAuthController extends Controller
{

    protected $redirectTo = '/';


    /**
     * @param Request $request
     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
     */
    public function enableTwoFactor(Request $request)
    {
        //Generate a secret key in Base32 format
        $secret = strtoupper(Base32::encode(random_bytes(10)));
        $user = auth()->user();

        session(['google2fa_secret' => \Crypt::encrypt($secret), '2fa:user:id' => auth()->user()->id]);

        //generate image for QR barcode
        $QRCode = \Google2FA::getQRCodeGoogleUrl(
            config('app.name'),
            $user->email,
            $secret,
            200
        );

        return view('2fa/enable', ['image' => $QRCode,
            'secret' => $secret]);
    }


    /**
     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
     */
    public function disableTwoFactor()
    {
        $user = auth()->user();

        $user->google2fa_secret = null;
        $user->save();

        return view('2fa/disable');
    }

    /**
     * @param ActivateSecretValidation $request
     * @return \Illuminate\Http\RedirectResponse
     */
    public function activateTwoFactor(ActivateSecretValidation $request)
    {

        auth()->user()->google2fa_secret = session('google2fa_secret');
        auth()->user()->save();
        session()->forget(['google2fa_secret', '2fa:user:id']);

        return redirect()->intended($this->redirectTo)->with(['success' => 'Your account is secured !']);
    }

}

3 méthodes : une pour la demande d’activation, qui va générer le QRcode et demander le code de confirmation, une autre pour la demande de désactivation, et enfin une dernière qui va servir à envoyer les données de confirmation, pour valider l’activation de la double authentification. Avec cette 3e méthode, on n’activera la double authentification QUE si l’user a bien confirmé le code.

On va pouvoir maintenant utiliser la méthode authenticated() qui est un petit hook qu’on va surcharger, puisque la méthode de base est simplement vide. Donc on ouvre LoginContoller et on ajoute cette méthode, qui va surcharger celle du Trait :

protected function authenticated(Request $request, Authenticatable $user)
{
    if (!is_null($user->google2fa_secret)) {
        \Auth::logout();

        $request->session()->put('2fa:user:id', $user->id);

        return redirect('2fa/validate');
    }

    return redirect()->intended($this->redirectTo);
}

Donc si l’user a activé la double authentification, il faut déconnecter l’utilisateur, et lui ajouter son user id en session. Quand c’est fait, on peut le diriger vers une page dans laquelle il doit saisir les codes de validation.

Si c’est désactivé, on ne le déconnecte pas et on l’emmène là où il voulait aller.

activate-double-auth

Maintenant, on ajoute la méthode validate2fa() dans LoginController, pour s’assurer que la valeur existe en session, et renvoyer l’user sur la page login si ce n’est pas le cas :

public function validate2fa()
{
    if (session('2fa:user:id')) {
        return view('2fa/validate');
    }

    return redirect('login');
}

Puis une méthode qui va servir à récupérer les données postées pour valider la double authentification :

public function postValidate2fa(SecretRequestValidation $request)
{
    $userId = $request->session()->pull('2fa:user:id');

    //login and redirect user
    \Auth::loginUsingId($userId);

    return redirect()->intended($this->redirectTo);
}

Et on va donc créer l’objet request correpondant :

php artisan make:request SecretRequestValidation

Et on va coller ceci dedans :

<?php

namespace App\Http\Requests;

use App\User;
use Illuminate\Foundation\Http\FormRequest;

/**
 * Class SecretRequestValidation
 * @package App\Http\Requests
 */
class SecretRequestValidation extends FormRequest
{

    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        try {
            User::findOrFail(
                session('2fa:user:id')
            );
        } catch (\Exception $e) {
            return false;
        }

        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'totp' => 'bail|required|digits:6|token_is_ok',
        ];
    }
}

Oui on va créer une (2 en fait) règle de validation supplémentaire, en créant une nouvelle classe dans app/Http/Requests :

<?php

namespace App\Http\Requests;

use App\User;
use Illuminate\Validation\Validator;

/**
 * Class CustomValidator
 * @package App\Http\Requests
 */
class CustomValidator extends Validator
{
    /**
     * @param $attribute
     * @param $value
     * @param $parameters
     * @return bool
     */
    public function validateTokenIsOk($attribute, $value, $parameters)
    {
        try {
            $user = User::findOrFail(session('2fa:user:id'));

        } catch (\Exception $exc) {
            return false;
        }

        $secret = \Crypt::decrypt($user->google2fa_secret);

        return \Google2FA::verifyKey($secret, $value);
    }

    /**
     * @param $attribute
     * @param $value
     * @param $parameters
     * @return mixed
     */
    public function validateTokenIsValid($attribute, $value, $parameters)
    {
        $secret = \Crypt::decrypt(session('google2fa_secret'));

        return \Google2FA::verifyKey($secret, $value);
    }
}

Il faut aussi modifier AppServiceProvider pour ajouter ces règles au boot. Donc on ajoute ceci à la méthode boot() :

public function boot()
{
    $this->app['validator']->resolver(function($translator, $data, $rules, $messages) {
        return new CustomValidator($translator, $data, $rules, $messages);
    });
}

Pensez à ajouter le use.

On peut maintenant valider la dernière requête (qu’on va d’abord créer) :


php artisan make:request ActivateSecretValidation

Et voici son contenu :

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

/**
 * Class ActivateSecretValidation
 * @package App\Http\Requests
 */
class ActivateSecretValidation extends FormRequest
{

    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
       return \Auth::check();
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'totp' => 'bail|required|digits:6|token_is_valid',
        ];
    }
}

Les routes

Les controllers sont prêts. Maintenant, il faut que les routes soient disponibles. Pour rappel, il faut une route pour activer la double authentification, une pour la désactiver. Une autre pour afficher un formulaire dans lequel l’user va pouvoir taper le code disponible dans l’application Google Autenticator, et enfin, la dernière url sur laquelle il va envoyer ce formulaire.


Route::get('/2fa/enable', 'GoogleAuthController@enableTwoFactor');
Route::get('/2fa/disable', 'GoogleAuthController@disableTwoFactor');
Route::post('/2fa/activate', 'GoogleAuthController@activateTwoFactor');

Route::get('/2fa/validate', 'Auth\LoginController@validate2fa');
Route::post('/2fa/validate', ['middleware' => 'throttle:5', 'uses' => 'Auth\LoginController@postValidate2fa']);

Les vues

3 vues sont à prévoir :

  • 1 pour l’activation
  • 1 pour la désactivation
  • 1 pour la validation après le login

On ne traîne pas, voici les 3 fichiers blade qui se passent d’explication.

disable.blade.php :

@extends('layouts.app')

@section('content')
    <div class="container">
        <div class="row">
            <div class="col-md-10 col-md-offset-1">
               <p>
                   Double authentification désactivée. Mauvaise idée.
               </p>
            </div>
        </div>
    </div>
@endsection

enable.blade.php

@extends('layouts.app')

@section('content')
    <div class="container">
        <div class="row">
            <div class="col-md-10 col-md-offset-1">
                <div class="panel panel-default">
                    <div class="panel-heading">Secret Key</div>

                    <div class="panel-body">
                        Ouvrez Google Authenticator et scannez le QR code suivant :
                        <br />
                        <img alt="Image of QR barcode" src="{{ $image }}" />

                        <br />
                        Si vous avez un souci avec le QR code, vous pouvez taper ce code: <code>{{ $secret }}</code>
                        <br /><br />
                    </div>

                    <form class="form-horizontal" role="form" method="POST" action="/2fa/activate">
                        {!! csrf_field() !!}

                        <div class="form-group{{ $errors->has('totp') ? ' has-error' : '' }}">
                            <label class="col-md-4 control-label">Clé de sécurité</label>

                            <div class="col-md-6">
                                <input type="number" class="form-control" name="totp">

                                @if ($errors->has('totp'))
                                    <span class="help-block">
                                            <strong>{{ $errors->first('totp') }}</strong>
                                        </span>
                                @endif
                            </div>
                        </div>

                        <div class="form-group">
                            <div class="col-md-6 col-md-offset-4">
                                <button type="submit" class="btn btn-primary">
                                    <i class="fa fa-btn fa-mobile"></i>Valider
                                </button>
                            </div>
                        </div>
                    </form>

                </div>
            </div>
        </div>
    </div>
@endsection

et validate.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">Double Authentification</div>

                    <div class="panel-body">
                        <form class="form-horizontal" role="form" method="POST" action="/2fa/validate">
                            {!! csrf_field() !!}

                            <div class="form-group{{ $errors->has('totp') ? ' has-error' : '' }}">
                                <label class="col-md-4 control-label">Clé de sécurité</label>

                                <div class="col-md-6">
                                    <input type="number" class="form-control" name="totp">

                                    @if ($errors->has('totp'))
                                        <span class="help-block">
                                            <strong>{{ $errors->first('totp') }}</strong>
                                        </span>
                                    @endif
                                </div>
                            </div>

                            <div class="form-group">
                                <div class="col-md-6 col-md-offset-4">
                                    <button type="submit" class="btn btn-primary">
                                        <i class="fa fa-btn fa-mobile"></i>Valider
                                    </button>
                                </div>
                            </div>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </div>
@endsection

Et voilà, c’est prêt !
Les sources de ce tuto sont dispo sur Github.

Crédits photo : Raphael Schaller raphaelphoto.ch

metrogeek

Laisser un commentaire