Retour au cours

backend / php-laravel

Authentification : Breeze et Sanctum

Leçon 161 exercice

Explication

Ce que vous allez apprendre

  • Distinguer clairement quand utiliser Breeze (sessions web) et Sanctum (tokens API/SPA)
  • Générer et vérifier un token d'API avec des permissions (abilities) limitées
  • Protéger des routes API avec le middleware auth:sanctum
  • Créer des Policies pour centraliser l'autorisation fine par modèle
  • Comprendre pourquoi le mot de passe ne doit jamais apparaître dans une réponse JSON

Dans quel contexte ?

Une même application Laravel doit servir à la fois une interface web classique (avec connexion par formulaire et session) et une API consommée par une application mobile qui ne peut pas gérer de cookies de session. Ces deux besoins d'authentification, bien que liés, demandent des outils différents : Breeze pour le premier, Sanctum pour le second.

D'abord, deux besoins d'authentification fondamentalement différents

Breeze fournit un scaffolding complet d'authentification classique : formulaire de connexion, inscription, réinitialisation de mot de passe, le tout reposant sur des sessions et des cookies — exactement ce qu'attend un navigateur web. Sanctum, à l'inverse, cible deux cas : des tokens d'API pour des clients externes (mobile, scripts tiers), et une authentification par cookie stateful pour des SPA du même domaine que le backend.

SolutionMécanismeCas d'usage
BreezeSessions + cookiesApplication web classique avec formulaires
Sanctum (tokens)Bearer token dans l'en-têteAPI consommée par mobile ou service tiers
Sanctum (SPA)Cookie stateful même domaineFrontend JS séparé mais sur le même domaine

Prérequis

Il faut connaître les middlewares (leçon précédente), puisque l'authentification Sanctum s'applique via ->middleware('auth:sanctum') sur les routes protégées.

Ensuite, un token n'a pas besoin d'avoir tous les droits

$user->createToken('mobile-app', ['produits:lire']) génère un token avec des "abilities" limitées — ce token ne pourra, par exemple, que lire des produits, jamais en créer ou en supprimer, même s'il est intercepté par un attaquant. C'est un principe de sécurité fondamental : donner à chaque token le minimum de droits nécessaire.

Piège fréquent

Créer systématiquement des tokens avec toutes les permissions (createToken('app') sans abilities précisées) revient à annuler l'intérêt du système d'abilities. Toujours réfléchir, dès la création du token, à ce dont ce client a réellement besoin.

Il reste à protéger l'exposition accidentelle du mot de passe

Le tableau $hidden = ['password', 'remember_token'] sur le modèle User garantit que ces champs ne sont JAMAIS inclus quand le modèle est converti en JSON, même par erreur d'un développeur qui oublierait de les exclure manuellement. C'est une protection automatique et systématique, à ne jamais retirer.

Astuce

protected function casts(): array { return ['password' => 'hashed']; } hache automatiquement le mot de passe (avec bcrypt) dès qu'on l'assigne au modèle, sans jamais avoir à appeler Hash::make() manuellement à chaque endroit du code où un mot de passe est défini.

Maintenant, une autorisation plus fine que "connecté ou pas"

Savoir qu'un utilisateur est connecté ne suffit pas toujours : encore faut-il savoir s'il a le droit de modifier CE produit précis. Une Policy (ProduitPolicy) centralise cette logique par modèle, testable indépendamment, et appelable aussi bien depuis un contrôleur ($this->authorize('update', $produit)) que depuis une vue Blade (@can('update', $produit)).

Maintenant que l'authentification et l'autorisation sont en place, la prochaine leçon montre comment structurer proprement les réponses JSON d'une API avec les Resources Laravel, et comment gérer leur versionnement dans le temps.

Commandes & code

Authentification : Breeze et Sanctum

bash
# Laravel Breeze : scaffolding d'authentification classique (sessions, cookies)
composer require laravel/breeze --dev
php artisan breeze:install blade    # ou vue / react / api selon le frontend
php artisan migrate
npm install && npm run build

# Laravel Sanctum : tokens API + authentification SPA (cookies stateful pour SPA du même domaine)
composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate
php
<?php
// app/Models/User.php : le trait HasApiTokens ajoute les capacités Sanctum
namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Sanctum\HasApiTokens;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable {
    use HasApiTokens, HasFactory, Notifiable;

    protected $fillable = ['nom', 'email', 'password'];
    protected $hidden = ['password', 'remember_token']; // jamais sérialisés en JSON

    protected function casts(): array {
        return ['password' => 'hashed']; // hash automatique à l'assignation (bcrypt)
    }
}

// Contrôleur de connexion API : génère un token personnel
namespace App\Http\Controllers\Auth;

use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;

class AuthTokenController extends Controller {
    public function login(Request $request) {
        $credentials = $request->validate([
            'email' => ['required', 'email'],
            'password' => ['required'],
        ]);

        $user = User::where('email', $credentials['email'])->first();

        if (!$user || !\Hash::check($credentials['password'], $user->password)) {
            throw ValidationException::withMessages([
                'email' => ['Identifiants invalides.'],
            ]);
        }

        // Token avec abilities (scopes) limitées : ce token ne peut que lire
        $token = $user->createToken('mobile-app', ['produits:lire'])->plainTextToken;

        return response()->json(['token' => $token]);
    }

    public function logout(Request $request) {
        $request->user()->currentAccessToken()->delete();
        return response()->noContent();
    }
}
php
<?php
// routes/api.php : protéger les routes avec le middleware Sanctum
Route::middleware('auth:sanctum')->group(function () {
    Route::get('/moi', fn (Request $request) => $request->user());

    // Vérifier une ability spécifique dans un contrôleur ou middleware
    Route::post('/produits', [ProduitController::class, 'store'])
        ->middleware('abilities:produits:ecrire');
});
php
<?php
// Policies : autorisation fine par modèle (app/Policies/ProduitPolicy.php)
namespace App\Policies;

use App\Models\{User, Produit};

class ProduitPolicy {
    public function update(User $user, Produit $produit): bool {
        return $user->id === $produit->createur_id || $user->hasRole('admin');
    }

    public function delete(User $user, Produit $produit): bool {
        return $user->hasRole('admin');
    }
}

// Utilisation : $this->authorize('update', $produit); dans un contrôleur
// ou @can('update', $produit) dans une vue Blade

Résumé

  • Breeze = authentification web classique par sessions ; Sanctum = tokens API et SPA du même domaine.
  • HasApiTokens + createToken('nom', ['ability']) génère des tokens à portée limitée (abilities).
  • Les Policies centralisent les règles d'autorisation par modèle, testables indépendamment des contrôleurs.
  • 'password' => 'hashed' dans les casts hash automatiquement le mot de passe à l'assignation.

Exercices pratiques

1 disponible
1

Mission : limiter les degats d'un token API vole

Objectif : Corriger une generation de token trop permissive et ajouter une Policy pour une autorisation fine par produit.

Contexte

Le endpoint de connexion mobile genere ses tokens avec $user->createToken('mobile-app')->plainTextToken, sans preciser la moindre ability. Un audit de securite signale que si ce token venait a fuiter (log mal configure, interception reseau), il donnerait acces a TOUTES les actions de l'API, y compris la suppression de produits, alors que l'application mobile ne fait jamais que consulter des produits. Par ailleurs, n'importe quel utilisateur connecte peut actuellement modifier n'importe quel produit via l'API, meme s'il ne l'a pas cree lui-meme.

Résoudre l’exercice →