laravel-passport

Auto-invoked skill

Implement full OAuth2 server with all grant types

Trigger Keywords

This skill automatically activates when Claude detects these keywords:

passport oauth2 server authorization code client credentials access token refresh token oauth provider

Overview

The laravel-passport skill provides expertise for building a complete OAuth2 authorization server. It covers all OAuth2 grant types, token scopes, client management, and third-party API access.

What This Skill Provides

  • OAuth2 Grant Types - Authorization Code, Client Credentials, PKCE
  • Token Management - Access tokens, refresh tokens, revocation
  • Client Management - Create and manage OAuth clients
  • Token Scopes - Fine-grained permission control
  • Personal Access Tokens - User-generated API tokens
  • Third-Party Access - Allow external apps to access your API

When to Use

Use Passport when:

  • Building an OAuth2 authorization server
  • Third-party applications need to access your API
  • Need all OAuth2 grant types
  • Require refresh tokens with rotation

Use Sanctum instead when:

  • Building first-party SPAs or mobile apps
  • Simple token authentication is sufficient
  • Don't need OAuth2 complexity

Quick Start

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

User Model Setup

<?php

use Laravel\Passport\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens;
}

Configure Auth Guard

<?php

// config/auth.php
return [
    'guards' => [
        'api' => [
            'driver' => 'passport', // Changed from 'token'
            'provider' => 'users',
        ],
    ],
];

Authorization Code Grant

Most secure option for third-party web applications.

Create Client

php artisan passport:client

Authorization Request

GET /oauth/authorize?client_id=1
    &redirect_uri=https://client-app.com/callback
    &response_type=code
    &scope=read-posts write-posts
    &state=random_state_string

Exchange Code for Token

POST /oauth/token
Content-Type: application/json

{
    "grant_type": "authorization_code",
    "client_id": "1",
    "client_secret": "abc123...",
    "redirect_uri": "https://client-app.com/callback",
    "code": "def456..."
}

Client Credentials Grant

For machine-to-machine communication without user context.

# Create machine client
php artisan passport:client --client
POST /oauth/token

{
    "grant_type": "client_credentials",
    "client_id": "2",
    "client_secret": "xyz789...",
    "scope": "read-data"
}
// Protect routes with client middleware
Route::middleware(['client'])->group(function () {
    Route::get('/api/stats', [StatsController::class, 'index']);
});

Authorization Code with PKCE

Recommended for SPAs and mobile apps (no client secret needed).

// Generate code_verifier and code_challenge
const codeVerifier = generateRandomString(128);
const codeChallenge = await sha256(codeVerifier);

// Authorization request with PKCE
GET /oauth/authorize?client_id=5
    &redirect_uri=https://app.com/callback
    &response_type=code
    &scope=read-posts
    &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
    &code_challenge_method=S256
POST /oauth/token

{
    "grant_type": "authorization_code",
    "client_id": "5",
    "redirect_uri": "https://app.com/callback",
    "code": "def456...",
    "code_verifier": "original_verifier"
}

Token Scopes

<?php

// app/Providers/AuthServiceProvider.php
use Laravel\Passport\Passport;

public function boot(): void
{
    Passport::tokensCan([
        'read-posts' => 'Read posts',
        'write-posts' => 'Create and edit posts',
        'delete-posts' => 'Delete posts',
        'admin' => 'Full administrative access',
    ]);

    Passport::setDefaultScope(['read-posts']);
}

Check Scopes in Controllers

<?php

public function store(Request $request)
{
    if (! $request->user()->tokenCan('write-posts')) {
        return response()->json(['error' => 'Insufficient permissions'], 403);
    }

    return Post::create($request->validated());
}

Scope Middleware

// Require any of these scopes
Route::middleware(['auth:api', 'scopes:write-posts,admin'])->group(function () {
    Route::post('/posts', [PostController::class, 'store']);
});

// Require all scopes
Route::middleware(['auth:api', 'scope:write-posts,admin'])->group(function () {
    Route::delete('/posts/{post}', [PostController::class, 'destroy']);
});

Personal Access Tokens

<?php

// Create token via code
$token = $user->createToken(
    'My API Token',
    ['read-posts', 'write-posts']
);

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

Token Lifetimes

<?php

// app/Providers/AuthServiceProvider.php
public function boot(): void
{
    // Access tokens expire in 15 days
    Passport::tokensExpireIn(now()->addDays(15));

    // Refresh tokens expire in 30 days
    Passport::refreshTokensExpireIn(now()->addDays(30));

    // Personal access tokens expire in 6 months
    Passport::personalAccessTokensExpireIn(now()->addMonths(6));

    // Prune revoked tokens
    Passport::pruneRevokedTokens();
}

Revoking Tokens

// Revoke current access token
$request->user()->token()->revoke();

// Revoke all user tokens
$user->tokens->each->revoke();

// Revoke specific token
$token = $user->tokens()->find($tokenId);
$token->revoke();

Refresh Tokens

async function refreshAccessToken(refreshToken) {
    const response = await fetch('/oauth/token', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
            grant_type: 'refresh_token',
            refresh_token: refreshToken,
            client_id: clientId,
            client_secret: clientSecret,
        }),
    });

    const data = await response.json();
    return {
        accessToken: data.access_token,
        refreshToken: data.refresh_token,
    };
}

Testing OAuth Flows

<?php

use Laravel\Passport\Passport;

it('allows access with correct scope', function () {
    $user = User::factory()->create();

    Passport::actingAs($user, ['write-posts']);

    $response = $this->postJson('/api/posts', [
        'title' => 'Test Post',
        'body' => 'Content',
    ]);

    $response->assertCreated();
});

it('denies access without required scope', function () {
    $user = User::factory()->create();

    Passport::actingAs($user, ['read-posts']);

    $response = $this->postJson('/api/posts', [
        'title' => 'Test Post',
    ]);

    $response->assertForbidden();
});

Common Pitfalls

  • Not running passport:install - Must run after migration to generate keys
  • Wrong auth guard - Must use 'driver' => 'passport' not 'token'
  • Missing HasApiTokens trait - Use Laravel\Passport\HasApiTokens
  • Exposing client secrets - Never expose in frontend, use PKCE instead
  • No token lifetimes - Always configure expiration in production
  • Password grant for third-party - Only for first-party apps (deprecated)
  • Not validating redirect URIs - Whitelist exact URIs to prevent token theft
  • Forgetting to revoke on security events - Revoke on password change
  • Not using HTTPS in production - OAuth2 requires HTTPS
  • Not pruning revoked tokens - Schedule passport:purge daily

Best Practices

  • Use Authorization Code + PKCE for SPAs and mobile apps
  • Use Client Credentials for machine-to-machine
  • Never use Password Grant for third-party apps
  • Always use HTTPS in production
  • Set reasonable token lifetimes
  • Implement token refresh logic
  • Revoke tokens on security events (password change)
  • Whitelist exact redirect URIs per client
  • Use scopes to limit token permissions
  • Prune revoked tokens regularly
  • Rate limit token endpoints
  • Store refresh tokens securely
  • Never expose client secrets in frontend

Related Commands

# Install Passport
php artisan passport:install

# Create OAuth client
php artisan passport:client

# Create password grant client
php artisan passport:client --password

# Create client credentials client
php artisan passport:client --client

# Purge revoked/expired tokens
php artisan passport:purge

# Generate encryption keys
php artisan passport:keys

Related Skills