Apprentissage avancé de la Programmation Backend avec Laravel et PHP
Apprentissage avancé de la Programmation Backend avec Laravel et PHP

WebAuthn, OTP et tokens API avec Laravel Sanctum/Passport

Introduction à l'authentification moderne et robuste

Dans le paysage numérique actuel, la sécurité de l'authentification est primordiale. Les attaques par hameçonnage (phishing), les fuites de mots de passe et la complexité de la gestion des identités constituent des défis constants pour les développeurs et les utilisateurs. Laravel, en tant que framework backend de premier plan, offre des outils puissants pour bâtir des systèmes d'authentification sécurisés et flexibles.

Cette leçon explorera trois piliers de l'authentification moderne :

  1. WebAuthn (FIDO2) : L'avenir de l'authentification sans mot de passe, résistant au phishing.
  2. OTP (One-Time Passwords) : L'authentification à deux facteurs (2FA) pour une couche de sécurité supplémentaire.
  3. Tokens API avec Laravel Sanctum/Passport : Des mécanismes robustes pour sécuriser les API de vos applications single-page (SPA), mobiles et les intégrations tierces.

Nous verrons comment intégrer ces technologies au sein d'une application Laravel, en tirant parti des solutions offertes par le framework.

1. WebAuthn (FIDO2) : L'ère de l'authentification sans mot de passe

1.1 Qu'est-ce que WebAuthn ?

WebAuthn (Web Authentication) est une spécification du W3C, développée par la FIDO Alliance, qui permet aux sites web d'intégrer une authentification forte basée sur la cryptographie à clé publique. Elle fait partie de l'initiative FIDO2, visant à remplacer les mots de passe par des méthodes d'authentification plus sûres et plus conviviales.

Plutôt que d'utiliser un secret partagé (le mot de passe), WebAuthn utilise une paire de clés cryptographiques :

  • Une clé privée : Stockée de manière sécurisée sur un authentificateur (clé USB FIDO2, module TPM d'un ordinateur, Touch ID/Face ID sur smartphone, etc.). Elle ne quitte jamais l'authentificateur.
  • Une clé publique : Transmise au serveur lors de l'enregistrement et stockée avec l'identité de l'utilisateur.

1.2 Avantages clés de WebAuthn

  • Résistance au Phishing : Contrairement aux mots de passe et même aux OTP classiques, WebAuthn est intrinsecquement résistant aux attaques par hameçonnage. L'authentificateur vérifie l'origine du site avant de signer une requête.
  • Sécurité renforcée : Repose sur des principes cryptographiques robustes, rendant les attaques par force brute ou les vols de bases de données moins efficaces.
  • Expérience utilisateur améliorée : Fini la mémorisation de mots de passe complexes. Un simple contact avec un capteur d'empreintes digitales, une reconnaissance faciale ou un code PIN suffit.
  • Interoperabilité : Fonctionne sur différents navigateurs et systèmes d'exploitation, avec une variété d'authentificateurs.

1.3 Comment fonctionne WebAuthn (flux simplifié)

  1. Enregistrement (Registration) :

    • L'utilisateur initie l'enregistrement sur le site.
    • Le serveur envoie une "challenge" (un nombre aléatoire cryptographique) au navigateur.
    • Le navigateur demande à l'authentificateur de générer une nouvelle paire de clés et de signer le challenge.
    • L'authentificateur génère les clés, stocke la clé privée, et renvoie la clé publique (ainsi que d'autres métadonnées et la signature du challenge) au navigateur.
    • Le navigateur envoie ces informations au serveur.
    • Le serveur vérifie la signature du challenge avec la clé publique fournie et stocke la clé publique associée à l'utilisateur.
  2. Authentification (Assertion) :

    • L'utilisateur tente de se connecter.
    • Le serveur envoie un nouveau challenge au navigateur.
    • Le navigateur demande à l'authentificateur de signer ce challenge avec la clé privée associée au site.
    • L'authentificateur demande la confirmation à l'utilisateur (empreinte, PIN, etc.).
    • L'authentificateur signe le challenge et renvoie la signature au navigateur.
    • Le navigateur envoie la signature au serveur.
    • Le serveur utilise la clé publique stockée pour vérifier la signature. Si elle est valide, l'utilisateur est authentifié.

1.4 Intégration avec Laravel

L'intégration de WebAuthn dans Laravel nécessite une bibliothèque côté serveur pour gérer les spécifications cryptographiques (générer les challenges, vérifier les signatures) et du JavaScript côté client pour interagir avec l'API navigator.credentials.create() et navigator.credentials.get().

Des packages comme laragear/webauthn ou simplewebauthn/webauthn-php (qui peuvent être utilisés dans un projet Laravel) simplifient grandement cette tâche.

Exemple conceptuel d'un contrôleur PHP pour l'enregistrement WebAuthn :

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Webauthn\Webauthn; // Supposons l'utilisation d'une lib Webauthn

class WebauthnController extends Controller
{
    protected $webauthn;

    public function __construct(Webauthn $webauthn)
    {
        $this->webauthn = $webauthn;
    }

    /**
     * Prépare les options d'enregistrement pour le client.
     */
    public function registerOptions(Request $request)
    {
        // Récupérer l'utilisateur actuel (ou l'utilisateur qui s'inscrit)
        $user = Auth::user() ?? User::find(1); // Exemple, à adapter

        // Générer les options d'enregistrement (challenge, RP ID, user ID, etc.)
        $publicKeyCredentialCreationOptions = $this->webauthn->prepareCreationOptions(
            $user, // L'entité utilisateur
            [], // Les informations d'authentificateur existantes pour éviter la réutilisation
            [
                'challenge' => random_bytes(32), // Un challenge cryptographique unique
                'rp' => [ // Relying Party - votre site web
                    'name' => config('app.name'),
                    'id' => parse_url(config('app.url'), PHP_URL_HOST),
                ],
                'user' => [
                    'id' => $user->id,
                    'name' => $user->email,
                    'displayName' => $user->name,
                ],
                // ... d'autres options comme 'attestation', 'timeout', etc.
            ]
        );

        // Stocker le challenge en session pour la vérification ultérieure
        $request->session()->put('webauthn_challenge', $publicKeyCredentialCreationOptions->getChallenge());

        // Retourner les options au client (front-end) au format JSON
        return response()->json($publicKeyCredentialCreationOptions);
    }

    /**
     * Vérifie la réponse d'enregistrement du client.
     */
    public function registerVerify(Request $request)
    {
        $user = Auth::user();
        $credential = $request->json()->all(); // La réponse de navigator.credentials.create()

        // Récupérer le challenge stocké en session
        $challenge = $request->session()->pull('webauthn_challenge');

        try {
            // Vérifier la credential et obtenir les informations de l'authentificateur
            $publicKeyCredentialSource = $this->webauthn->processCreate(
                $credential,
                $challenge,
                [], // Les informations d'authentificateur existantes
                true // Vérifier l'attestation si nécessaire
            );

            // Enregistrer la clé publique de l'authentificateur dans votre base de données
            $user->webauthnCredentials()->create([
                'type' => $publicKeyCredentialSource->getType(),
                'credential_id' => base64_encode($publicKeyCredentialSource->getCredentialId()),
                'public_key' => base64_encode($publicKeyCredentialSource->getPublicKey()),
                'counter' => $publicKeyCredentialSource->getCounter(),
                // ... autres champs pertinents
            ]);

            return response()->json(['message' => 'Authentificateur WebAuthn enregistré avec succès.']);

        } catch (\Throwable $e) {
            return response()->json(['error' => $e->getMessage()], 400);
        }
    }
}

Ce code PHP prépare les options pour le navigateur et vérifie la réponse. Côté front-end, JavaScript sera utilisé pour appeler navigator.credentials.create() avec les options fournies par registerOptions et envoyer le résultat à registerVerify.

2. OTP (One-Time Passwords) : L'authentification à deux facteurs (2FA)

2.1 Qu'est-ce que l'OTP ?

Les One-Time Passwords (OTP), ou mots de passe à usage unique, sont des codes générés dynamiquement qui ne peuvent être utilisés qu'une seule fois pour authentifier un utilisateur. Ils sont la pierre angulaire de l'authentification à deux facteurs (2FA) ou multi-facteurs, ajoutant une couche de sécurité "quelque chose que vous avez" (le générateur OTP) en plus de "quelque chose que vous savez" (le mot de passe).

Les types d'OTP les plus courants sont :

  • TOTP (Time-based One-Time Password) : Le code change toutes les 30 ou 60 secondes, basé sur l'heure actuelle et un secret partagé. C'est ce qu'utilisent Google Authenticator, Authy, etc.
  • HOTP (HMAC-based One-Time Password) : Le code change à chaque utilisation, basé sur un compteur et un secret partagé.

2.2 Comment fonctionne l'OTP (TOTP)

  1. Enregistrement :

    • Le serveur génère un secret cryptographique unique pour l'utilisateur.
    • Ce secret est généralement encodé dans un QR Code (conformément à la spécification otpauth).
    • L'utilisateur scanne ce QR Code avec une application d'authentification (Google Authenticator, Authy, Microsoft Authenticator, etc.). L'application stocke le secret.
    • Le serveur peut demander à l'utilisateur de confirmer un premier code généré par l'application pour s'assurer que la configuration a réussi.
  2. Authentification :

    • L'utilisateur saisit son mot de passe habituel.
    • Ensuite, il est invité à saisir un code OTP généré par son application d'authentification.
    • Le serveur, connaissant le secret de l'utilisateur et l'heure actuelle, génère le code TOTP qu'il s'attend à recevoir.
    • Si le code fourni par l'utilisateur correspond au code généré par le serveur (ou un code dans une petite "fenêtre" de temps pour tenir compte des décalages d'horloge), l'authentification est réussie.

2.3 Intégration avec Laravel

Plusieurs packages Laravel facilitent l'intégration des OTP, notamment pragmarx/google2fa-qrcode (qui inclut bacon/bacon-qr-code pour la génération des QR codes).

Étapes clés :

  1. Migration de la base de données : Ajouter une colonne two_factor_secret (et potentiellement two_factor_recovery_codes) à la table users.
  2. Génération du secret : Lors de l'activation du 2FA par l'utilisateur.
  3. Affichage du QR Code : Pour que l'utilisateur puisse scanner le secret.
  4. Vérification de l'OTP : À chaque tentative de connexion (après la vérification du mot de passe).

Exemple de contrôleur PHP pour la gestion de l'OTP :

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use PragmaRX\Google2FAQRCode\Google2FA; // Utilisation de la librairie

class TwoFactorAuthController extends Controller
{
    protected $google2fa;

    public function __construct(Google2FA $google2fa)
    {
        $this->google2fa = $google2fa;
    }

    /**
     * Affiche la page d'activation 2FA et génère le QR Code.
     */
    public function showTwoFactorEnableForm(Request $request)
    {
        $user = Auth::user();

        // Si l'utilisateur n'a pas encore de secret, en générer un nouveau
        if (empty($user->two_factor_secret)) {
            $user->two_factor_secret = $this->google2fa->generateSecretKey();
            $user->save();
        }

        // Générer l'URL du QR Code
        $qrCodeUrl = $this->google2fa->get<!-- qrcode_url(
            config('app.name'), // Nom de votre application
            $user->email,
            $user->two_factor_secret
        ); -->
        $qrCodeUrl = $this->google2fa->getQRCodeInline(
            config('app.name'), // Nom de votre application
            $user->email,
            $user->two_factor_secret
        );

        // La vue aura besoin de $qrCodeUrl et $user->two_factor_secret (pour l'affichage en texte si QR non scannable)
        return view('auth.two-factor-enable', compact('qrCodeUrl', 'user'));
    }

    /**
     * Active le 2FA après vérification du code initial.
     */
    public function enableTwoFactor(Request $request)
    {
        $request->validate([
            'code' => ['required', 'digits:6'],
        ]);

        $user = Auth::user();

        // Vérifier le code fourni par l'utilisateur
        $isValid = $this->google2fa->verifyKey(
            $user->two_factor_secret,
            $request->input('code')
        );

        if ($isValid) {
            $user->two_factor_enabled = true; // Assurez-vous d'avoir une colonne 'two_factor_enabled'
            $user->save();
            return redirect()->route('dashboard')->with('status', 'Authentification à deux facteurs activée !');
        }

        return back()->withErrors(['code' => 'Code de vérification invalide.']);
    }

    /**
     * Vérifie le code 2FA lors de la connexion.
     */
    public function verifyTwoFactor(Request $request)
    {
        $request->validate([
            'code' => ['required', 'digits:6'],
        ]);

        // L'utilisateur doit être temporairement stocké en session après la vérification du mot de passe
        // avant d'être redirigé vers cette page de vérification 2FA.
        $user = Auth::user(); // Ou récupérer l'utilisateur à partir de la session

        if (empty($user->two_factor_secret) || !$user->two_factor_enabled) {
            return redirect()->route('login'); // Pas de 2FA activé, rediriger
        }

        $isValid = $this->google2fa->verifyKey(
            $user->two_factor_secret,
            $request->input('code')
        );

        if ($isValid) {
            // L'utilisateur est maintenant entièrement authentifié.
            // Vous pouvez marquer la session comme 2FA validée.
            $request->session()->put('two_factor_passed', true);
            return redirect()->intended('/dashboard');
        }

        return back()->withErrors(['code' => 'Code 2FA invalide.']);
    }
}

Ce code illustre les fonctionnalités de base pour générer un secret, afficher le QR code, et vérifier un code OTP.

3. Tokens API avec Laravel Sanctum et Passport

Pour les applications modernes qui séparent le frontend (SPA, mobile) du backend (API), les tokens API sont essentiels pour l'authentification sans état. Laravel offre deux solutions robustes : Sanctum et Passport.

3.1 Laravel Sanctum : Léger et puissant

Laravel Sanctum est une solution d'authentification API légère et simple, idéale pour :

  • Les Single Page Applications (SPAs) qui communiquent avec le backend Laravel.
  • Les applications mobiles qui interagissent avec votre API.
  • L'émission de tokens d'accès personnels pour les utilisateurs ou les intégrations de services (par exemple, un utilisateur génère un token pour un script qui accède à ses données).

Sanctum gère deux types d'authentification principaux :

  1. Authentification SPA basée sur les sessions :

    • Fonctionne en tirant parti du système de sessions intégré de Laravel et des cookies.
    • Lorsqu'une SPA est hébergée sur le même domaine (ou un sous-domaine configuré), l'authentification est gérée par les cookies de session Laravel standards après une première connexion basée sur les identifiants.
    • Sanctum s'assure que les requêtes CORS et la protection CSRF fonctionnent correctement.
  2. Tokens d'accès personnels (Personal Access Tokens) :

    • Pour les applications mobiles ou les clients tiers qui ne partagent pas le même domaine.
    • Les utilisateurs peuvent générer des tokens (hachés et stockés dans la base de données) avec des "capacités" (scopes) spécifiques (ex: read, create-post).
    • Ces tokens sont envoyés dans l'en-tête Authorization: Bearer <token> des requêtes API.
    • Sanctum vérifie le token et les capacités requises par la route.

3.1.1 Mise en œuvre de Sanctum (Tokens d'accès personnels)

  1. Installation :

    composer require laravel/sanctum
    php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
    php artisan migrate
    
  2. Ajouter le trait à votre modèle User :

    use Laravel\Sanctum\HasApiTokens;
    
    class User extends Authenticatable
    {
        use HasApiTokens, Notifiable;
        // ...
    }
    
  3. Protéger les routes API : Utilisez le middleware auth:sanctum sur vos routes API.

    // routes/api.php
    use Illuminate\Http\Request;
    use Illuminate\Support\Facades\Route;
    
    Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
        return $request->user();
    });
    
    Route::middleware('auth:sanctum')->post('/posts', function (Request $request) {
        // Logique de création de post, nécessite la capacité 'create-post'
        if ($request->user()->tokenCan('create-post')) {
            return response()->json(['message' => 'Post créé avec succès!']);
        }
        return response()->json(['message' => 'Non autorisé.'], 403);
    });
    
  4. Génération de tokens côté serveur : Un utilisateur peut générer un token via une route ou une interface.

    Exemple de contrôleur pour générer un token personnel :

    <?php
    
    namespace App\Http\Controllers;
    
    use Illuminate\Http\Request;
    use Illuminate\Support\Facades\Auth;
    
    class PersonalAccessTokenController extends Controller
    {
        public function createToken(Request $request)
        {
            $request->validate([
                'name' => 'required|string',
                'abilities' => 'nullable|array',
            ]);
    
            $user = Auth::user();
    
            if (!$user) {
                return response()->json(['message' => 'Non authentifié.'], 401);
            }
    
            // Créer le token avec les capacités spécifiées
            $token = $user->createToken(
                $request->name,
                $request->input('abilities', []) // Utilise un tableau vide si 'abilities' n'est pas fourni
            )->plainTextToken; // Récupère le token brut pour l'utilisateur
    
            return response()->json([
                'message' => 'Token créé avec succès!',
                'token' => $token,
            ]);
        }
    }
    

    L'utilisateur devra stocker ce $token en toute sécurité, car il ne sera affiché qu'une seule fois.

3.2 Laravel Passport : Le serveur OAuth2 complet

Laravel Passport est une implémentation complète et facile à utiliser du protocole OAuth2 pour votre application Laravel. Il est conçu pour les scénarios où vous avez besoin d'une autorisation robuste pour des applications tierces ou des clients complexes.

Quand utiliser Passport ?

  • Vous construisez une API publique qui sera consommée par de nombreuses applications tierces.
  • Vous avez besoin de différents types de "grants" OAuth2 (Authorization Code, Password Grant, Client Credentials, Implicit).
  • Vous devez émettre des tokens de rafraîchissement (refresh tokens) pour maintenir des sessions longues sans réauthentification.
  • Vous voulez une conformité totale avec la spécification OAuth2.

Passport fournit un système complet avec des tables pour les clients OAuth, les jetons d'accès, les jetons de rafraîchissement, et les portées (scopes).

3.2.1 Mise en œuvre de Passport (aperçu)

  1. Installation :

    composer require laravel/passport
    php artisan migrate
    php artisan passport:install # Crée les clés de chiffrement et les clients par défaut
    
  2. Configuration du modèle User :

    use Laravel\Passport\HasApiTokens;
    
    class User extends Authenticatable
    {
        use HasApiTokens, Notifiable;
        // ...
    }
    
  3. Enregistrement des routes Passport : Dans votre AuthServiceProvider.php, appelez Passport::routes() dans la méthode boot().

    use Laravel\Passport\Passport;
    
    public function boot()
    {
        $this->registerPolicies();
    
        Passport::routes(); // Enregistre toutes les routes nécessaires à Passport
    }
    
  4. Protéger les routes API : Utilisez le middleware auth:api.

    Route::middleware('auth:api')->get('/user', function (Request $request) {
        return $request->user();
    });
    
  5. Exemple de grant Passport (Password Grant) : Souvent utilisé pour les applications mobiles de première partie, où l'application "fait confiance" à l'utilisateur pour lui donner ses identifiants, et l'application cliente peut ensuite obtenir un token d'accès.

    # Créer un client pour le Password Grant (sera utilisé par l'application mobile/SPA)
    php artisan passport:client --password
    

    Ensuite, l'application cliente ferait une requête POST à /oauth/token avec grant_type=password, client_id, client_secret, username, et password pour obtenir un token d'accès et un token de rafraîchissement.

3.3 Quand utiliser Sanctum ou Passport ?

  • Utilisez Sanctum si :

    • Vous construisez une SPA qui consomme votre propre API Laravel sur le même domaine (ou sous-domaine).
    • Vous avez une application mobile qui consomme votre propre API Laravel.
    • Vous avez besoin d'un moyen simple pour les utilisateurs de générer des tokens API pour des usages personnels ou des intégrations légères (ex: un script CLI accédant à leurs données).
    • Vous n'avez pas besoin de toutes les complexités du protocole OAuth2 (Authorization Code Grant, Refresh Tokens, etc.).
  • Utilisez Passport si :

    • Vous avez besoin d'un serveur OAuth2 complet pour autoriser des applications tierces à accéder à vos ressources pour le compte de vos utilisateurs (ex: "Connectez-vous avec Google", mais pour votre propre API).
    • Vous avez besoin de différents types de "grants" OAuth2.
    • Vous avez besoin de gérer des tokens de rafraîchissement pour des sessions de longue durée.
    • La conformité avec la spécification OAuth2 est une exigence.

En général, commencez par Sanctum si vos besoins sont simples. Vous pourrez toujours migrer vers Passport si vos exigences en matière d'autorisation deviennent plus complexes et nécessitent une implémentation OAuth2 complète.

4. Architecture d'authentification combinée

Une application Laravel moderne peut tirer parti de ces trois technologies en les combinant pour offrir une sécurité maximale et une flexibilité d'accès :

  1. Authentification initiale : Les utilisateurs se connectent traditionnellement avec un mot de passe ou, mieux encore, utilisent WebAuthn pour une connexion sans mot de passe et résistante au phishing.
  2. Deuxième facteur : Si la sécurité est critique, l'utilisateur peut activer l'authentification à deux facteurs (2FA) via OTP. Après la première étape de connexion, un code OTP est demandé.
  3. Accès API : Pour les applications clientes (SPA, mobile) ou les intégrations, des tokens API (Sanctum ou Passport) sont utilisés pour l'authentification des requêtes sans état. L'utilisateur peut générer ces tokens via l'interface web, après s'être authentifié avec WebAuthn/mot de passe + OTP.

Cette approche multicouche offre une robustesse significative, protégeant l'application contre divers vecteurs d'attaque tout en offrant une expérience utilisateur moderne et adaptable.

Conclusion et Résumé

Nous avons exploré trois piliers essentiels de l'authentification et de l'autorisation dans les applications Laravel modernes :

  • WebAuthn représente l'avenir de l'authentification sans mot de passe, en tirant parti de la cryptographie à clé publique pour offrir une résistance supérieure au phishing et une meilleure convivialité.
  • Les OTP (One-Time Passwords) fournissent une couche de sécurité supplémentaire via l'authentification à deux facteurs, protégeant les comptes même en cas de compromission du mot de passe.
  • Laravel Sanctum et Passport sont des solutions robustes pour l'authentification API, Sanctum étant idéal pour les SPAs et les tokens d'accès personnels légers, tandis que Passport fournit une implémentation complète du protocole OAuth2 pour les besoins d'autorisation plus complexes avec des tiers.

En combinant ces technologies de manière judicieuse, vous pouvez construire des applications Laravel sécurisées, flexibles et prêtes à relever les défis de l'authentification dans l'écosystème web actuel. Le choix de la bonne approche dépendra toujours des besoins spécifiques de votre application, de son public cible et de son niveau d'exposition. Prioriser la sécurité par couches est toujours la meilleure stratégie.