Laravel Passport

Com implementar OAuth2 complet a Laravel amb Passport: grant types, scopes, clients i tokens.

Què és Passport?#

Laravel Passport proporciona una implementació completa del protocol OAuth2 per a Laravel. OAuth2 és l'estàndard de la indústria per a l'autorització delegada: permet que aplicacions de tercers accedeixin als recursos d'un usuari sense que aquest hagi de compartir les seves credencials. Exemples coneguts d'OAuth2 són "Iniciar sessió amb Google" o les integracions de tercers amb GitHub.

Passport és necessari quan la teva aplicació ha de funcionar com a proveïdor OAuth2, és a dir, quan vols que desenvolupadors externs puguin crear aplicacions que accedeixin a les dades dels teus usuaris amb el seu consentiment. Per a casos més senzills com APIs privades, tokens d'API o autenticació SPA, Sanctum és una opció més lleugera i recomanada.

Instal·lació#

composer require laravel/passport
php artisan migrate
php artisan passport:install

La comanda passport:install crea les claus de xifratge que Passport utilitza per generar tokens d'accés segurs. També crea dos clients OAuth: un "personal access" client i un "password grant" client. Les claus es guarden a storage/ i no s'han de pujar al control de versions.

Afegeix el trait HasApiTokens de Passport al model User:

use Laravel\Passport\HasApiTokens;
 
class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;
}

Configura el guard api perquè utilitzi el driver de Passport al fitxer config/auth.php:

'guards' => [
    'web' => [
        'driver' => 'session',
        'provider' => 'users',
    ],
    'api' => [
        'driver' => 'passport',
        'provider' => 'users',
    ],
],

Clients OAuth#

En el protocol OAuth2, un "client" és una aplicació que vol accedir als recursos d'un usuari. Cada client té un ID i un secret que l'identifiquen. Pots gestionar clients amb comandes Artisan o a través de l'API JSON que Passport exposa.

Crear clients#

# Client per al flux Authorization Code (aplicacions web de tercers)
php artisan passport:client
 
# Client per al flux Password Grant (apps de primera part)
php artisan passport:client --password
 
# Client per a credencials del client (machine-to-machine)
php artisan passport:client --client

La comanda interactiva et demanarà el nom del client i la URL de redirecció (callback). Per a aplicacions web, la URL de redirecció és on l'usuari serà enviat després d'autoritzar l'accés.

Gestió via API#

Passport exposa endpoints JSON per gestionar clients des del frontend:

GET    /oauth/clients          - Llistar els clients de l'usuari
POST   /oauth/clients          - Crear un client nou
PUT    /oauth/clients/{id}     - Actualitzar un client
DELETE /oauth/clients/{id}     - Eliminar un client

Aquests endpoints estan protegits amb el middleware auth, així que l'usuari ha d'estar autenticat per gestionar els seus clients.

Grant Types#

OAuth2 defineix diversos "grant types" (fluxos d'autorització) per a diferents escenaris. Passport implementa els principals.

Authorization Code Grant#

És el flux més comú i segur per a aplicacions web de tercers. L'aplicació redirigeix l'usuari al teu servidor d'autorització, l'usuari aprova l'accés i el servidor retorna un codi d'autorització que l'aplicació bescanvia per un token d'accés.

L'aplicació de tercers redirigeix l'usuari a la pàgina d'autorització:

GET /oauth/authorize?client_id=1
    &redirect_uri=https://app-tercera.com/callback
    &response_type=code
    &scope=articles-read
    &state=random-string

Si l'usuari aprova, Laravel redirigeix al redirect_uri amb un codi d'autorització:

https://app-tercera.com/callback?code=abc123&state=random-string

L'aplicació bescanvia el codi per un token d'accés fent una petició al servidor:

// L'aplicació de tercers fa aquesta petició
$response = Http::asForm()->post('https://teva-app.com/oauth/token', [
    'grant_type' => 'authorization_code',
    'client_id' => 'client-id',
    'client_secret' => 'client-secret',
    'redirect_uri' => 'https://app-tercera.com/callback',
    'code' => $request->code,
]);
 
$accessToken = $response->json()['access_token'];
$refreshToken = $response->json()['refresh_token'];

Authorization Code Grant amb PKCE#

PKCE (Proof Key for Code Exchange) és una extensió de seguretat per a clients que no poden guardar un secret de manera segura, com les SPAs o les aplicacions mòbils. En lloc del client secret, utilitza un "code verifier" i un "code challenge":

php artisan passport:client --public
// L'aplicació genera un code_verifier i un code_challenge
$codeVerifier = Str::random(128);
$codeChallenge = strtr(rtrim(
    base64_encode(hash('sha256', $codeVerifier, true)),
    '='
), '+/', '-_');

La petició d'autorització inclou el code_challenge:

GET /oauth/authorize?client_id=1
    &redirect_uri=https://app.com/callback
    &response_type=code
    &scope=
    &state=random-string
    &code_challenge=abc123...
    &code_challenge_method=S256

I el bescanvi del codi inclou el code_verifier en lloc del client_secret:

$response = Http::asForm()->post('https://teva-app.com/oauth/token', [
    'grant_type' => 'authorization_code',
    'client_id' => 'client-id',
    'redirect_uri' => 'https://app.com/callback',
    'code_verifier' => $codeVerifier,
    'code' => $request->code,
]);

Password Grant#

El Password Grant permet que una aplicació de primera part (la teva pròpia app mòbil, per exemple) enviï directament les credencials de l'usuari per obtenir un token. No s'hauria d'utilitzar amb aplicacions de tercers perquè implica compartir les credencials:

$response = Http::asForm()->post('https://teva-app.com/oauth/token', [
    'grant_type' => 'password',
    'client_id' => 'password-client-id',
    'client_secret' => 'password-client-secret',
    'username' => 'user@example.com',
    'password' => 'secret',
    'scope' => 'articles-read articles-write',
]);

Laravel recomana utilitzar Sanctum en lloc del Password Grant per a aplicacions de primera part. El Password Grant és un flux llegat d'OAuth2 que es considera menys segur.

Client Credentials Grant#

El Client Credentials Grant s'utilitza per a comunicació màquina a màquina, on no hi ha cap usuari involucrat. Per exemple, un servei de backend que necessita accedir a l'API d'un altre servei:

$response = Http::asForm()->post('https://teva-app.com/oauth/token', [
    'grant_type' => 'client_credentials',
    'client_id' => 'client-id',
    'client_secret' => 'client-secret',
    'scope' => 'articles-read',
]);

Per protegir rutes amb aquest grant type, utilitza el middleware client:

Route::middleware('client:articles-read')->group(function () {
    Route::get('/api/articles', [ArticleController::class, 'index']);
});

Refresh Tokens#

Els tokens d'accés tenen una durada limitada. Quan un token expira, el refresh token permet obtenir-ne un de nou sense que l'usuari hagi de tornar a autoritzar:

$response = Http::asForm()->post('https://teva-app.com/oauth/token', [
    'grant_type' => 'refresh_token',
    'refresh_token' => 'el-refresh-token',
    'client_id' => 'client-id',
    'client_secret' => 'client-secret',
    'scope' => '',
]);

Scopes#

Els scopes limiten els permisos que una aplicació pot sol·licitar. Quan un usuari autoritza una aplicació, veu exactament quins permisos demana i pot decidir si els concedeix.

Definir scopes#

Registra els scopes disponibles al AppServiceProvider:

use Laravel\Passport\Passport;
 
public function boot(): void
{
    Passport::tokensCan([
        'articles-read' => 'Llegir articles',
        'articles-write' => 'Crear i editar articles',
        'articles-delete' => 'Eliminar articles',
        'profile-read' => 'Veure el teu perfil',
        'profile-write' => 'Modificar el teu perfil',
    ]);
 
    // Scopes per defecte si no se n'especifiquen
    Passport::defaultScopes([
        'articles-read',
        'profile-read',
    ]);
}

Comprovar scopes a les rutes#

Passport inclou dos middlewares per verificar scopes a les rutes:

// L'usuari ha de tenir TOTS els scopes indicats
Route::get('/api/articles', [ArticleController::class, 'index'])
    ->middleware(['auth:api', 'scopes:articles-read,profile-read']);
 
// L'usuari ha de tenir ALMENYS UN dels scopes
Route::get('/api/articles', [ArticleController::class, 'index'])
    ->middleware(['auth:api', 'scope:articles-read,articles-write']);

La diferència és subtil: scopes (plural) requereix tots els scopes, mentre que scope (singular) requereix almenys un.

Comprovar scopes al codi#

// Al controlador
if ($request->user()->tokenCan('articles-write')) {
    // El token té el scope articles-write
}
 
if ($request->user()->tokenCant('articles-delete')) {
    // El token NO té el scope articles-delete
}

Protegir rutes#

Per protegir rutes d'API amb Passport, utilitza el middleware auth:api:

Route::middleware('auth:api')->group(function () {
    Route::get('/user', function (Request $request) {
        return $request->user();
    });
 
    Route::apiResource('articles', ArticleController::class);
});

Configuració de tokens#

Durada dels tokens#

Pots configurar la durada dels tokens d'accés i dels refresh tokens:

// app/Providers/AppServiceProvider.php
use Laravel\Passport\Passport;
 
public function boot(): void
{
    Passport::tokensExpireIn(now()->addDays(15));
    Passport::refreshTokensExpireIn(now()->addDays(30));
    Passport::personalAccessTokensExpireIn(now()->addMonths(6));
}

Personal Access Tokens#

Els personal access tokens permeten als usuaris crear tokens per a ells mateixos sense necessitat del flux d'autorització OAuth2. Són útils per a integracions personals, scripts o eines de desenvolupament:

// L'usuari crea un token per a ell mateix
$token = $user->createToken('Token personal', ['articles-read']);
$accessToken = $token->accessToken;

Passport exposa endpoints per gestionar-los:

GET    /oauth/personal-access-tokens          - Llistar tokens
POST   /oauth/personal-access-tokens          - Crear un token
DELETE /oauth/personal-access-tokens/{id}     - Revocar un token

Revocar tokens#

use Laravel\Passport\Token;
 
// Revocar un token específic
$token = Token::find($tokenId);
$token->revoke();
 
// Revocar el refresh token associat
$refreshToken = $token->refreshToken;
$refreshToken->revoke();
 
// Revocar tots els tokens d'un usuari
$user->tokens->each->revoke();

Purgar tokens caducats#

Amb el temps, la taula de tokens pot créixer molt. Passport inclou una comanda per netejar tokens caducats i revocats:

# Purgar tokens revocats i caducats
php artisan passport:purge
 
# Purgar només tokens caducats fa més de 6 hores
php artisan passport:purge --hours=6
 
# Purgar només tokens revocats
php artisan passport:purge --revoked
 
# Purgar només tokens caducats
php artisan passport:purge --expired

Pots programar aquesta comanda perquè s'executi automàticament:

// routes/console.php
Schedule::command('passport:purge')->daily();

Per a la majoria de projectes, Sanctum és suficient i molt més senzill. Utilitza Passport només quan necessitis ser un proveïdor OAuth2 complet, amb fluxos d'autorització, scopes i clients de tercers.