Laravel Passport
認証ライブラリ
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'),
];