Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

Autenticazione a due fattori con OTP

Implementare l'autenticazione a due fattori via OTP (One Time Password) con Google Authenticator e il package per Laravel pragmarx/google2fa-laravel
Implementare l'autenticazione a due fattori via OTP (One Time Password) con Google Authenticator e il package per Laravel pragmarx/google2fa-laravel
Link copiato negli appunti

In questo capitolo concluderemo la rassegna sulle varie possibilità offerte dall'autenticazione a due fattori implementando l'accesso tramite OTP (One Time Password).

Ci sono varie App che forniscono questa funzionalità. Nel nostro esempio useremo Google Authenticator con il package per Laravel pragmarx/google2fa-laravel.

Installazione del package e migrazione

Per iniziare, installiamo il package con Composer:

composer require pragmarx/google2fa-laravel

Quindi abilitiamo la configurazione del package, sempre con Composer:

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

Dobbiamo creare ora un modello per conservare la chiave segreta generata da Google Authenticator e associarla ad un profilo cliente. Creeremo anche un controller per gestire la logica dell'autenticazione a due fattori.

php artisan make:model LoginAuth -m -c

Ora dobbiamo definire la struttura della nuova tabella ed effettuare una migrazione. Definiamo prima la tabella.

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateLoginAuthsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('login_auths', function (Blueprint $table) {
            $table->id();
            $table->integer('customer_id');
            $table->boolean('google2fa_enable')->default(false);
            $table->string('google2fa_secret')->nullable();
            $table->timestamps();
        });
    }
    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('login_auths');
    }
}

La tabella avrà anche un flag che permetterà al cliente di abilitare o disabilitare l'autenticazione a due fattori. customer_id collegherà questo modello al profilo cliente associato. Eseguiamo quindi la migrazione:

php artisan migrate

Il modello

Quindi definiamo il modello per abilitare la relazione associativa.

namespace App;
use Illuminate\Database\Eloquent\Model;
class LoginAuth extends Model
{
    protected $fillable = [
        'customer_id'
    ];
    public function customer()
    {
        return $this->belongsTo('App\Customer');
    }
}

Poi modifichiamo anche il modello del cliente inserendo il seguente metodo:

public function loginAuth()
    {
        return $this->hasOne('App\LoginAuth');
    }

Il controller

Passiamo ora al nuovo controller che abbiamo creato. Dobbiamo come prima cosa definire la logica alla base della route di arrivo dopo il primo login.

public function settings(Request $request)
    {
        if(!$request->session()->has('customer_id')) {
            return redirect('register');
        }
        $customer = Customer::find($request->session()->get('customer_id'));
        $google2fa_url = '';
        $secret_key = '';
        if ($customer->loginAuth()->exists()) {
            $google2fa = (new \PragmaRX\Google2FAQRCode\Google2FA());
            $google2fa_url = $google2fa->getQRCodeInline(
                'PHP E-commerce',
                $customer->email,
                $customer->loginAuth->google2fa_secret
            );
            $secret_key = $customer->loginAuth->google2fa_secret;
        }
        $data = array(
            'user' => $customer,
            'secret' => $secret_key,
            'google2fa_url' => $google2fa_url
        );
        return view('2fa.settings')->with('data', $data);
    }

Il metodo invia alla view l'oggetto cliente, la chiave segreta generata da Google e l'URL di dati che servirà a visualizzare l'immagine del QR code che verrà poi scansionato dall'utente con la sua App mobile. La view avrà questa struttura:

@if($data['user']->loginAuth === null)
                    <form class="form-horizontal" method="post" action="{{ route('generate') }}">
                        {{ csrf_field() }}
                        <div class="form-group">
                            <button type="submit" class="btn btn-primary">
                                Generate Secret Key to Enable 2FA
                            </button>
                        </div>
                    </form>
                    @elseif(!$data['user']->loginAuth->google2fa_enable)
                        <figure class="mt-5 mb-5" id="google-2fa">
                            <img src="{{$data['google2fa_url'] }}" id="qrcode" class="img-fluid">
                        </figure>
                    <form class="form-horizontal" method="post" action="{{ route('enable') }}">
                        {{ csrf_field() }}
                        <div class="form-group{{ $errors->has('verify-code') ? ' has-error' : '' }}">
                            <label for="secret" class="control-label">Authenticator Code</label>
                            <input id="secret" type="password" 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>
                    @elseif($data['user']->loginAuth->google2fa_enable)
                    <div class="alert alert-success">
                        2FA is currently <strong>enabled</strong> on your account.
                    </div>
                    <form class="form-horizontal" method="post" action="{{ route('disable') }}">
                        {{ csrf_field() }}
                        <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>
                        <button type="submit" class="btn btn-primary ">Disable 2FA</button>
                    </form>

La logica è questa: se l'utente non ha ancora abilitato l'autenticazione a due fattori, gli viene presentato il form per generare la chiave segreta. Viceversa, gli viene presentato un form per inserire il codice OTP ottenuto dall'App e quello per disabilitare l'autenticazione a due fattori (in questo caso dovrà reinserire la sua password).

Gli altri tre metodi del controller gestiscono appunto la generazione della chiave segreta e l'abilitazione e la disabilitazione della feature.

public function generate(Request $request)
    {
        if(!$request->session()->has('customer_id')) {
            return redirect('register');
        }
        $customer = Customer::find($request->session()->get('customer_id'));
        $google2fa = (new \PragmaRX\Google2FAQRCode\Google2FA());
        $login_auth = LoginAuth::firstOrNew(['customer_id' => $customer->id]);
        $login_auth->customer_id = $customer->id;
        $login_auth->google2fa_enable = 0;
        $login_auth->google2fa_secret = $google2fa->generateSecretKey();
        $login_auth->save();
        return redirect('2fa')->with('success', 'Secret key is generated.');
    }
    public function enable(Request $request)
    {
        if(!$request->session()->has('customer_id')) {
            return redirect('register');
        }
        $customer = Customer::find($request->session()->get('customer_id'));
        $google2fa = (new \PragmaRX\Google2FAQRCode\Google2FA());
        $secret = $request->input('secret');
        $valid = $google2fa->verifyKey($customer->loginAuth->google2fa_secret, $secret);
        if ($valid) {
            $customer->loginAuth->google2fa_enable = 1;
            $customer->loginAuth->save();
            return redirect('2fa')->with('success', '2FA is enabled successfully.');
        } else {
            return redirect('2fa')->with('error', 'Invalid verification Code, Please try again.');
        }
    }
    public function disable(Request $request)
    {
        if(!$request->session()->has('customer_id')) {
            return redirect('register');
        }
        $customer = Customer::find($request->session()->get('customer_id'));
        if (!(Hash::check($request->get('current-password'), $customer->password))) {
            return redirect()->back()->with('error', 'Your password does not match with your account password. Please try again.');
        }
        $customer->loginAuth->google2fa_enable = 0;
        $customer->loginAuth->save();
        return redirect('2fa')->with('success', '2FA is now disabled.');
    }

L'abilitazione prevede un check di verifica da parte dell'App per quello che concerne la coerenza tra la chiave registrata nel database e quella inviata dall'utente tramite il form. La chiave ha una scadenza predefinita di 30 secondi.

Il middleware

Ora dobbiamo creare un middleware per gestire la verifica del codice OTP. Creiamo la directory app/Support e creiamo una classe che estenda la classe specifica del package per l'autenticazione.

namespace App\Support;
use PragmaRX\Google2FALaravel\Support\Authenticator;
use App\Customer;
class Google2FAAuthenticator extends Authenticator
{
    protected function canPassWithoutCheckingOTP()
    {
        $customer = Customer::find(session()->get('customer_id'));
        if($customer->loginAuth === null) {
            return true;
        }
        return
            !$customer->loginAuth->google2fa_enable ||
            !$this->isEnabled() ||
            $this->noUserIsAuthenticated() ||
            $this->twoFactorAuthStillValid();
    }
    protected function getGoogle2FASecretKey()
    {
        $customer = Customer::find(session()->get('customer_id'));
        $secret = $customer->loginAuth->{$this->config('otp_secret_column')};
        if (is_null($secret) || empty($secret)) {
            throw new InvalidSecretKey('Secret key cannot be empty.');
        }
        return $secret;
    }
}

Quindi creiamo la struttura del middleware usando Composer:

php artisan make:middleware LoginAuthMiddleware

Il middleware avrà questa struttura:

namespace App\Http\Middleware;
use Closure;
use App\Support\Google2FAAuthenticator;
class LoginAuthMiddleware
{
    /**
     * 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();
    }
}

Se l'utente è autenticato, il controllo passa alla route che segue. Al contrario, viene richiesto l'inserimento del codice OTP.

Ora in app/Http/Kernel.php registriamo il nostro middleware nell'array $routeMiddleware.

protected $routeMiddleware = [
    // ---------
    '2fa' => \App\Http\Middleware\LoginAuthMiddleware::class,
];

Quindi creiamo la view per l'inserimento del codice OTP:

<form class="form-horizontal" action="{{ route('verify') }}" method="post">
                            {{ csrf_field() }}
                            <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>

Il gruppo di route

Infine definiamo un nuovo gruppo di route che per semplicità avranno il prefisso 2fa.

Route::group(['prefix'=>'2fa'], function(){
Route::get('/','LoginAuthController@settings');
Route::post('/generate','LoginAuthController@generate')->name('generate');
Route::post('/enable','LoginAuthController@enable')->name('enable');
Route::post('/disable','LoginAuthController@disable')->name('disable');
Route::post('/verify', function () {
        return redirect(URL()->previous());
    })->name('verify')->middleware('2fa');
});

Come ultima modifica, dobbiamo effettuare il redirect lato client dopo un login valido.

$( "#login-form" ).on( "submit", function( e ) {
            e.preventDefault();
            var $form = $( this );
            $form.find( ".alert" ).remove();
            $.post( "/ajax/login", $form.serialize(), function ( res ) {
                if( res.success ) {
                    window.location = "/2fa";
                } else {
                    $form.append( '<div class="alert alert-danger mt-4">' + res.error + '</div>' );
                }
            });
        });

Ti consigliamo anche