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

Autenticazione a due fattori via email

In questo capitolo analizzeremo una procedura per implementare l'autenticazione a due fattori via email nel nostro e-commerce creato con Laravel
In questo capitolo analizzeremo una procedura per implementare l'autenticazione a due fattori via email nel nostro e-commerce creato con Laravel
Link copiato negli appunti

L'autenticazione a due fattori crea un ulteriore livello di protezione tra il login e la sessione utente. Inviando un codice numerico one-time all'utente (nel nostro caso via e-mail, ma è possibile usare anche gli SMS), impediamo che un account compromesso consenta di effettuare modifiche sul profilo utente o anche acquisti indebiti. E' infatti improbabile che chi ha compromesso un account abbia anche gli accessi all'email e al telefono dell'utente.

Per implementare questa feature abbiamo innanzitutto bisogno di definire un nuovo modello e una nuova tabella per memorizzare temporaneamente il codice d'accesso collegato ad un profilo utente.

php artisan make:model AccessCode --migration

Il nostro schema avrà questa struttura:

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateAccessCodesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('access_codes', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('code', 4);
            $table->bigInteger('customer_id');
            $table->timestamps();
        });
    }
    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('access_codes');
    }
}

Useremo un codice numerico a 4 cifre a titolo d'esempio, anche se la lunghezza comunemente usata in produzione è di 6 o 8 cifre. Quindi creiamo il nostro modello:

namespace App;
use Illuminate\Database\Eloquent\Model;
class AccessCode extends Model
{
    protected $table = 'access_codes';
    protected $primaryKey = 'id';
    public $incrementing = true;
    protected $fillable = ['code', 'customer_id'];
}

La classe per l'invio del codice

Ora dobbiamo creare una classe di tipo Mailable per inviare il codice all'utente via email. L'unica proprietà che passeremo a questa classe sarà il codice di autenticazione.

namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class AccessCodeEmail extends Mailable
{
    use Queueable, SerializesModels;
    /**
     * Create a new message instance.
     *
     * @return void
     */
    public function __construct($code)
    {
        $this->code = $code;
    }
    /**
     * Build the message.
     *
     * @return $this
     */
    public function build()
    {
        return
            $this->from('phpecommerce@localhost')->
            subject('Your Access Code')->
            view('email.accesscode', [
                'code' => $this->code
            ]);
    }
}

Quindi creiamo la view in Blade che definirà il corpo dell'email:

<p>Dear customer,<br>
    your access code is <strong>{{ $code }}</strong>.</p>

Il passo successivo è la creazione di un metodo protetto nel nostro controller AJAX che genererà il codice di accesso, lo salverà e lo invierà all'utente.

protected function accessCode($customer)
    {
        $digits = '0123456789';
        $rand_str = str_shuffle($digits);
        $code = substr($rand_str, 0, 4);
        $accessCode = new AccessCode([
           'code' => $code,
           'customer_id' => $customer->id
        ]);
        $accessCode->save();
        Mail::to($customer->email)->send(new AccessCodeEmail($code));
    }

Quindi dobbiamo modificare il metodo di login eliminando il salvataggio in sessione dell'utente e inserendo il metodo appena creato.

$this->accessControl($customer);
$this->accessCode($customer);
return response()->json(['success' => true, 'customer_id' => $customer->id, 'from_checkout' => $from_checkout]);

Restituiamo anche l'ID dell'utente in modo che il codice JavaScript lato client possa inviarlo all'endpoint AJAX che validerà il codice di accesso usando sia il codice che l'ID dell'utente.

Validazione del codice di accesso

Lo step successivo consiste infatti nella validazione del codice di accesso a cui dedicheremo un metodo pubblico specifico del nostro controller.

public function validateAccessCode(Request $request)
    {
        $code = $request->get('code');
        $customer_id = (int) $request->get('customer_id');
        $from_checkout = (int) $request->get('from_checkout');
        if(!AccessCode::where([['customer_id', '=', $customer_id], ['code', '=', $code]])->exists()) {
            return response()->json(['error' => 'Invalid access code.']);
        }
        AccessCode::where([['customer_id', '=', $customer_id], ['code', '=', $code]])->delete();
        session()->put('logged_in', '1');
        session()->put('customer_id', $customer_id);
        return response()->json(['success' => true, 'from_checkout' => $from_checkout]);
    }

Se non esiste una corrispondenza nel database con i dati inseriti dall'utente, restituiamo immediatamente un errore e non proseguiamo oltre.

Se invece i dati sono corretti, cancelliamo il codice di accesso dal database e autentichiamo l'utente salvandolo nella sessione corrente.

Il codice lato client del form di login viene modificato in modo da mostrare il form di inserimento del codice di autenticazione con l'ID utente ricevuto via AJAX solo in caso di successo.

$( "#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 ) {
                    $( "[name=customer_id]", "#two-factor-auth-form" ).val( res.customer_id );
                    $( "#two-factor-auth-form" ).show();
                    $form.hide();
                } else {
                    $form.append( '<div class="alert alert-danger mt-4">' + res.error + '</div>' );
                }
            });
        });

Il codice che gestirà invece il form di inserimento del codice di autenticazione effettuerà un redirect in caso di successo o mostrerà il messaggio di errore qualora il codice non fosse valido.

$( "#two-factor-auth-form" ).on( "submit", function( e ) {
           e.preventDefault();
            var $form = $( this );
            $form.find( ".alert" ).remove();
            $.post( "/ajax/validate-access-code", $form.serialize(), function ( res ) {
                if( res.success ) {
                    var redirectTo = res.from_checkout === 1 ? "/checkout" : "/profile";
                    window.location = redirectTo;
                } else {
                    $form.append( '<div class="alert alert-danger mt-4">' + res.error + '</div>' );
                }
            });
        });

La scadenza del codice

Il nostro sistema può essere perfezionato se teniamo presente che la caratteristica di Laravel di aggiungere due timestamp MySQL alle collezioni può essere sfruttata per impostare una scadenza al codice di autenticazione.

Il problema maggiore di questa modifica consiste nel definire un limite di tempo che non sia eccessivo e che al contempo permetta all'utente di riuscire a completare l'operazione. Infatti potrebbero esserci dei ritardi nella consegna dell'email o dell'SMS e, se il limite temporale è troppo esiguo, l'utente potrebbe non inserirlo nel form nel limite consentito, dovendo quindi ripetere nuovamente l'operazione, cosa che produrrebbe frustrazione.

C'è poi anche un aspetto relativo all'accessibilità da considerare: alcuni utenti (come le persone anziane o con disabilità motoria) potrebbero avere difficoltà nel controllare l'email o il cellulare se la scadenza è nell'arco di pochi minuti. Un valore accettabile è quello di 1 ora, in modo da metterci al riparo da eventuali ritardi, permettendo agli utenti di effettuare l'operazione con calma e di non commettere errori.

Possiamo quindi scrivere il seguente metodo helper:

protected function isAccessCodeExpired($code)
{
    $limit = 60 * 60; // 1 ora
    return ((time() - strtotime($code->created_at)) > $limit);
}

Conclusione

In questo capitolo abbiamo visto come implementare un sistema di autenticazione a due fattori. Nel prossimo capitolo aggiungeremo il supporto all'invio di SMS.

Ti consigliamo anche