https://d226lax1qjow5r.cloudfront.net/blog/blogposts/2fa-logins-laravel-nexmo-dr/2fa-laravel.png

Connexion 2FA avec Laravel et Nexmo

Publié le May 18, 2021

Temps de lecture : 10 minutes

Cet article a été publié à l'origine sur michaelheap.com avant Michael ne rejoigne l'équipe de Nexmo l'équipe!

J'ai récemment écrit sur l'amorçage de Laravel avec l'authentification de l'utilisateur et à quel point c'est facile (sérieusement, cela prend moins de 5 minutes). Cela nous fournit un excellent point de départ pour nos Applications, mais ensuite je suis tombé sur ce billet sur l'intégration de l'authentification à deux facteurs avec Google Authenticator et j'ai commencé à penser à Nexmo Verify.

J'ai récemment intégré Verify dans un chatbot sans problème, et j'ai pensé qu'il pourrait être utile de l'intégrer dans mon flux de connexion Laravel.

Vonage API Account

To complete this tutorial, you will need a Vonage API account. If you don’t have one already, you can sign up today and start building with free credit. Once you have an account, you can find your API Key and API Secret at the top of the Vonage API Dashboard.

This tutorial also uses a virtual phone number. To purchase one, go to Numbers > Buy Numbers and search for one that meets your needs.

Collecte du numéro de téléphone de l'utilisateur

Nous devons collecter le numéro de téléphone de l'utilisateur - nous ne pouvons pas lui envoyer un message de vérification sans ce numéro. Nous pourrions collecter ce numéro après l'enregistrement de l'utilisateur, mais j'ai décidé de le collecter au moment de l'enregistrement.

La première chose à faire est de modifier la table des utilisateurs afin qu'un champ soit prêt à stocker le numéro de téléphone de l'utilisateur. Pour ce faire, créons une nouvelle migration pour modifier notre users table :

php artisan make:migration add_users_phone_number

Cela crée un fichier dans le dossier database/migrations appelé <current time>_add_users_phone_number.php. Ouvrez ce fichier et remplacez son contenu par ce qui suit :

<?php

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

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

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

Cette migration ajoute une colonne nommée phone_number lorsqu'elle est exécutée, et supprime cette colonne lorsqu'elle est annulée. Appliquez-la maintenant en exécutant php artisan migrate dans votre terminal.

Ensuite, nous devons ajouter une entrée de texte à notre formulaire d'inscription pour que l'utilisateur fournisse son numéro de téléphone. Modifiez resources/views/auth/register.blade.php et ajoutez ce qui suit au bas du formulaire, juste avant le bouton d'envoi :

<div class="form-group{{ $errors->has('phone_number') ? ' has-error' : '' }}">
    <label for="name" class="col-md-4 control-label">Phone Number</label>
    <div class="col-md-6">
        <input id="name" type="tel" class="form-control" name="phone_number" value="{{ old('phone_number') }}" required autofocus>

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

Si nous visitons maintenant le site http://localhost:8000/register, nous devrions voir le champ du numéro de téléphone au bas de notre formulaire d'inscription. Nous y sommes presque, mais il manque encore un élément clé : nous n'enregistrons pas le numéro fourni par l'utilisateur dans notre nouveau champ de la base de données.

Laravel conserve toute sa logique d'enregistrement d'un utilisateur dans le fichier app/Http/Controllers/Auth/RegisterController.php dans le fichier Ouvrez-le et jetez-y un coup d'œil - vous devriez voir une méthode validator et une méthode create . Nous devons modifier ces deux éléments pour enregistrer le numéro de téléphone de l'utilisateur. Nous allons devoir modifier ces deux méthodes pour enregistrer le numéro de téléphone de notre utilisateur.

Commençons par la méthode validator méthode. Nous devons ajouter une nouvelle entrée pour phone_number pour nous assurer que le numéro fourni est valide. J'ai choisi d'être assez strict avec mes règles de validation, en exigeant qu'il ait exactement 12 caractères et qu'il soit unique pour tous les utilisateurs - vous pouvez choisir d'être moins strict. Après avoir ajouté une règle de validation, votre méthode validator devrait ressembler à ce qui suit :

return Validator::make($data, [
    'name' => 'required|max:255',
    'email' => 'required|email|max:255|unique:users',
    'password' => 'required|min:6|confirmed',
    'phone_number' =>; 'required|size:12|unique:users',
]);

Une fois que ces données ont passé les règles de validation que nous avons spécifiées, nous devons les stocker dans la base de données. Pour ce faire, nous modifions la méthode create et ajoutons une ligne qui enregistre notre numéro de téléphone. Toutes les données de la requête entrante sont disponibles dans la variable $data il suffit donc d'ajouter une seule ligne :

return User::create([
    'name' => $data['name'],
    'email' => $data['email'],
    'password' => bcrypt($data['password']),
    'phone_number' =>; $data['phone_number']
]);

Si nous essayons d'ajouter un utilisateur maintenant, cela ne fonctionnera pas comme prévu. Ceci est dû à une fonctionnalité de sécurité dans Laravel qui empêche l'assignation massive de propriétés à une classe. Nous n'avons pas informé notre User que phone_number est un champ valide, elle rejettera donc notre demande d'enregistrement. Pour résoudre ce problème, éditez app/User.php et ajoutez phone_number au tableau $fillable tableau :

protected $fillable = [
    'name', 'email', 'password', 'phone_number'
];

Après avoir effectué ce changement, n'hésitez pas à créer un Account via la page d'inscription et vous connecter à notre application.

Ajout de Nexmo Verify

Maintenant que nous avons le numéro de téléphone de l'utilisateur, nous sommes en mesure de commencer à mettre en œuvre notre logique Verify. Laravel exécute la demande de connexion de l'utilisateur à travers app/Http/Controllers/Auth/LoginController.php pour vérifier si les informations d'identification fournies sont valides ou non. Si les informations d'identification sont valides, Laravel recherchera alors une méthode authenticated dans le fichier LoginController. Si la méthode existe, il exécutera la logique qu'elle contient. C'est ici que nous ajouterons notre logique d'authentification à deux facteurs.

Ouvrez app/Http/Controllers/Auth/LoginController.php et ajoutez ce qui suit en haut, à côté des autres déclarations use à côté des autres déclarations :

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Contracts\Auth\Authenticatable;

Nous avons besoin de ces trois use pour pouvoir saisir notre méthode authenticated que nous devrions ajouter ensuite. Ajoutez ce qui suit à la LoginController à la classe :

public function authenticated(Request $request, Authenticatable $user)
{
    Auth::logout();
    $request->session()->put('verify:user:id', $user->id);
    // @TODO: Send the Verify SMS here
    return redirect('verify');
}

Ce code déconnecte à nouveau l'utilisateur, en stockant son identifiant dans la session afin que nous sachions sous quel utilisateur il a essayé de se connecter. Une fois la demande de vérification terminée, nous utiliserons cet identifiant pour reconnecter automatiquement l'utilisateur.

Déclenchement d'une demande de Verify

Vous avez peut-être remarqué qu'il y a un @TODO pour ajouter la logique Verify SMS. Nous n'avons pas encore de moyen d'envoyer un SMS via Nexmo, donc nous allons nous en occuper ensuite. Heureusement, Nexmo a un package paquetage Laravel qui nous facilite la tâche. En suivant le README de ce projet, nous installons à la fois le client Nexmo et le fournisseur de service Laravel avec Composer :

composer require nexmo/client @beta composer require nexmo/laravel 1.0.0-beta3

Après l'installation, nous devons indiquer à Laravel que notre client existe. Pour ce faire, nous devons modifier deux sections dans config/app.php pour ce faire - providers et aliases.

Ajouter ce qui suit à providers:

Nexmo\Laravel\NexmoServiceProvider::class

Ajouter ce qui suit à aliases:

'Nexmo' => \Nexmo\Laravel\Facade\Nexmo::class

Enfin, nous devons exécuter php artisan vendor:publish pour générer notre fichier de configuration Nexmo. Une fois que nous avons exécuté cette commande, nous pouvons éditer config/nexmo.php et fournir nos identifiants d'API dans api_key et api_secret. Nous pouvons soit les fournir directement ici, soit utiliser la commande .env similaire au fichier de configuration de la base de données. Je vais utiliser le fichier .env J'ai donc modifié le fichier config/nexmo.php pour qu'il contienne ce qui suit :

'api_key' => env('NEXMO_KEY', ''),
'api_secret' => env('NEXMO_SECRET', ''),

Ensuite, dans .envj'ai ajouté deux entrées au bas du fichier - NEXMO_KEY et NEXMO_SECRET:

NEXMO_KEY=mykey NEXMO_SECRET=mysecret

Maintenant que le client Nexmo est configuré, nous pouvons retourner à app/Http/Controllers/Auth/LoginController.php et mettre en place notre système de notification. Remplacez le commentaire @TODO que nous avons laissé par le commentaire suivant :

$verification = Nexmo::verify()->start([
    'number' => $user->phone_number,
    'brand'  => 'Laravel Demo'
]);
$request->session()->put('verify:request_id', $verification->getRequestId());

Cela déclenchera une demande de vérification via Numbers au numéro de téléphone que nous avons enregistré pour cet utilisateur. Nous devrons également ajouter use Nexmo; au début du fichier pour que notre façade soit disponible. Une fois que vous aurez fait cela, vous pourrez vous connecter et déclencher une demande de vérification - mais ne le faites pas encore ! Nous n'avons pas de moyen pour que l'utilisateur fournisse son code de vérification, vous ne pourrez donc pas confirmer votre identité.

Verify the request

À la fin de LoginController::authenticated nous redirigeons l'utilisateur vers une /verify url. Il est temps d'enregistrer cette route avec Laravel et d'écrire une implémentation pour elle.

Ouvrir routes/web.php et ajoutez ce qui suit au bas de la page :

Route::get('/verify', 'VerifyController@show')->name('verify');
Route::post('/verify', 'VerifyController@verify')->name('verify');

Cela permet d'enregistrer deux itinéraires (a GET et a POST à /verify) que nous utiliserons pour vérifier le code d'un utilisateur. Nous avons dit à Laravel qu'il devait appeler les routes show et verify sur la méthode VerifyController pour ces requêtes, donc nous devrions générer le contrôleur en utilisant artisan:

php artisan make:controller VerifyController

Cela créera un fichier à l'adresse app/Http/Controllers/VerifyController.php - vous devez remplacer son contenu par ce qui suit :

<?php

namespace App\Http\Controllers;

use Auth;
use Nexmo;
use Illuminate\Http\Request;

class VerifyController extends Controller
{
    public function show(Request $request) {
        return view('verify');
    }

    public function verify(Request $request) {
        return 'Not Implemented';
    }
}

Ceci est suffisant pour afficher la vue verify lorsque quelqu'un fait une requête GET vers /verify. Une fois de plus, ce fichier n'existe pas encore, alors créons-le à resources/views/verify.blade.php avec le contenu suivant :

@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">Verify</div>

                <div class="panel-body">
                    <form class="form-horizontal" role="form" method="POST" action="{{ route('verify') }}">
                        {{ csrf_field() }}

                        <div class="form-group{{ $errors->has('code') ? ' has-error' : '' }}">
                            <label for="code" class="col-md-4 control-label">Code</label>

                            <div class="col-md-6">
                                <input id="code" type="number" class="form-control" name="code" value="{{ old('code') }}" required autofocus>

                                @if ($errors->has('code'))
                                    <span class="help-block">
                                        <strong>{{ $errors->first('code') }}</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">
                                    Verify Account
                                </button>
                            </div>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

Il y a beaucoup de HTML, mais tout ce qu'il fait, c'est afficher un formulaire unique avec un bouton d'envoi. Vous pouvez visiter la page Verify pour le voir maintenant.

Maintenant que nous avons notre page pour saisir notre code Verify, il ne nous reste plus qu'à vérifier le code fourni avec Nexmo. Remplacez votre méthode verify dans VerifyController par le code suivant. Cette méthode valide que nos données entrantes ont une longueur de 4 caractères (les codes de vérification Nexmo peuvent avoir une longueur de 4 ou 6 caractères, je travaille avec 4) puis vérifie le code fourni avec Nexmo. S'il n'est pas validé, une exception est levée et nous renvoyons une erreur à l'utilisateur. Sinon, nous récupérons l'identifiant de la session, nous connectons l'utilisateur et nous redirigeons vers le contrôleur d'accueil.

public function verify(Request $request) {
    $this->validate($request, [
        'code' => 'size:4',
    ]);

    try {
        Nexmo::verify()->check(
            $request->session()->get('verify:request_id'),
            $request->code
        );
        Auth::loginUsingId($request->session()->pull('verify:user:id'));
        return redirect('/home');
    } catch (Nexmo\Client\Exception\Request $e) {
        return redirect()->back()->withErrors([
            'code' => $e->getMessage()
        ]);

    }
}

À ce stade, notre intégration devrait fonctionner de bout en bout. Si vous enregistrez tous vos changements et essayez de vous connecter, vous devriez être redirigé vers la page verify et recevoir un message texte avec votre code de vérification. Saisissez le code et vous serez connecté comme prévu.

Félicitations ! Vous venez d'intégrer l'authentification à deux facteurs avec Nexmo Verify dans votre application Laravel.

Mettre un peu d'ordre dans les détails

Bien que cela fonctionne, il y a encore quelques problèmes à résoudre. Par exemple, un utilisateur peut se connecter une deuxième fois sans confirmer la première demande de Verify. Il peut également accéder à la page /verify sans avoir de demande de Verify active. Enfin, nous ne vérifions pas l'identité de l'utilisateur après l'enregistrement, mais seulement lorsqu'il se déconnecte et essaie de se reconnecter.

Nous n'allons pas résoudre ces questions dans ce billet - je vous laisse les découvrir !

Partager:

https://a.storyblok.com/f/270183/384x384/1c8825919c/mheap.png
Michael HeapAnciens de Vonage

Michael est un ingénieur logiciel polyglotte qui s'attache à réduire la complexité des systèmes et à les rendre plus prévisibles. Travaillant avec une variété de langages et d'outils, il partage son expertise technique avec des publics du monde entier lors de groupes d'utilisateurs et de conférences. Au quotidien, Michael est un ancien défenseur des développeurs chez Vonage, où il a passé son temps à apprendre, enseigner et écrire sur toutes sortes de technologies.