Laravel Passport

認証ライブラリPHPLaravelOAuth 2.0認証サーバーAPIトークン認可

認証ライブラリ

Laravel Passport

概要

Laravel Passportは、LaravelアプリケーションにフルOAuth2サーバー実装を数分で提供するライブラリです。2025年現在、Laravel 12.x対応版として、OAuth 2.0およびOAuth 2.1標準に完全準拠した認証サーバー機能を提供しています。Andy MillingtonとSimon HampによってメンテナンスされているLeague OAuth2サーバーをベースに構築されており、Laravel Frameworkとの深い統合により、設定の簡素化と強力な機能を実現します。認可コードグラント、クライアントクレデンシャルグラント、パスワードグラント、暗黙的グラント、デバイス認証グラント、PKCE対応など、現代のAPIアーキテクチャに必要な全てのOAuth2フローをサポートし、エンタープライズレベルのAPI認証・認可機能を提供します。

詳細

Laravel Passport 12.x系は、Laravelの豊富な機能を活用してOAuth2サーバーの複雑な実装を抽象化します。Eloquent ORMによるクライアント・トークン管理、Laravelの認証システムとの自動統合、APIリソースの包括的な保護機能を提供します。PKCE (Proof Key for Code Exchange) による認可コード攻撃防止、スコープベースの細かい認可制御、トークンの自動期限管理、リフレッシュトークンローテーション等、セキュリティのベストプラクティスを内蔵しています。SPA (Single Page Application) 対応のための暗号化JWTクッキー機能、マルチガード対応、カスタムユーザープロバイダー対応により、あらゆるアプリケーション要件に対応可能です。

主な特徴

  • フルOAuth2サーバー: 認可コード、クライアントクレデンシャル、パスワード、暗黙的、デバイス認証グラント対応
  • Laravel統合: Eloquent ORM、認証システム、ミドルウェア、Artisanコマンドとの完全統合
  • PKCE対応: セキュアなモバイルアプリ・SPAでの認証フロー
  • スコープベース認可: 細かな権限制御とAPI保護
  • SPA統合: 暗号化JWTクッキーによるセッションレス認証
  • エンタープライズ対応: マルチガード、カスタムプロバイダー、高度なセキュリティ設定

メリット・デメリット

メリット

  • Laravel生態系との深い統合により、設定と実装が簡潔で効率的
  • OAuth 2.0/2.1標準準拠で業界標準のセキュリティ実装を提供
  • 豊富なグラントタイプで多様なクライアントアプリケーションに対応
  • PKCEサポートにより追加実装なしで高セキュリティを実現
  • 充実したArtisanコマンドで運用管理が容易
  • Laravel公式ライブラリとして長期サポートと安定性を保証

デメリット

  • Laravel Framework専用で、他のPHPフレームワークでは使用不可
  • OAuth2の複雑性により、学習コストと理解のためのスキルが必要
  • 大規模な機能により、シンプルなAPI認証には過剰な場合がある
  • データベースマイグレーションとスキーマ変更が必要
  • パフォーマンス考慮事項として、トークン管理による若干のオーバーヘッド
  • 設定の複雑性により、初期設定に時間を要する場合がある

参考ページ

書き方の例

基本的なインストールとセットアップ

# Laravel Passportのインストール
composer require laravel/passport

# Passport APIインストール(データベースマイグレーション + 暗号化キー生成)
php artisan install:api --passport

# 本番環境での暗号化キー生成(デプロイ時)
php artisan passport:keys
<?php
// app/Models/User.php - ユーザーモデル設定
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',
        ];
    }

    /**
     * パスワードグラント用のユーザー名フィールドカスタマイズ
     */
    public function findForPassport(string $username): User
    {
        return $this->where('email', $username)->first();
    }

    /**
     * パスワードグラント用のパスワード検証カスタマイズ
     */
    public function validateForPassportPasswordGrant(string $password): bool
    {
        return Hash::check($password, $this->password);
    }
}
// config/auth.php - API認証ガード設定
'guards' => [
    'web' => [
        'driver' => 'session',
        'provider' => 'users',
    ],

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

    // マルチガード対応例
    'api-admin' => [
        'driver' => 'passport',
        'provider' => 'admins',
    ],
],

OAuth2クライアント作成とスコープ定義

# 認可コードグラントクライアント作成
php artisan passport:client

# パスワードグラントクライアント作成
php artisan passport:client --password

# クライアントクレデンシャルグラントクライアント作成
php artisan passport:client --client

# パブリック(PKCE)クライアント作成
php artisan passport:client --public
// app/Providers/AppServiceProvider.php - Passport設定
<?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
    {
        // カスタムスコープ定義
        Passport::tokensCan([
            'user:read' => 'ユーザー情報の取得',
            'user:write' => 'ユーザー情報の更新',
            'orders:read' => '注文情報の参照',
            'orders:create' => '注文の作成',
            'orders:manage' => '注文の管理',
            'admin:read' => '管理者権限での閲覧',
            'admin:write' => '管理者権限での編集',
        ]);

        // トークン期限設定
        Passport::tokensExpireIn(CarbonInterval::days(15));
        Passport::refreshTokensExpireIn(CarbonInterval::days(30));
        Passport::personalAccessTokensExpireIn(CarbonInterval::months(6));

        // パスワードグラント有効化
        Passport::enablePasswordGrant();

        // SPA用クッキー名カスタマイズ
        Passport::cookie('api_token');

        // 暗号化キーのカスタムパス
        // Passport::loadKeysFrom(__DIR__.'/../secrets/oauth');
    }
}

APIルート保護とスコープベース認可

// routes/api.php - API ルート定義
<?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;

// パブリックエンドポイント
Route::get('/health', function () {
    return ['status' => 'ok', 'timestamp' => now()];
});

// 認証必須エンドポイント
Route::middleware('auth:api')->group(function () {
    Route::get('/user', function (Request $request) {
        return $request->user();
    });

    // スコープベース認可
    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']);
    });

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

// クライアントクレデンシャル用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 コントローラーとトークンスコープチェック

// 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();
        
        // プログラマティックなスコープチェック
        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 認証フローとトークン取得

// 認可コードフロー - リダイレクト処理
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);
});

// 認可コードフロー - コールバック処理
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();

    // トークンを保存・使用
    session(['access_token' => $tokenData['access_token']]);
    session(['refresh_token' => $tokenData['refresh_token']]);

    return redirect('/dashboard')->with('success', 'OAuth認証が完了しました');
});

クライアントクレデンシャルとパスワードグラント

// app/Services/OAuthService.php - OAuth サービスクラス
<?php

namespace App\Services;

use Illuminate\Support\Facades\Http;

class OAuthService
{
    /**
     * クライアントクレデンシャルグラントでトークン取得
     */
    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();
    }

    /**
     * パスワードグラントでトークン取得
     */
    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();
    }

    /**
     * リフレッシュトークンでアクセストークン更新
     */
    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();
    }

    /**
     * トークンの取り消し
     */
    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();
    }
}

テスト環境でのPassport使用

// tests/Feature/ApiTest.php - API テスト
<?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'] // write スコープなし
        );

        $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統合とJWTクッキー認証

// bootstrap/app.php - SPA認証ミドルウェア設定
<?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) {
        // SPA用のJWTクッキーミドルウェア追加
        $middleware->web(append: [
            CreateFreshApiToken::class,
        ]);
    })
    ->withExceptions(function (Exceptions $exceptions) {
        //
    })->create();
// JavaScript でのSPA API リクエスト例
// Laravel自動生成のJWTクッキーを利用
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トークンの設定(必要に応じて)
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';

運用・メンテナンス機能

# 期限切れ・取り消されたトークンの削除
php artisan passport:purge

# 6時間以上経過した期限切れトークンのみ削除
php artisan passport:purge --hours=6

# 取り消されたトークンのみ削除
php artisan passport:purge --revoked

# 期限切れトークンのみ削除
php artisan passport:purge --expired

# 定期的なクリーンアップスケジュール設定
# routes/console.php
use Illuminate\Support\Facades\Schedule;

Schedule::command('passport:purge')->hourly();
// config/oauth.php - 設定ファイル例
<?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'),
];