League OAuth2 Server
認証ライブラリ
League OAuth2 Server
概要
League OAuth2 Serverは、PHPでOAuth 2.0認証サーバーを構築するための本格的なライブラリで、RFC 6749準拠のセキュアでスケーラブルな認証基盤を提供します。
詳細
League OAuth2 Serverは、The PHP Leagueによって開発されたOAuth 2.0認証サーバー実装のためのPHPライブラリです。RFC 6749、RFC 7636(PKCE)、RFC 7662(Token Introspection)などの主要なOAuth 2.0関連仕様に完全準拠しています。Authorization Code Grant、Client Credentials Grant、Refresh Token Grant、Implicit Grant(非推奨)、Password Grant(非推奨)、Device Authorization Grant(RFC 8628)の各フローを完全サポートしています。JWT(JSON Web Token)によるアクセストークン発行、スコープベースの認可制御、マルチテナント対応、カスタムGrantタイプの実装が可能です。セキュリティ面では、PKCE(Proof Key for Code Exchange)、トークンの暗号化、CSRF攻撃対策、レート制限などの最新のセキュリティ標準を実装しています。データベースへの保存はRepository パターンで抽象化されており、MySQL、PostgreSQL、MongoDBなど様々なデータストアに対応可能です。企業レベルのアプリケーションに必要な機能が充実しており、マイクロサービスアーキテクチャでのAPI認証基盤としても広く利用されています。
メリット・デメリット
メリット
- RFC完全準拠: OAuth 2.0およびOpenID Connect仕様に完全準拠
- 企業グレード: 本格的な認証サーバー構築が可能
- 高セキュリティ: PKCE、JWT、トークン暗号化など最新セキュリティ対応
- 柔軟性: カスタムGrantタイプやRepository実装が可能
- スケーラブル: 大規模システムでの運用に対応
- マルチテナント: 複数のクライアントアプリケーション管理
- 豊富な機能: スコープ管理、トークン検証、リフレッシュなど
デメリット
- 複雑性: OAuth 2.0の仕様理解と適切な実装が必要
- 設定コスト: 本格的なセットアップには時間と専門知識が必要
- PHP専用: 他の言語環境では利用不可
- 依存関係: 多くのコンポーネントに依存
- 学習コスト: OAuth 2.0とOpenID Connect仕様の深い理解が必要
主要リンク
- League OAuth2 Server公式ページ
- League OAuth2 Server公式ドキュメント
- The PHP League
- RFC 6749 - OAuth 2.0仕様
- RFC 7636 - PKCE仕様
- OpenID Connect仕様
書き方の例
Hello World(基本的な認証サーバー)
<?php
require_once 'vendor/autoload.php';
use League\OAuth2\Server\AuthorizationServer;
use League\OAuth2\Server\ResourceServer;
use League\OAuth2\Server\Grant\AuthorizationCodeGrant;
use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface;
use League\OAuth2\Server\Repositories\ClientRepositoryInterface;
use League\OAuth2\Server\Repositories\ScopeRepositoryInterface;
use League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface;
use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
// 基本的な認証サーバーセットアップ
class BasicAuthServer
{
private $authorizationServer;
private $resourceServer;
public function __construct()
{
// リポジトリの初期化(実際の実装では適切なRepositoryクラスを使用)
$clientRepository = new ClientRepository();
$scopeRepository = new ScopeRepository();
$accessTokenRepository = new AccessTokenRepository();
$authCodeRepository = new AuthCodeRepository();
$refreshTokenRepository = new RefreshTokenRepository();
// 認証サーバーの設定
$this->authorizationServer = new AuthorizationServer(
$clientRepository,
$accessTokenRepository,
$scopeRepository,
'file://' . __DIR__ . '/private.key', // プライベートキー
'your-encryption-key' // 暗号化キー
);
// Authorization Code Grantの有効化
$authCodeGrant = new AuthorizationCodeGrant(
$authCodeRepository,
$refreshTokenRepository,
new \DateInterval('PT10M') // 認証コード有効期限: 10分
);
$authCodeGrant->setRefreshTokenTTL(new \DateInterval('P1M')); // リフレッシュトークン: 1ヶ月
$this->authorizationServer->enableGrantType(
$authCodeGrant,
new \DateInterval('PT1H') // アクセストークン有効期限: 1時間
);
// リソースサーバーの設定
$this->resourceServer = new ResourceServer(
$accessTokenRepository,
'file://' . __DIR__ . '/public.key' // 公開キー
);
}
public function handleAuthorizationRequest()
{
$request = \Laminas\Diactoros\ServerRequestFactory::fromGlobals();
try {
// 認証リクエストの検証
$authRequest = $this->authorizationServer->validateAuthorizationRequest($request);
// ユーザー認証(実際の実装では適切な認証処理を実装)
$user = $this->authenticateUser($request);
if (!$user) {
throw new \Exception('User authentication failed');
}
$authRequest->setUser($user);
$authRequest->setAuthorizationApproved(true);
// 認証レスポンスの生成
return $this->authorizationServer->completeAuthorizationRequest(
$authRequest,
new \Laminas\Diactoros\Response()
);
} catch (\Exception $e) {
return new \Laminas\Diactoros\Response\JsonResponse([
'error' => 'server_error',
'error_description' => $e->getMessage()
], 500);
}
}
public function handleTokenRequest()
{
$request = \Laminas\Diactoros\ServerRequestFactory::fromGlobals();
try {
return $this->authorizationServer->respondToAccessTokenRequest($request, new \Laminas\Diactoros\Response());
} catch (\Exception $e) {
return new \Laminas\Diactoros\Response\JsonResponse([
'error' => 'invalid_request',
'error_description' => $e->getMessage()
], 400);
}
}
public function validateResourceRequest()
{
$request = \Laminas\Diactoros\ServerRequestFactory::fromGlobals();
try {
return $this->resourceServer->validateAuthenticatedRequest($request);
} catch (\Exception $e) {
return new \Laminas\Diactoros\Response\JsonResponse([
'error' => 'invalid_token',
'error_description' => $e->getMessage()
], 401);
}
}
private function authenticateUser($request)
{
// 実際の実装ではセッションやDBからユーザー情報を取得
return new \League\OAuth2\Server\Entities\UserEntity('user-123');
}
}
// 基本的な使用例
$authServer = new BasicAuthServer();
// ルーティング例
$requestUri = $_SERVER['REQUEST_URI'];
$requestMethod = $_SERVER['REQUEST_METHOD'];
if ($requestUri === '/oauth/authorize' && $requestMethod === 'GET') {
$response = $authServer->handleAuthorizationRequest();
} elseif ($requestUri === '/oauth/token' && $requestMethod === 'POST') {
$response = $authServer->handleTokenRequest();
} elseif (strpos($requestUri, '/api/') === 0) {
$validatedRequest = $authServer->validateResourceRequest();
if ($validatedRequest) {
// 保護されたリソースへのアクセス
echo json_encode(['message' => 'Protected resource accessed successfully']);
}
} else {
http_response_code(404);
echo 'Not Found';
}
?>
複数Grant Type対応の認証サーバー
<?php
require_once 'vendor/autoload.php';
use League\OAuth2\Server\AuthorizationServer;
use League\OAuth2\Server\Grant\AuthorizationCodeGrant;
use League\OAuth2\Server\Grant\ClientCredentialsGrant;
use League\OAuth2\Server\Grant\RefreshTokenGrant;
use League\OAuth2\Server\Grant\PasswordGrant;
class AdvancedAuthServer
{
private $authorizationServer;
private $repositories;
public function __construct()
{
$this->repositories = [
'client' => new ClientRepository(),
'scope' => new ScopeRepository(),
'accessToken' => new AccessTokenRepository(),
'authCode' => new AuthCodeRepository(),
'refreshToken' => new RefreshTokenRepository(),
'user' => new UserRepository()
];
$this->authorizationServer = new AuthorizationServer(
$this->repositories['client'],
$this->repositories['accessToken'],
$this->repositories['scope'],
'file://' . __DIR__ . '/keys/private.key',
'def00000a3f7b7bce7bb66b6b7bb7b7c2ef9ea61c7f5e5b4a2d3c7dd7e9f8a1a2'
);
$this->setupGrantTypes();
}
private function setupGrantTypes()
{
// 1. Authorization Code Grant (最も一般的)
$authCodeGrant = new AuthorizationCodeGrant(
$this->repositories['authCode'],
$this->repositories['refreshToken'],
new \DateInterval('PT10M') // 認証コード: 10分
);
$authCodeGrant->setRefreshTokenTTL(new \DateInterval('P1M')); // リフレッシュトークン: 1ヶ月
$this->authorizationServer->enableGrantType(
$authCodeGrant,
new \DateInterval('PT1H') // アクセストークン: 1時間
);
// 2. Client Credentials Grant (サービス間通信)
$this->authorizationServer->enableGrantType(
new ClientCredentialsGrant(),
new \DateInterval('PT1H')
);
// 3. Refresh Token Grant
$refreshTokenGrant = new RefreshTokenGrant($this->repositories['refreshToken']);
$refreshTokenGrant->setRefreshTokenTTL(new \DateInterval('P1M'));
$this->authorizationServer->enableGrantType(
$refreshTokenGrant,
new \DateInterval('PT1H')
);
// 4. Password Grant (レガシー用途のみ推奨)
$passwordGrant = new PasswordGrant(
$this->repositories['user'],
$this->repositories['refreshToken']
);
$passwordGrant->setRefreshTokenTTL(new \DateInterval('P1M'));
$this->authorizationServer->enableGrantType(
$passwordGrant,
new \DateInterval('PT1H')
);
}
public function handleRequest()
{
$request = \Laminas\Diactoros\ServerRequestFactory::fromGlobals();
$requestUri = $request->getUri()->getPath();
$requestMethod = $request->getMethod();
try {
switch (true) {
case ($requestUri === '/oauth/authorize' && $requestMethod === 'GET'):
return $this->handleAuthorizationRequest($request);
case ($requestUri === '/oauth/authorize' && $requestMethod === 'POST'):
return $this->handleAuthorizationApproval($request);
case ($requestUri === '/oauth/token' && $requestMethod === 'POST'):
return $this->handleTokenRequest($request);
default:
return new \Laminas\Diactoros\Response\JsonResponse([
'error' => 'unsupported_endpoint'
], 404);
}
} catch (\Exception $e) {
return new \Laminas\Diactoros\Response\JsonResponse([
'error' => 'server_error',
'error_description' => $e->getMessage()
], 500);
}
}
private function handleAuthorizationRequest($request)
{
try {
$authRequest = $this->authorizationServer->validateAuthorizationRequest($request);
// ユーザーがログインしているかチェック
session_start();
if (!isset($_SESSION['user_id'])) {
// ログインページにリダイレクト
$loginUrl = '/login?' . http_build_query([
'redirect_uri' => $request->getUri()->__toString()
]);
return new \Laminas\Diactoros\Response\RedirectResponse($loginUrl);
}
// 認証同意画面を表示
$authorizationForm = $this->generateAuthorizationForm($authRequest);
return new \Laminas\Diactoros\Response\HtmlResponse($authorizationForm);
} catch (\Exception $e) {
return new \Laminas\Diactoros\Response\JsonResponse([
'error' => 'invalid_request',
'error_description' => $e->getMessage()
], 400);
}
}
private function handleAuthorizationApproval($request)
{
session_start();
$authRequest = $this->authorizationServer->validateAuthorizationRequest($request);
// ユーザーの承認を確認
$postData = $request->getParsedBody();
$approved = isset($postData['approve']) && $postData['approve'] === 'yes';
if ($approved) {
$user = new \League\OAuth2\Server\Entities\UserEntity($_SESSION['user_id']);
$authRequest->setUser($user);
$authRequest->setAuthorizationApproved(true);
} else {
$authRequest->setAuthorizationApproved(false);
}
return $this->authorizationServer->completeAuthorizationRequest(
$authRequest,
new \Laminas\Diactoros\Response()
);
}
private function handleTokenRequest($request)
{
return $this->authorizationServer->respondToAccessTokenRequest(
$request,
new \Laminas\Diactoros\Response()
);
}
private function generateAuthorizationForm($authRequest)
{
$client = $authRequest->getClient();
$scopes = $authRequest->getScopes();
$scopeList = implode(', ', array_map(function($scope) {
return $scope->getIdentifier();
}, $scopes));
return "
<!DOCTYPE html>
<html>
<head>
<title>認証の承認</title>
<style>
body { font-family: Arial, sans-serif; margin: 50px; }
.auth-form { max-width: 400px; margin: 0 auto; padding: 20px; border: 1px solid #ddd; }
.button { padding: 10px 20px; margin: 5px; border: none; border-radius: 3px; cursor: pointer; }
.approve { background-color: #28a745; color: white; }
.deny { background-color: #dc3545; color: white; }
</style>
</head>
<body>
<div class='auth-form'>
<h2>認証の承認</h2>
<p><strong>{$client->getName()}</strong> が以下の権限でアクセスを要求しています:</p>
<p><strong>スコープ:</strong> {$scopeList}</p>
<form method='post'>
<button type='submit' name='approve' value='yes' class='button approve'>承認</button>
<button type='submit' name='approve' value='no' class='button deny'>拒否</button>
</form>
</div>
</body>
</html>";
}
}
// ルーティング例
$authServer = new AdvancedAuthServer();
echo $authServer->handleRequest();
?>
PKCE対応とセキュリティ強化
<?php
require_once 'vendor/autoload.php';
use League\OAuth2\Server\AuthorizationServer;
use League\OAuth2\Server\Grant\AuthorizationCodeGrant;
use League\OAuth2\Server\CodeChallengeVerifiers\S256Verifier;
use League\OAuth2\Server\CodeChallengeVerifiers\PlainVerifier;
class SecureAuthServer
{
private $authorizationServer;
public function __construct()
{
$clientRepository = new SecureClientRepository();
$scopeRepository = new SecureScopeRepository();
$accessTokenRepository = new SecureAccessTokenRepository();
$authCodeRepository = new SecureAuthCodeRepository();
$refreshTokenRepository = new SecureRefreshTokenRepository();
$this->authorizationServer = new AuthorizationServer(
$clientRepository,
$accessTokenRepository,
$scopeRepository,
'file://' . __DIR__ . '/keys/private.key',
$this->generateSecureEncryptionKey()
);
$this->setupSecureGrantTypes();
}
private function setupSecureGrantTypes()
{
// PKCE対応のAuthorization Code Grant
$authCodeGrant = new AuthorizationCodeGrant(
$this->repositories['authCode'],
$this->repositories['refreshToken'],
new \DateInterval('PT10M')
);
// PKCEコードチャレンジ検証の設定
$authCodeGrant->setCodeChallengeVerifiers([
new S256Verifier(), // SHA256 (推奨)
new PlainVerifier() // Plain text (非推奨、レガシー対応)
]);
// リフレッシュトークンの設定
$authCodeGrant->setRefreshTokenTTL(new \DateInterval('P1M'));
$this->authorizationServer->enableGrantType(
$authCodeGrant,
new \DateInterval('PT1H')
);
}
private function generateSecureEncryptionKey(): string
{
// 実際の実装では環境変数から取得
return getenv('OAUTH2_ENCRYPTION_KEY') ?:
'def00000' . bin2hex(random_bytes(32));
}
public function handlePKCEAuthorizationRequest()
{
$request = \Laminas\Diactoros\ServerRequestFactory::fromGlobals();
try {
// PKCE対応の認証リクエスト検証
$authRequest = $this->authorizationServer->validateAuthorizationRequest($request);
// PKCEパラメータの検証
$this->validatePKCEParameters($request);
// セキュリティヘッダーの追加
$response = new \Laminas\Diactoros\Response();
$response = $this->addSecurityHeaders($response);
// レート制限の実装
if (!$this->checkRateLimit($request)) {
return new \Laminas\Diactoros\Response\JsonResponse([
'error' => 'too_many_requests',
'error_description' => 'Rate limit exceeded'
], 429);
}
// ユーザー認証とセッション検証
$user = $this->authenticateSecureUser($request);
if (!$user) {
return $this->redirectToSecureLogin($request);
}
$authRequest->setUser($user);
$authRequest->setAuthorizationApproved(true);
return $this->authorizationServer->completeAuthorizationRequest($authRequest, $response);
} catch (\Exception $e) {
$this->logSecurityEvent('authorization_request_failed', [
'error' => $e->getMessage(),
'client_ip' => $this->getClientIP($request),
'user_agent' => $request->getHeaderLine('User-Agent')
]);
return new \Laminas\Diactoros\Response\JsonResponse([
'error' => 'invalid_request',
'error_description' => 'Security validation failed'
], 400);
}
}
private function validatePKCEParameters($request)
{
$queryParams = $request->getQueryParams();
// code_challengeの存在確認
if (!isset($queryParams['code_challenge'])) {
throw new \Exception('PKCE code_challenge is required');
}
// code_challenge_methodの検証
$method = $queryParams['code_challenge_method'] ?? 'plain';
if (!in_array($method, ['S256', 'plain'])) {
throw new \Exception('Invalid code_challenge_method');
}
// S256の使用を強制(セキュリティ強化)
if ($method !== 'S256') {
throw new \Exception('Only S256 code_challenge_method is allowed');
}
// code_challengeの形式検証
$codeChallenge = $queryParams['code_challenge'];
if (!preg_match('/^[A-Za-z0-9\-._~]{43,128}$/', $codeChallenge)) {
throw new \Exception('Invalid code_challenge format');
}
}
private function addSecurityHeaders($response)
{
return $response
->withHeader('X-Content-Type-Options', 'nosniff')
->withHeader('X-Frame-Options', 'DENY')
->withHeader('X-XSS-Protection', '1; mode=block')
->withHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains')
->withHeader('Content-Security-Policy', "default-src 'self'")
->withHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
}
private function checkRateLimit($request): bool
{
$clientIP = $this->getClientIP($request);
$rateLimitKey = "oauth2_rate_limit:{$clientIP}";
// Redis実装例(実際の実装では適切なキャッシュシステムを使用)
$redis = new \Redis();
$redis->connect('127.0.0.1', 6379);
$currentRequests = $redis->incr($rateLimitKey);
if ($currentRequests === 1) {
$redis->expire($rateLimitKey, 3600); // 1時間のウィンドウ
}
// 1時間に100リクエストまで許可
return $currentRequests <= 100;
}
private function authenticateSecureUser($request)
{
session_start();
if (!isset($_SESSION['user_id'])) {
return null;
}
// セッションの有効性検証
if (!$this->validateSession($_SESSION)) {
session_destroy();
return null;
}
// 二要素認証の確認
if (!isset($_SESSION['2fa_verified'])) {
return null;
}
return new \League\OAuth2\Server\Entities\UserEntity($_SESSION['user_id']);
}
private function validateSession($session): bool
{
// セッションタイムアウトの確認
if (time() - $session['created_at'] > 3600) { // 1時間
return false;
}
// IPアドレスの確認(セッションハイジャック対策)
if ($session['ip_address'] !== $_SERVER['REMOTE_ADDR']) {
return false;
}
// User-Agentの確認
if ($session['user_agent'] !== $_SERVER['HTTP_USER_AGENT']) {
return false;
}
return true;
}
private function redirectToSecureLogin($request)
{
$originalUrl = urlencode($request->getUri()->__toString());
$loginUrl = "/secure-login?redirect_uri={$originalUrl}";
return new \Laminas\Diactoros\Response\RedirectResponse($loginUrl);
}
private function getClientIP($request): string
{
// プロキシ環境を考慮したクライアントIP取得
$headers = [
'HTTP_CF_CONNECTING_IP', // Cloudflare
'HTTP_CLIENT_IP', // Proxy
'HTTP_X_FORWARDED_FOR', // Load balancer/proxy
'HTTP_X_FORWARDED', // Proxy
'HTTP_X_CLUSTER_CLIENT_IP', // Cluster
'HTTP_FORWARDED_FOR', // Proxy
'HTTP_FORWARDED', // Proxy
'REMOTE_ADDR' // Standard
];
foreach ($headers as $header) {
if (!empty($_SERVER[$header])) {
$ips = explode(',', $_SERVER[$header]);
return trim($ips[0]);
}
}
return 'unknown';
}
private function logSecurityEvent(string $event, array $context)
{
$logEntry = [
'timestamp' => date('c'),
'event' => $event,
'context' => $context,
'server' => [
'host' => $_SERVER['HTTP_HOST'] ?? 'unknown',
'request_uri' => $_SERVER['REQUEST_URI'] ?? 'unknown'
]
];
error_log('OAuth2 Security Event: ' . json_encode($logEntry));
// 重要なセキュリティイベントは外部監視システムに送信
if (in_array($event, ['authorization_request_failed', 'token_intrusion_attempt'])) {
$this->sendSecurityAlert($logEntry);
}
}
private function sendSecurityAlert(array $logEntry)
{
// セキュリティ監視システムへの通知実装
// 例: Slack通知、メール送信、SIEM連携など
}
}
// セキュア実装の使用例
$secureAuthServer = new SecureAuthServer();
try {
$response = $secureAuthServer->handlePKCEAuthorizationRequest();
// レスポンスの送信
http_response_code($response->getStatusCode());
foreach ($response->getHeaders() as $name => $values) {
foreach ($values as $value) {
header(sprintf('%s: %s', $name, $value), false);
}
}
echo $response->getBody();
} catch (\Exception $e) {
http_response_code(500);
echo json_encode([
'error' => 'server_error',
'error_description' => 'An unexpected error occurred'
]);
}
?>
JWT Token実装とスコープ管理
<?php
require_once 'vendor/autoload.php';
use League\OAuth2\Server\AuthorizationServer;
use League\OAuth2\Server\CryptKey;
use League\OAuth2\Server\ResponseTypes\BearerTokenResponse;
class JWTAuthServer
{
private $authorizationServer;
private $jwtConfiguration;
public function __construct()
{
$this->jwtConfiguration = $this->setupJWTConfiguration();
$this->setupAuthServer();
}
private function setupJWTConfiguration()
{
return [
'private_key' => new CryptKey('file://' . __DIR__ . '/keys/private.key'),
'public_key' => new CryptKey('file://' . __DIR__ . '/keys/public.key'),
'encryption_key' => 'def00000a3f7b7bce7bb66b6b7bb7b7c2ef9ea61c7f5e5b4a2d3c7dd7e9f8a1a2',
'issuer' => 'https://auth.example.com',
'audience' => 'https://api.example.com'
];
}
private function setupAuthServer()
{
$repositories = [
'client' => new JWTClientRepository(),
'scope' => new JWTScopeRepository(),
'accessToken' => new JWTAccessTokenRepository(),
'authCode' => new JWTAuthCodeRepository(),
'refreshToken' => new JWTRefreshTokenRepository()
];
$this->authorizationServer = new AuthorizationServer(
$repositories['client'],
$repositories['accessToken'],
$repositories['scope'],
$this->jwtConfiguration['private_key'],
$this->jwtConfiguration['encryption_key']
);
// カスタムJWTレスポンスタイプの設定
$responseType = new CustomJWTBearerTokenResponse();
$responseType->setPrivateKey($this->jwtConfiguration['private_key']);
$responseType->setEncryptionKey($this->jwtConfiguration['encryption_key']);
$this->authorizationServer->setDefaultResponseType($responseType);
$this->setupGrantTypesWithScopes();
}
private function setupGrantTypesWithScopes()
{
// スコープ付きAuthorization Code Grant
$authCodeGrant = new \League\OAuth2\Server\Grant\AuthorizationCodeGrant(
$this->repositories['authCode'],
$this->repositories['refreshToken'],
new \DateInterval('PT10M')
);
$authCodeGrant->setRefreshTokenTTL(new \DateInterval('P1M'));
$this->authorizationServer->enableGrantType(
$authCodeGrant,
new \DateInterval('PT1H')
);
// スコープ付きClient Credentials Grant
$this->authorizationServer->enableGrantType(
new \League\OAuth2\Server\Grant\ClientCredentialsGrant(),
new \DateInterval('PT2H') // サービス間通信は長めの有効期限
);
}
public function handleScopedTokenRequest()
{
$request = \Laminas\Diactoros\ServerRequestFactory::fromGlobals();
try {
// スコープの事前検証
$this->validateRequestedScopes($request);
$response = $this->authorizationServer->respondToAccessTokenRequest(
$request,
new \Laminas\Diactoros\Response()
);
return $response;
} catch (\Exception $e) {
return new \Laminas\Diactoros\Response\JsonResponse([
'error' => 'invalid_scope',
'error_description' => $e->getMessage()
], 400);
}
}
private function validateRequestedScopes($request)
{
$body = $request->getParsedBody();
$requestedScopes = explode(' ', $body['scope'] ?? '');
// 定義済みスコープ
$validScopes = [
'read' => [
'description' => 'Read access to user data',
'level' => 'basic'
],
'write' => [
'description' => 'Write access to user data',
'level' => 'elevated'
],
'admin' => [
'description' => 'Administrative access',
'level' => 'privileged'
],
'profile' => [
'description' => 'Access to user profile',
'level' => 'basic'
],
'email' => [
'description' => 'Access to user email',
'level' => 'basic'
],
'openid' => [
'description' => 'OpenID Connect authentication',
'level' => 'basic'
]
];
foreach ($requestedScopes as $scope) {
if (!isset($validScopes[$scope])) {
throw new \Exception("Invalid scope: {$scope}");
}
}
// 特権スコープの制限
$privilegedScopes = array_filter($validScopes, function($config) {
return $config['level'] === 'privileged';
});
$requestedPrivileged = array_intersect($requestedScopes, array_keys($privilegedScopes));
if (!empty($requestedPrivileged)) {
$this->validatePrivilegedScopeRequest($request, $requestedPrivileged);
}
}
private function validatePrivilegedScopeRequest($request, array $privilegedScopes)
{
$body = $request->getParsedBody();
$clientId = $body['client_id'] ?? '';
// 特権スコープを要求できるクライアントの制限
$authorizedClients = ['admin-client', 'system-service'];
if (!in_array($clientId, $authorizedClients)) {
throw new \Exception('Client not authorized for privileged scopes: ' . implode(', ', $privilegedScopes));
}
}
}
class CustomJWTBearerTokenResponse extends BearerTokenResponse
{
protected function getExtraParams(\League\OAuth2\Server\Entities\AccessTokenEntityInterface $accessToken): array
{
return [
'token_type' => 'Bearer',
'issued_at' => time(),
'issuer' => 'https://auth.example.com',
'audience' => 'https://api.example.com'
];
}
protected function extraTokenFields(\League\OAuth2\Server\Entities\AccessTokenEntityInterface $accessToken): array
{
return [
'iss' => 'https://auth.example.com',
'aud' => 'https://api.example.com',
'sub' => $accessToken->getUserIdentifier(),
'client_id' => $accessToken->getClient()->getIdentifier(),
'scopes' => array_map(function($scope) {
return $scope->getIdentifier();
}, $accessToken->getScopes())
];
}
}
// JWT実装の使用例
$jwtAuthServer = new JWTAuthServer();
$requestUri = $_SERVER['REQUEST_URI'];
$requestMethod = $_SERVER['REQUEST_METHOD'];
if ($requestUri === '/oauth/token' && $requestMethod === 'POST') {
$response = $jwtAuthServer->handleScopedTokenRequest();
http_response_code($response->getStatusCode());
foreach ($response->getHeaders() as $name => $values) {
foreach ($values as $value) {
header(sprintf('%s: %s', $name, $value), false);
}
}
echo $response->getBody();
}
?>
高度なRepository実装とデータベース連携
<?php
require_once 'vendor/autoload.php';
use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface;
use League\OAuth2\Server\Repositories\ClientRepositoryInterface;
use League\OAuth2\Server\Repositories\ScopeRepositoryInterface;
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
use League\OAuth2\Server\Entities\ClientEntityInterface;
class DatabaseAccessTokenRepository implements AccessTokenRepositoryInterface
{
private $pdo;
public function __construct(\PDO $pdo)
{
$this->pdo = $pdo;
}
public function getNewToken(ClientEntityInterface $clientEntity, array $scopes, $userIdentifier = null)
{
$accessToken = new AccessTokenEntity();
$accessToken->setClient($clientEntity);
foreach ($scopes as $scope) {
$accessToken->addScope($scope);
}
$accessToken->setUserIdentifier($userIdentifier);
$accessToken->setExpiryDateTime(new \DateTime('+1 hour'));
return $accessToken;
}
public function persistNewAccessToken(AccessTokenEntityInterface $accessTokenEntity)
{
$stmt = $this->pdo->prepare('
INSERT INTO oauth_access_tokens (
id, user_id, client_id, scopes, revoked, created_at, updated_at, expires_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
');
$stmt->execute([
$accessTokenEntity->getIdentifier(),
$accessTokenEntity->getUserIdentifier(),
$accessTokenEntity->getClient()->getIdentifier(),
json_encode(array_map(function($scope) {
return $scope->getIdentifier();
}, $accessTokenEntity->getScopes())),
0, // not revoked
date('Y-m-d H:i:s'),
date('Y-m-d H:i:s'),
$accessTokenEntity->getExpiryDateTime()->format('Y-m-d H:i:s')
]);
}
public function revokeAccessToken($tokenId)
{
$stmt = $this->pdo->prepare('
UPDATE oauth_access_tokens
SET revoked = 1, updated_at = ?
WHERE id = ?
');
$stmt->execute([date('Y-m-d H:i:s'), $tokenId]);
}
public function isAccessTokenRevoked($tokenId)
{
$stmt = $this->pdo->prepare('
SELECT revoked FROM oauth_access_tokens WHERE id = ?
');
$stmt->execute([$tokenId]);
$result = $stmt->fetch(\PDO::FETCH_ASSOC);
if (!$result) {
return true; // トークンが存在しない場合は無効として扱う
}
return (bool) $result['revoked'];
}
}
class DatabaseClientRepository implements ClientRepositoryInterface
{
private $pdo;
public function __construct(\PDO $pdo)
{
$this->pdo = $pdo;
}
public function getClientEntity($clientIdentifier)
{
$stmt = $this->pdo->prepare('
SELECT id, name, secret, redirect_uri, is_confidential, scopes
FROM oauth_clients
WHERE id = ? AND revoked = 0
');
$stmt->execute([$clientIdentifier]);
$clientData = $stmt->fetch(\PDO::FETCH_ASSOC);
if (!$clientData) {
return null;
}
$client = new ClientEntity();
$client->setIdentifier($clientData['id']);
$client->setName($clientData['name']);
$client->setRedirectUri($clientData['redirect_uri']);
$client->setConfidential((bool) $clientData['is_confidential']);
return $client;
}
public function validateClient($clientIdentifier, $clientSecret, $grantType)
{
$stmt = $this->pdo->prepare('
SELECT id, secret, allowed_grant_types
FROM oauth_clients
WHERE id = ? AND revoked = 0
');
$stmt->execute([$clientIdentifier]);
$clientData = $stmt->fetch(\PDO::FETCH_ASSOC);
if (!$clientData) {
return false;
}
// Client Secretの検証(confidentialクライアントのみ)
if ($clientSecret !== null && !password_verify($clientSecret, $clientData['secret'])) {
return false;
}
// Grant Typeの検証
$allowedGrantTypes = json_decode($clientData['allowed_grant_types'], true);
if (!in_array($grantType, $allowedGrantTypes)) {
return false;
}
return true;
}
}
class DatabaseScopeRepository implements ScopeRepositoryInterface
{
private $pdo;
public function __construct(\PDO $pdo)
{
$this->pdo = $pdo;
}
public function getScopeEntityByIdentifier($identifier)
{
$stmt = $this->pdo->prepare('
SELECT id, description, is_default
FROM oauth_scopes
WHERE id = ?
');
$stmt->execute([$identifier]);
$scopeData = $stmt->fetch(\PDO::FETCH_ASSOC);
if (!$scopeData) {
return null;
}
$scope = new ScopeEntity();
$scope->setIdentifier($scopeData['id']);
$scope->setDescription($scopeData['description']);
return $scope;
}
public function finalizeScopes(
array $scopes,
$grantType,
ClientEntityInterface $clientEntity,
$userIdentifier = null
) {
// クライアント固有のスコープ制限
$allowedScopes = $this->getClientAllowedScopes($clientEntity->getIdentifier());
// ユーザー固有のスコープ制限
if ($userIdentifier) {
$userAllowedScopes = $this->getUserAllowedScopes($userIdentifier);
$allowedScopes = array_intersect($allowedScopes, $userAllowedScopes);
}
// 要求されたスコープをフィルタリング
$finalizedScopes = [];
foreach ($scopes as $scope) {
if (in_array($scope->getIdentifier(), $allowedScopes)) {
$finalizedScopes[] = $scope;
}
}
return $finalizedScopes;
}
private function getClientAllowedScopes(string $clientId): array
{
$stmt = $this->pdo->prepare('
SELECT scopes FROM oauth_clients WHERE id = ?
');
$stmt->execute([$clientId]);
$result = $stmt->fetch(\PDO::FETCH_ASSOC);
if (!$result) {
return [];
}
return json_decode($result['scopes'], true) ?: [];
}
private function getUserAllowedScopes(string $userId): array
{
$stmt = $this->pdo->prepare('
SELECT s.id
FROM oauth_scopes s
JOIN user_scope_permissions usp ON s.id = usp.scope_id
WHERE usp.user_id = ? AND usp.granted = 1
');
$stmt->execute([$userId]);
return $stmt->fetchAll(\PDO::FETCH_COLUMN);
}
}
class DatabaseManager
{
private $pdo;
public function __construct()
{
$dsn = 'mysql:host=localhost;dbname=oauth2_server;charset=utf8mb4';
$this->pdo = new \PDO($dsn, 'username', 'password', [
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
\PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
\PDO::ATTR_EMULATE_PREPARES => false
]);
}
public function createTables()
{
$tables = [
'oauth_clients' => '
CREATE TABLE oauth_clients (
id VARCHAR(80) NOT NULL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
secret VARCHAR(255),
redirect_uri TEXT,
is_confidential BOOLEAN DEFAULT FALSE,
scopes JSON,
allowed_grant_types JSON,
revoked BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
)',
'oauth_access_tokens' => '
CREATE TABLE oauth_access_tokens (
id VARCHAR(100) NOT NULL PRIMARY KEY,
user_id VARCHAR(80),
client_id VARCHAR(80) NOT NULL,
scopes JSON,
revoked BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
expires_at TIMESTAMP,
FOREIGN KEY (client_id) REFERENCES oauth_clients(id) ON DELETE CASCADE
)',
'oauth_scopes' => '
CREATE TABLE oauth_scopes (
id VARCHAR(80) NOT NULL PRIMARY KEY,
description TEXT,
is_default BOOLEAN DEFAULT FALSE
)',
'user_scope_permissions' => '
CREATE TABLE user_scope_permissions (
user_id VARCHAR(80) NOT NULL,
scope_id VARCHAR(80) NOT NULL,
granted BOOLEAN DEFAULT FALSE,
granted_at TIMESTAMP NULL,
PRIMARY KEY (user_id, scope_id),
FOREIGN KEY (scope_id) REFERENCES oauth_scopes(id) ON DELETE CASCADE
)'
];
foreach ($tables as $tableName => $sql) {
try {
$this->pdo->exec($sql);
echo "Created table: {$tableName}\n";
} catch (\PDOException $e) {
echo "Failed to create table {$tableName}: " . $e->getMessage() . "\n";
}
}
}
public function seedData()
{
// サンプルクライアントの挿入
$this->pdo->prepare('
INSERT INTO oauth_clients (id, name, secret, redirect_uri, is_confidential, scopes, allowed_grant_types)
VALUES (?, ?, ?, ?, ?, ?, ?)
')->execute([
'test-client',
'Test Application',
password_hash('test-secret', PASSWORD_BCRYPT),
'https://app.example.com/callback',
true,
json_encode(['read', 'write', 'profile']),
json_encode(['authorization_code', 'refresh_token'])
]);
// サンプルスコープの挿入
$scopes = [
['read', 'Read access to user data'],
['write', 'Write access to user data'],
['profile', 'Access to user profile'],
['admin', 'Administrative access']
];
$stmt = $this->pdo->prepare('INSERT INTO oauth_scopes (id, description) VALUES (?, ?)');
foreach ($scopes as $scope) {
$stmt->execute($scope);
}
}
public function getPDO(): \PDO
{
return $this->pdo;
}
}
// データベース統合の使用例
$dbManager = new DatabaseManager();
$dbManager->createTables();
$dbManager->seedData();
$pdo = $dbManager->getPDO();
// Repository実装
$accessTokenRepo = new DatabaseAccessTokenRepository($pdo);
$clientRepo = new DatabaseClientRepository($pdo);
$scopeRepo = new DatabaseScopeRepository($pdo);
// 認証サーバーの設定
$authServer = new AuthorizationServer(
$clientRepo,
$accessTokenRepo,
$scopeRepo,
'file://' . __DIR__ . '/keys/private.key',
'encryption-key'
);
echo "OAuth2 Server setup complete with database integration.\n";
?>