Laravel 7 e l'autenticazione a 2 fattori (2FA)

Piccola pausa da Flutter. Per un progetto in sviluppo basato su Laravel 7, avevo bisogno di attivare sugli utenti l'autenticazione a due fattori con OTP su cellulare. In questo articolo vi presento la mia soluzione che fa uso di Google Authenticator. 


Il progetto di base 

Posizioniamoci nella nostra cartella dei progetti Laravel e creiamo da zero un nuovo progetto, magari dandogli il nome "laravel-2fa", con i normali comandi da shell:


composer create-project --prefer-dist laravel/laravel:^7.30.1 laravel-2fa


e avviamo Visual Studio Code per lavorare sul progetto. Da terminale diamo i comandi di installazione necessari per la UI di laravel e per creare lo scaffold delle pagine di autenticazione  


composer require laravel/ui:^2.4

php artisan ui bootstrap --auth


e quindi i comandi di installazione e di avvio dei pacchetti NPM


npm install

npm run dev


Con phpMyAdmin creiamo un database su cui far girare la nostra applicazione e sistemiamo i riferimenti nel file .env. Per evitare di dover ripetere la registrazione dell'utente durante i vari test, nella cartella database\seeds aggiungiamo un file UsersSeeder.php che ne crei uno (email: admin@test2fa.it, password: password) da utilizzare:


<?php

use App\User;
use Illuminate\Database\Seeder;

class UsersSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {

        $new_user = new User();
        $new_user->name = 'admin';
        $new_user->email = 'admin@test2fa.it';
        $new_user->password = bcrypt('password');
        $new_user->save();


    }
}


e modifichiamo i DatabaseSeeder.php in questo modo


<?php

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     *
     * @return void
     */
    public function run()
    {
        $this->call(UsersSeeder::class);
    }
}


Da terminale facciamo girare le migrazioni per creare la tabella standard degli utenti e seminiamola con i dati che abbiamo appena definito.


php artisan migrate --seed


Avviamo il progetto e verifichiamo che le funzionalità standard di autenticazione previste Laravel siano funzionanti.


Se tutto funziona, possiamo andare oltre.

La verifica a 2 passaggi e Google Authenticator

La verifica in due passaggi permette di aumentare la sicurezza di un account, richiedendo un secondo passaggio di verifica ogni volta che si esegue l'accesso. Oltre alla normale password, se è attiva la 2FA (two-factor-authetication) servirà un codice OTP (one-time-password) generato e/o ricevuto su un diverso dispositivo.

Quando si imposta l'autenticazione a due fattori su un account che si desidera proteggere con Google Authenticator, l'app genera un codice a sei cifre, aggiornato ogni 30 secondi, che deve successivamente essere inserito per completare l’accesso all'account. Una particolarità di Google Authenticator è che se l'orologio del dispositivo è perfettamente sincronizzato, l'app funziona anche senza bisogno di connessione internet. 

L'app è disponibile su sia su Android che su iOS, liberamente scaricabile dai rispettivi store e per completare i test vi consiglio di farlo.

   

Nel nostro progetto Laravel avremo quindi la necessità di generare quanto necessario per associare il nostro account all'app Google Authenticator e gestire la richiesta e l'inserimento del codice OTP generato.



Integriamo il progetto

Per il supporto di Google Authenticator, utilizzeremo un package già disponibile che possiamo installare con il seguente comando da terminale:


composer require pragmarx/google2fa-laravel


Sempre da terminale, estraiamo il file di configurazione per permetterne la modifica 


php artisan vendor:publish --provider="PragmaRX\Google2FALaravel\ServiceProvider"


Il controller

Creiamo ora un controller per gestire l'abilitazione e la disattivazione dell'opzione 2FA da parte dell'utente:


php artisan make:controller Google2FAController


e modifichiamo il contenuto iniziandone a definire la struttura


<?php

namespace App\Http\Controllers;

use Crypt;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Validation\ValidatesRequests;

use Illuminate\Support\Facades\Auth;
use Hash;


class Google2FAController extends Controller
{
    use ValidatesRequests;

    /**
     * Create a new authentication controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('auth');
    }

    /**
     *
     * @param \Illuminate\Http\Request $request
     * @return \Illuminate\Http\Response
     */
    public function enableDisable()
    {


    }

    /**
     *
     * @param \Illuminate\Http\Request $request
     * @return \Illuminate\Http\Response
     */
    public function set2FA(Request $request)
    {

    }

    /**
     *
     * @param \Illuminate\Http\Request $request
     * @return \Illuminate\Http\Response
     */
    public function unset2FA(Request $request)
    {


    }
 

}


Come si vede, dovremo definire una funzione abilitazione/disabilitazione (enableDisable) oltre a due funzioni che ci permettano di aggiornare dati dell'utente con la scelta effettuata (set2FA e unset2FA).

Aggiornamento del database

Per memorizzare lo stato di attivazione della 2FA sugli utenti ci occorre modificare la tabella, quindi creiamo una migrazione di aggiornamento per aggiungere un campo:


php artisan make:migration add_google2fa_secret_to_users


Modifichiamo il file di migrazione appena creato con questo codice


<?php

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\Schema;

class AddGoogle2faSecretToUsers extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('users', function ($table) {
            $table->string('google2fa_secret')->nullable();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('users', function ($table) {
            $table->dropColumn('google2fa_secret');
        });
    }
}


che ci permette di aggiungere un campo di testo google2fa_secret e lanciamo la migrazione per aggiornate il DB


php artisan migrate


Le rotte

Definiamo delle rotte che possano essere gestite dal controller. quindi in web.php aggiungiamo queste righe.


Route::prefix('2fa')->name('2fa.')->group(function(){
    Route::post('/set2FA', 'Google2FAController@set2FA')->name('set2FA');
    Route::post('/unset2FA', 'Google2FAController@unset2FA')->name('unset2FA');
    Route::get('/set', 'Google2FAController@enableDisable')->name('enableDisable');
});


Le viste

Nella cartella delle viste, creiamo  una sottocartella 2fa e iniziamo a definire una vista per l'abilitazione e la disattivazione enable_disable.blade.php con il seguente contenuto


@extends('layouts.app')

@section('content')
<div class="container spark-screen">
    <div class="row">
        <div class="col-md-10 col-md-offset-1">
            <div class="card card-default">
                <div class="card-header">2FA with Google Authenticator</div>

                <div class="card-body">

                    <p>Two factor authentication (2FA) strengthens access security by requiring two methods (also referred to as factors) to verify your identity. Two factor authentication protects against phishing, social engineering and password brute force attacks and secures your logins from attackers exploiting weak or stolen credentials.</p>

                    @if(!Auth::user()->google2fa_secret)

                        <div class="row">
                            <div class="col-6">

                                1. Scan this QR code with your Google Authenticator App. Alternatively, you can use the code:: <strong><code>{{ $secret }}</code></strong>
                                <br /><br />
       
                                <div class="visible-print text-center">
                                    {!! $image !!}
                                </div>
                               
                                <form class="form-horizontal" method="post" action="{{ route('2fa.set2FA') }}">
                                    @csrf
                                    <div class="form-group{{ $errors->has('verify-code') ? ' has-error' : '' }}">
                                        <input type="hidden" name="secretcode" value="{{ $secret }}">
                                        <label for="secret" class="control-label">2. Enter the pin from Google Authenticator app:</label>
                                        <input id="secret" type="text" class="form-control col-md-4" name="secret" required>
                                        @if ($errors->has('verify-code'))
                                            <span class="help-block">
                                                <strong>{{ $errors->first('verify-code') }}</strong>
                                            </span>
                                        @endif
                                    </div>
                                    <button type="submit" class="btn btn-primary">Enable 2FA</button>
                                </form>                                

                            </div>
                            <div class="col-6">
                               
                                <div class="text-center">
                                    <p>Get Google Authenticator</p>
                                    <a href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=it&gl=US">
<img src="{{ asset("images/playstore.png")}}" alt=""></a>
                                    <a href="https://apps.apple.com/it/app/google-authenticator/id388497605">
<img src="{{ asset("images/applestore.png")}}" width="153" height="46" alt=""></a>
                                </div>
                               
                            </div>

                        </div>

                    @else
                        <div class="alert alert-success">
                            2FA is currently <strong>enabled</strong> on your account.
                        </div>
                        <form class="form-horizontal" method="post" action="{{ route('2fa.unset2FA') }}">
                            @csrf
                            <div class="form-group{{ $errors->has('current-password') ? ' has-error' : '' }}">
                                <label for="change-password" class="control-label">Current Password</label>
                                <input id="current-password" type="password" class="form-control col-md-4" name="current-password" required>
                                @if ($errors->has('current-password'))
                                    <span class="help-block">
                                        <strong>{{ $errors->first('current-password') }}</strong>
                                    </span>
                                @endif
                            </div>
                            <br />
                            <button type="submit" class="btn btn-primary ">Disable 2FA</button>
                        </form>
                    @endif

                    <br />
                    <a href="{{ url('/home') }}">Back to Home</a>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection


La vista così creata, se l'opzione 2FA è già attiva, mostra il pulsane di disattivazione, con richiesta della password utente (in modo che solo l'utente stesso possa disattivare l'opzione)


mentre se l'opzione non è attiva, genera al momento codice QRCode ed un codice in chiaro da usare in alternativa, per associare Google Authenticator (facendone la scansione dall'app o inserendo il codice in chiaro manualmente)


Per semplicità, modifichiamo la vista home.blade.php in modo da esporre lo stato dell'opzione 2FA e eventualmente i pulsanti per abilitare o disabilitare. In realtà queste opzioni dovrebbero essere rese disponibili su una eventuale pagina profilo dell'utente.


@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="card">
                <div class="card-header">{{ __('Dashboard') }}</div>

                <div class="card-body">
                    @if (session('status'))
                        <div class="alert alert-success" role="alert">
                            {{ session('status') }}
                        </div>
                    @endif
                    <br>    
                    @if (Auth::user()->google2fa_secret)
                        <a href="{{ url('2fa/set') }}" class="btn btn-primary">Disable 2FA</a>
                    @else
                        <a href="{{ url('2fa/set') }}" class="btn btn-primary">Enable 2FA</a>
                    @endif
                    <br>    

                    {{ __('You are logged in!') }}
                </div>
            </div>
        </div>
    </div>
</div>
@endsection


Completiamo il controller

Completiamo il controller definendo le funzioni che avevamo solo titolato.

Per la funzione enableDisable


    public function enableDisable()
    {

        //get user
        $user = $user = Auth::user();

        if ($user->google2fa_secret) {
            return view('2fa/enable_disable');
        } else {
            //generate new secret
            $google2fa = (new \PragmaRX\Google2FAQRCode\Google2FA());
            $secret = $google2fa->generateSecretKey();

            //generate image for QR barcode
            $imageDataUri = $google2fa->getQRCodeInline(
                env('APP_NAME', $request->getHttpHost()),
                $user->email,
                $secret,
                200
            );

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


se l'opzione non è attiva generiamo un codice di attivazione usando la funzione $google2fa->generateSecretKey() e costruiamo il QRCode usando lo stesso codice (aggiungiamo il nome dell'applicazione e l'email dell'utente solo come dati accessori in modo che se dall'app si fa la scansione del QRCode, saranno già proposti come dati identificativi dell'OTP) con la funzione  $google2fa->getQRCodeInline()  e passiamo questi dati alla vista che si occuperà di visualizzarli. Se l'opzione è già attiva, ci limitiamo a mostrare la pagina di dsattivazione.  

Per la funzione unset2FA


    public function unset2FA(Request $request)
    {
        $validatedData = $request->validate([
            'current-password' => 'required',
        ]);

        if (!(Hash::check($request->get('current-password'), Auth::user()->password))) {
            // The passwords matches
            return redirect()->back()->with('alert_type', 'danger')->with('alert_message', "Your password does not matches with your account password. Please try again.");
        }

        $user = Auth::user();
        $user->google2fa_secret = null;
        $user->save();
        return redirect('home')->with('alert_type', 'success')->with('alert_message', "2FA is now disabled.");
    }


verifichiamo che la password dell'utente inserita sia corretta (utilizzando la funzione Hash) e in caso positivo, svuotiamo il campo che abbiamo aggiunto sulla tabella degli utenti.

Per la funzione set2FA


    public function set2FA(Request $request)
    {

        $google2fa = (new \PragmaRX\Google2FAQRCode\Google2FA());

        //get user
        $user = $request->user();
        $secret = $request->input('secret');
        $secretcode = $request->input('secretcode');


        // check code
        $valid = $google2fa->verifyKey($secretcode, $secret);
        if ($valid) {
            //encrypt and then save secret
            $user->google2fa_secret = Crypt::encrypt($secretcode);
            $user->save();
            return redirect('home')->with('alert_type', 'success')->with('alert_message', "2FA is enabled successfully.");
        } else {
            return redirect('home')->with('alert_type', 'danger')->with('alert_message', "Invalid verification Code, Please try again..");
        }
    }


con la funzione $google2fa->verifyKey() verifichiamo che il codice OTP inserito sia coerente con il codice passato alla vista di abilitazione e in caso positivo memorizziamo nel campo aggiunto alla tabella utente il codice segreto, opportunamente criptato, sia per indicare che sull'utente è abilitata l'opzione 2FA, sia per per mettere la verifica degli OTP durante la fase login.

La verifica dell'OTP

Per gestire la verifica dell'OTP nella fase di login, abbiamo bisogno di introdurre un middleware.
Nella cartella app creiamo una sottocartella Support e in questa creiamo un file Goggle2FaAuthenticator.php con questo codice 


<?php

namespace App\Support;

use Crypt;
use PragmaRX\Google2FALaravel\Support\Authenticator;

class Google2FAAuthenticator extends Authenticator
{
    protected function canPassWithoutCheckingOTP()
    {

        if($this->getUser()->google2fa_secret == null)
            return true;
        return
            !$this->getUser()->google2fa_secret ||
            !$this->isEnabled() ||
            $this->noUserIsAuthenticated() ||
            $this->twoFactorAuthStillValid();
    }

    protected function getGoogle2FASecretKey()
    {
        $secret = Crypt::decrypt($this->getUser()
->{$this->config('otp_secret_column')});

        if (is_null($secret) || empty($secret)) {
            throw new InvalidSecretKey('Secret key cannot be empty.');
        }

        return $secret;
    }

}


e quindi da terminale un nuovo middleware 


php artisan make:middleware Google2FAMiddleware


con questo codice


<?php

namespace App\Http\Middleware;

use Closure;

class Google2FAMiddleware
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $authenticator = app(Google2FAAuthenticator::class)->boot($request);

        if ($authenticator->isAuthenticated()) {
            return $next($request);
        }

        return $authenticator->makeRequestOneTimePasswordResponse();
    }
}


registriamo il middleware creato nel file Kernel.php presente in app/Http 


    protected $routeMiddleware = [
        ....
        '2fa' => \App\Http\Middleware\Google2FAMiddleware::class,
        ....
    ];


Non ci resta che creare una rotta per gestire la verifica del codice OTP

Route::prefix('2fa')->name('2fa.')->group(function(){

    ...

    // 2fa middleware
    Route::post('/verify', function () {
        return redirect(URL()->previous());
    })->name('verify')->middleware('2fa');
});

e la relativa vista verify.blade.php sempre nella cartella view/2fa


@extends('layouts.app')
@section('content')
    <div class="container">
        <div class="row justify-content-md-center">
            <div class="col-md-8 ">
                <div class="card">
                    <div class="card-header">Two Factor Authentication</div>
                    <div class="card-body">
                        <p>Two factor authentication (2FA) strengthens access
 security by requiring two methods (also referred to as factors) to verify
your identity. Two factor authentication protects against phishing, social
engineering and password brute force attacks and secures your logins from
attackers exploiting weak or stolen credentials.</p>

                        @if ($errors->any())
                            <div class="alert alert-danger">
                                <ul>
                                    @foreach ($errors->all() as $error)
                                        <li>{{ $error }}</li>
                                    @endforeach
                                </ul>
                            </div>
                        @endif

                        Enter the pin from Google Authenticator app:<br/><br/>
                        <form class="form-horizontal" action="{{ route('2fa.verify') }}" method="POST">
                            @csrf
                            <div class="form-group{{ $errors->has('one_time_password-code') ? ' has-error' : '' }}">
                                <label for="one_time_password" class="control-label">One Time Password</label>
                                <input id="one_time_password" name="one_time_password" class="form-control col-md-4"  type="text" required/>
                            </div>
                            <button class="btn btn-primary" type="submit">Authenticate</button>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </div>
@endsection



Abbiamo terminato.


Come per gli articoli precedenti potete trovare il codice completo tra le mie repo GitHub: https://github.com/luigimicco/laravel-2fa

Buon lavoro !

Commenti