Laravel Passport

authentication libraryPHPLaravelOAuth 2.0authorization serverAPI tokensauthorization

Authentication Library

Laravel Passport

Overview

Laravel Passport provides a full OAuth2 server implementation for your Laravel application in a matter of minutes. As of 2025, with Laravel 12.x compatibility, it offers comprehensive OAuth 2.0 and OAuth 2.1 standard-compliant authorization server functionality. Built on top of the League OAuth2 server maintained by Andy Millington and Simon Hamp, it achieves simplified configuration and powerful capabilities through deep integration with the Laravel Framework. It supports all OAuth2 flows necessary for modern API architectures, including authorization code grant, client credentials grant, password grant, implicit grant, device authorization grant, and PKCE support, providing enterprise-level API authentication and authorization features.

Details

Laravel Passport 12.x series abstracts the complex implementation of OAuth2 servers by leveraging Laravel's rich features. It provides client and token management via Eloquent ORM, automatic integration with Laravel's authentication system, and comprehensive API resource protection capabilities. It incorporates security best practices such as PKCE (Proof Key for Code Exchange) for authorization code attack prevention, scope-based fine-grained authorization control, automatic token expiration management, and refresh token rotation. With encrypted JWT cookie functionality for SPA (Single Page Application) support, multi-guard compatibility, and custom user provider support, it can handle any application requirements.

Key Features

  • Full OAuth2 Server: Authorization code, client credentials, password, implicit, device authorization grant support
  • Laravel Integration: Complete integration with Eloquent ORM, authentication system, middleware, and Artisan commands
  • PKCE Support: Secure authentication flows for mobile apps and SPAs
  • Scope-based Authorization: Fine-grained permission control and API protection
  • SPA Integration: Sessionless authentication via encrypted JWT cookies
  • Enterprise Ready: Multi-guard, custom providers, advanced security configurations

Advantages and Disadvantages

Advantages

  • Deep integration with Laravel ecosystem enables simple and efficient configuration and implementation
  • Industry-standard security implementation with OAuth 2.0/2.1 standard compliance
  • Rich grant types support diverse client applications
  • PKCE support achieves high security without additional implementation
  • Comprehensive Artisan commands make operational management easy
  • Long-term support and stability guaranteed as an official Laravel library

Disadvantages

  • Laravel Framework exclusive, cannot be used with other PHP frameworks
  • OAuth2 complexity requires learning costs and skills for understanding
  • Extensive functionality may be excessive for simple API authentication
  • Requires database migrations and schema changes
  • Performance considerations with slight overhead from token management
  • Configuration complexity may require time for initial setup

Reference Pages

Usage Examples

Basic Installation and Setup

# Install Laravel Passport
composer require laravel/passport

# Install Passport API (database migrations + encryption key generation)
php artisan install:api --passport

# Generate encryption keys for production (on deployment)
php artisan passport:keys
<?php
// app/Models/User.php - User model configuration
namespace App\Models;

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

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

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

    protected $hidden = [
        'password',
        'remember_token',
    ];

    protected function casts(): array
    {
        return [
            'email_verified_at' => 'datetime',
            'password' => 'hashed',
        ];
    }

    /**
     * Customize username field for password grant
     */
    public function findForPassport(string $username): User
    {
        return $this->where('email', $username)->first();
    }

    /**
     * Customize password validation for password grant
     */
    public function validateForPassportPasswordGrant(string $password): bool
    {
        return Hash::check($password, $this->password);
    }
}
// config/auth.php - API authentication guard configuration
'guards' => [
    'web' => [
        'driver' => 'session',
        'provider' => 'users',
    ],

    'api' => [
        'driver' => 'passport',
        'provider' => 'users',
    ],

    // Multi-guard support example
    'api-admin' => [
        'driver' => 'passport',
        'provider' => 'admins',
    ],
],

OAuth2 Client Creation and Scope Definition

# Create authorization code grant client
php artisan passport:client

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

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

# Create public (PKCE) client
php artisan passport:client --public
// app/Providers/AppServiceProvider.php - Passport configuration
<?php

namespace App\Providers;

use Carbon\CarbonInterval;
use Illuminate\Support\ServiceProvider;
use Laravel\Passport\Passport;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        // Define custom scopes
        Passport::tokensCan([
            'user:read' => 'Retrieve user information',
            'user:write' => 'Update user information',
            'orders:read' => 'View order information',
            'orders:create' => 'Create orders',
            'orders:manage' => 'Manage orders',
            'admin:read' => 'Admin read access',
            'admin:write' => 'Admin write access',
        ]);

        // Configure token lifetimes
        Passport::tokensExpireIn(CarbonInterval::days(15));
        Passport::refreshTokensExpireIn(CarbonInterval::days(30));
        Passport::personalAccessTokensExpireIn(CarbonInterval::months(6));

        // Enable password grant
        Passport::enablePasswordGrant();

        // Customize SPA cookie name
        Passport::cookie('api_token');

        // Custom path for encryption keys
        // Passport::loadKeysFrom(__DIR__.'/../secrets/oauth');
    }
}

API Route Protection and Scope-based Authorization

// routes/api.php - API route definitions
<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\UserController;
use App\Http\Controllers\Api\OrderController;
use App\Http\Controllers\Api\AdminController;

// Public endpoints
Route::get('/health', function () {
    return ['status' => 'ok', 'timestamp' => now()];
});

// Authentication required endpoints
Route::middleware('auth:api')->group(function () {
    Route::get('/user', function (Request $request) {
        return $request->user();
    });

    // Scope-based authorization
    Route::middleware('scope:user:read')->group(function () {
        Route::get('/profile', [UserController::class, 'profile']);
        Route::get('/settings', [UserController::class, 'settings']);
    });

    Route::middleware('scope:user:write')->group(function () {
        Route::put('/profile', [UserController::class, 'updateProfile']);
    });

    Route::middleware('scope:orders:read')->group(function () {
        Route::get('/orders', [OrderController::class, 'index']);
        Route::get('/orders/{order}', [OrderController::class, 'show']);
    });

    Route::middleware('scope:orders:create')->group(function () {
        Route::post('/orders', [OrderController::class, 'store']);
    });

    // Admin API
    Route::middleware('scope:admin:read')->group(function () {
        Route::get('/admin/users', [AdminController::class, 'users']);
        Route::get('/admin/statistics', [AdminController::class, 'statistics']);
    });
});

// Client credentials API
Route::middleware(['auth:api', Laravel\Passport\Http\Middleware\EnsureClientIsResourceOwner::class])
    ->group(function () {
        Route::get('/servers', function () {
            return [
                'servers' => [
                    ['id' => 1, 'name' => 'Web Server 1', 'status' => 'active'],
                    ['id' => 2, 'name' => 'API Server 1', 'status' => 'active'],
                ]
            ];
        });
    });

API Controllers and Token Scope Checking

// app/Http/Controllers/Api/UserController.php
<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;

class UserController extends Controller
{
    public function profile(Request $request): JsonResponse
    {
        $user = $request->user();
        
        // Programmatic scope checking
        if (!$user->tokenCan('user:read')) {
            return response()->json(['error' => 'Insufficient scope'], 403);
        }

        return response()->json([
            'id' => $user->id,
            'name' => $user->name,
            'email' => $user->email,
            'created_at' => $user->created_at,
            'token_scopes' => $user->token()->scopes ?? [],
        ]);
    }

    public function updateProfile(Request $request): JsonResponse
    {
        $request->validate([
            'name' => 'sometimes|string|max:255',
            'email' => 'sometimes|email|unique:users,email,' . $request->user()->id,
        ]);

        $user = $request->user();
        
        if (!$user->tokenCan('user:write')) {
            return response()->json(['error' => 'Insufficient scope'], 403);
        }

        $user->update($request->only(['name', 'email']));

        return response()->json([
            'message' => 'Profile updated successfully',
            'user' => $user->fresh()
        ]);
    }

    public function settings(Request $request): JsonResponse
    {
        $user = $request->user();
        
        return response()->json([
            'token_id' => $user->token()->id,
            'token_scopes' => $user->token()->scopes,
            'client_id' => $user->token()->client_id,
            'expires_at' => $user->token()->expires_at,
            'revoked' => $user->token()->revoked,
        ]);
    }
}

OAuth2 Authentication Flow and Token Acquisition

// Authorization code flow - redirect handling
Route::get('/oauth/redirect', function (Request $request) {
    $request->session()->put('state', $state = Str::random(40));
    $request->session()->put('code_verifier', $codeVerifier = Str::random(128));

    $codeChallenge = strtr(rtrim(
        base64_encode(hash('sha256', $codeVerifier, true)), '='
    ), '+/', '-_');

    $query = http_build_query([
        'client_id' => config('oauth.client_id'),
        'redirect_uri' => config('oauth.redirect_uri'),
        'response_type' => 'code',
        'scope' => 'user:read user:write orders:read',
        'state' => $state,
        'code_challenge' => $codeChallenge,
        'code_challenge_method' => 'S256',
    ]);

    return redirect(config('oauth.authorization_url') . '?' . $query);
});

// Authorization code flow - callback handling
Route::get('/oauth/callback', function (Request $request) {
    $state = $request->session()->pull('state');
    $codeVerifier = $request->session()->pull('code_verifier');

    throw_unless(
        strlen($state) > 0 && $state === $request->state,
        InvalidArgumentException::class,
        'Invalid state parameter'
    );

    $response = Http::asForm()->post(config('oauth.token_url'), [
        'grant_type' => 'authorization_code',
        'client_id' => config('oauth.client_id'),
        'redirect_uri' => config('oauth.redirect_uri'),
        'code_verifier' => $codeVerifier,
        'code' => $request->code,
    ]);

    $tokenData = $response->json();

    // Store and use tokens
    session(['access_token' => $tokenData['access_token']]);
    session(['refresh_token' => $tokenData['refresh_token']]);

    return redirect('/dashboard')->with('success', 'OAuth authentication completed');
});

Client Credentials and Password Grant

// app/Services/OAuthService.php - OAuth service class
<?php

namespace App\Services;

use Illuminate\Support\Facades\Http;

class OAuthService
{
    /**
     * Get token with client credentials grant
     */
    public function getClientCredentialsToken(array $scopes = []): array
    {
        $response = Http::asForm()->post(config('oauth.token_url'), [
            'grant_type' => 'client_credentials',
            'client_id' => config('oauth.client_id'),
            'client_secret' => config('oauth.client_secret'),
            'scope' => implode(' ', $scopes),
        ]);

        if (!$response->successful()) {
            throw new \Exception('Failed to obtain client credentials token');
        }

        return $response->json();
    }

    /**
     * Get token with password grant
     */
    public function getPasswordGrantToken(string $email, string $password, array $scopes = []): array
    {
        $response = Http::asForm()->post(config('oauth.token_url'), [
            'grant_type' => 'password',
            'client_id' => config('oauth.password_client_id'),
            'client_secret' => config('oauth.password_client_secret'),
            'username' => $email,
            'password' => $password,
            'scope' => implode(' ', $scopes),
        ]);

        if (!$response->successful()) {
            throw new \Exception('Failed to obtain password grant token');
        }

        return $response->json();
    }

    /**
     * Refresh access token with refresh token
     */
    public function refreshToken(string $refreshToken): array
    {
        $response = Http::asForm()->post(config('oauth.token_url'), [
            'grant_type' => 'refresh_token',
            'refresh_token' => $refreshToken,
            'client_id' => config('oauth.client_id'),
            'client_secret' => config('oauth.client_secret'),
        ]);

        if (!$response->successful()) {
            throw new \Exception('Failed to refresh token');
        }

        return $response->json();
    }

    /**
     * Revoke token
     */
    public function revokeToken(string $token): bool
    {
        $response = Http::asForm()->post(config('oauth.revocation_url'), [
            'token' => $token,
            'client_id' => config('oauth.client_id'),
            'client_secret' => config('oauth.client_secret'),
        ]);

        return $response->successful();
    }
}

Using Passport in Test Environment

// tests/Feature/ApiTest.php - API tests
<?php

namespace Tests\Feature;

use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Passport\Passport;
use Tests\TestCase;

class ApiTest extends TestCase
{
    use RefreshDatabase;

    public function test_user_can_retrieve_profile_with_correct_scope(): void
    {
        $user = User::factory()->create();

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

        $response = $this->get('/api/profile');

        $response->assertStatus(200)
                ->assertJsonStructure([
                    'id',
                    'name', 
                    'email',
                    'created_at',
                    'token_scopes'
                ]);
    }

    public function test_user_cannot_update_profile_without_write_scope(): void
    {
        $user = User::factory()->create();

        Passport::actingAs(
            $user,
            ['user:read'] // No write scope
        );

        $response = $this->put('/api/profile', [
            'name' => 'Updated Name'
        ]);

        $response->assertStatus(403);
    }

    public function test_client_can_access_server_endpoints(): void
    {
        $client = \Laravel\Passport\Client::factory()->create();

        Passport::actingAsClient(
            $client,
            ['servers:read']
        );

        $response = $this->get('/api/servers');

        $response->assertStatus(200)
                ->assertJsonStructure([
                    'servers' => [
                        '*' => ['id', 'name', 'status']
                    ]
                ]);
    }
}

SPA Integration and JWT Cookie Authentication

// bootstrap/app.php - SPA authentication middleware configuration
<?php

use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Laravel\Passport\Http\Middleware\CreateFreshApiToken;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        api: __DIR__.'/../routes/api.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware) {
        // Add JWT cookie middleware for SPA
        $middleware->web(append: [
            CreateFreshApiToken::class,
        ]);
    })
    ->withExceptions(function (Exceptions $exceptions) {
        //
    })->create();
// JavaScript SPA API request examples
// Using Laravel auto-generated JWT cookies
async function fetchUserProfile() {
    try {
        const response = await axios.get('/api/user');
        console.log('User profile:', response.data);
        return response.data;
    } catch (error) {
        console.error('Failed to fetch user profile:', error);
        throw error;
    }
}

async function updateUserProfile(profileData) {
    try {
        const response = await axios.put('/api/profile', profileData);
        console.log('Profile updated:', response.data);
        return response.data;
    } catch (error) {
        console.error('Failed to update profile:', error);
        throw error;
    }
}

// CSRF token configuration (if needed)
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';

Operations and Maintenance Features

# Remove revoked and expired tokens
php artisan passport:purge

# Remove only expired tokens older than 6 hours
php artisan passport:purge --hours=6

# Remove only revoked tokens
php artisan passport:purge --revoked

# Remove only expired tokens
php artisan passport:purge --expired

# Schedule regular cleanup
# routes/console.php
use Illuminate\Support\Facades\Schedule;

Schedule::command('passport:purge')->hourly();
// config/oauth.php - Configuration file example
<?php

return [
    'authorization_url' => env('OAUTH_AUTHORIZATION_URL', 'https://your-app.com/oauth/authorize'),
    'token_url' => env('OAUTH_TOKEN_URL', 'https://your-app.com/oauth/token'),
    'revocation_url' => env('OAUTH_REVOCATION_URL', 'https://your-app.com/oauth/revoke'),
    
    'client_id' => env('OAUTH_CLIENT_ID'),
    'client_secret' => env('OAUTH_CLIENT_SECRET'),
    'redirect_uri' => env('OAUTH_REDIRECT_URI', 'https://your-app.com/oauth/callback'),
    
    'password_client_id' => env('OAUTH_PASSWORD_CLIENT_ID'),
    'password_client_secret' => env('OAUTH_PASSWORD_CLIENT_SECRET'),
];