League OAuth2 Server
Authentication Library
League OAuth2 Server
Overview
League OAuth2 Server is a comprehensive library for building OAuth 2.0 authorization servers in PHP, providing a secure and scalable authentication infrastructure that complies with RFC 6749.
Details
League OAuth2 Server is a PHP library for implementing OAuth 2.0 authorization servers, developed by The PHP League. It fully complies with major OAuth 2.0-related specifications including RFC 6749, RFC 7636 (PKCE), and RFC 7662 (Token Introspection). It provides complete support for Authorization Code Grant, Client Credentials Grant, Refresh Token Grant, Implicit Grant (deprecated), Password Grant (deprecated), and Device Authorization Grant (RFC 8628) flows. The library enables JWT (JSON Web Token) access token issuance, scope-based authorization control, multi-tenant support, and custom Grant type implementation. Security features include implementation of latest security standards such as PKCE (Proof Key for Code Exchange), token encryption, CSRF attack protection, and rate limiting. Database storage is abstracted through the Repository pattern, supporting various data stores including MySQL, PostgreSQL, and MongoDB. It provides comprehensive functionality required for enterprise-level applications and is widely used as an API authentication infrastructure in microservices architectures.
Pros and Cons
Pros
- RFC Compliant: Full compliance with OAuth 2.0 and OpenID Connect specifications
- Enterprise Grade: Capable of building professional authorization servers
- High Security: Latest security support including PKCE, JWT, and token encryption
- Flexibility: Custom Grant types and Repository implementations possible
- Scalable: Supports operation in large-scale systems
- Multi-tenant: Management of multiple client applications
- Rich Features: Scope management, token validation, refresh, and more
Cons
- Complexity: Requires understanding of OAuth 2.0 specifications and proper implementation
- Setup Cost: Professional setup requires time and specialized knowledge
- PHP Only: Cannot be used in other language environments
- Dependencies: Depends on many components
- Learning Curve: Deep understanding of OAuth 2.0 and OpenID Connect specifications required
Key Links
- League OAuth2 Server Official Page
- League OAuth2 Server Official Documentation
- The PHP League
- RFC 6749 - OAuth 2.0 Specification
- RFC 7636 - PKCE Specification
- OpenID Connect Specification
Code Examples
Hello World (Basic Authorization Server)
<?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;
// Basic authorization server setup
class BasicAuthServer
{
private $authorizationServer;
private $resourceServer;
public function __construct()
{
// Repository initialization (use appropriate Repository classes in actual implementation)
$clientRepository = new ClientRepository();
$scopeRepository = new ScopeRepository();
$accessTokenRepository = new AccessTokenRepository();
$authCodeRepository = new AuthCodeRepository();
$refreshTokenRepository = new RefreshTokenRepository();
// Authorization server configuration
$this->authorizationServer = new AuthorizationServer(
$clientRepository,
$accessTokenRepository,
$scopeRepository,
'file://' . __DIR__ . '/private.key', // Private key
'your-encryption-key' // Encryption key
);
// Enable Authorization Code Grant
$authCodeGrant = new AuthorizationCodeGrant(
$authCodeRepository,
$refreshTokenRepository,
new \DateInterval('PT10M') // Authorization code TTL: 10 minutes
);
$authCodeGrant->setRefreshTokenTTL(new \DateInterval('P1M')); // Refresh token: 1 month
$this->authorizationServer->enableGrantType(
$authCodeGrant,
new \DateInterval('PT1H') // Access token TTL: 1 hour
);
// Resource server configuration
$this->resourceServer = new ResourceServer(
$accessTokenRepository,
'file://' . __DIR__ . '/public.key' // Public key
);
}
public function handleAuthorizationRequest()
{
$request = \Laminas\Diactoros\ServerRequestFactory::fromGlobals();
try {
// Validate authorization request
$authRequest = $this->authorizationServer->validateAuthorizationRequest($request);
// User authentication (implement proper authentication in actual implementation)
$user = $this->authenticateUser($request);
if (!$user) {
throw new \Exception('User authentication failed');
}
$authRequest->setUser($user);
$authRequest->setAuthorizationApproved(true);
// Generate authorization response
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)
{
// Get user information from session or DB in actual implementation
return new \League\OAuth2\Server\Entities\UserEntity('user-123');
}
}
// Basic usage example
$authServer = new BasicAuthServer();
// Routing example
$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) {
// Access to protected resource
echo json_encode(['message' => 'Protected resource accessed successfully']);
}
} else {
http_response_code(404);
echo 'Not Found';
}
?>
Multi-Grant Type Authorization Server
<?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 (most common)
$authCodeGrant = new AuthorizationCodeGrant(
$this->repositories['authCode'],
$this->repositories['refreshToken'],
new \DateInterval('PT10M') // Authorization code: 10 minutes
);
$authCodeGrant->setRefreshTokenTTL(new \DateInterval('P1M')); // Refresh token: 1 month
$this->authorizationServer->enableGrantType(
$authCodeGrant,
new \DateInterval('PT1H') // Access token: 1 hour
);
// 2. Client Credentials Grant (service-to-service communication)
$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 (recommended for legacy use only)
$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);
// Check if user is logged in
session_start();
if (!isset($_SESSION['user_id'])) {
// Redirect to login page
$loginUrl = '/login?' . http_build_query([
'redirect_uri' => $request->getUri()->__toString()
]);
return new \Laminas\Diactoros\Response\RedirectResponse($loginUrl);
}
// Display authorization consent screen
$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);
// Check user approval
$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>Authorization Approval</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>Authorization Approval</h2>
<p><strong>{$client->getName()}</strong> is requesting access with the following permissions:</p>
<p><strong>Scopes:</strong> {$scopeList}</p>
<form method='post'>
<button type='submit' name='approve' value='yes' class='button approve'>Approve</button>
<button type='submit' name='approve' value='no' class='button deny'>Deny</button>
</form>
</div>
</body>
</html>";
}
}
// Routing example
$authServer = new AdvancedAuthServer();
echo $authServer->handleRequest();
?>
PKCE Support and Security Enhancement
<?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-enabled Authorization Code Grant
$authCodeGrant = new AuthorizationCodeGrant(
$this->repositories['authCode'],
$this->repositories['refreshToken'],
new \DateInterval('PT10M')
);
// PKCE code challenge verification setup
$authCodeGrant->setCodeChallengeVerifiers([
new S256Verifier(), // SHA256 (recommended)
new PlainVerifier() // Plain text (deprecated, for legacy support)
]);
// Refresh token configuration
$authCodeGrant->setRefreshTokenTTL(new \DateInterval('P1M'));
$this->authorizationServer->enableGrantType(
$authCodeGrant,
new \DateInterval('PT1H')
);
}
private function generateSecureEncryptionKey(): string
{
// Get from environment variables in actual implementation
return getenv('OAUTH2_ENCRYPTION_KEY') ?:
'def00000' . bin2hex(random_bytes(32));
}
public function handlePKCEAuthorizationRequest()
{
$request = \Laminas\Diactoros\ServerRequestFactory::fromGlobals();
try {
// PKCE-enabled authorization request validation
$authRequest = $this->authorizationServer->validateAuthorizationRequest($request);
// PKCE parameter validation
$this->validatePKCEParameters($request);
// Add security headers
$response = new \Laminas\Diactoros\Response();
$response = $this->addSecurityHeaders($response);
// Rate limiting implementation
if (!$this->checkRateLimit($request)) {
return new \Laminas\Diactoros\Response\JsonResponse([
'error' => 'too_many_requests',
'error_description' => 'Rate limit exceeded'
], 429);
}
// User authentication and session validation
$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();
// Check code_challenge existence
if (!isset($queryParams['code_challenge'])) {
throw new \Exception('PKCE code_challenge is required');
}
// Validate code_challenge_method
$method = $queryParams['code_challenge_method'] ?? 'plain';
if (!in_array($method, ['S256', 'plain'])) {
throw new \Exception('Invalid code_challenge_method');
}
// Force S256 usage (security enhancement)
if ($method !== 'S256') {
throw new \Exception('Only S256 code_challenge_method is allowed');
}
// Validate code_challenge format
$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 implementation example (use appropriate cache system in actual implementation)
$redis = new \Redis();
$redis->connect('127.0.0.1', 6379);
$currentRequests = $redis->incr($rateLimitKey);
if ($currentRequests === 1) {
$redis->expire($rateLimitKey, 3600); // 1 hour window
}
// Allow up to 100 requests per hour
return $currentRequests <= 100;
}
private function authenticateSecureUser($request)
{
session_start();
if (!isset($_SESSION['user_id'])) {
return null;
}
// Session validity validation
if (!$this->validateSession($_SESSION)) {
session_destroy();
return null;
}
// Two-factor authentication check
if (!isset($_SESSION['2fa_verified'])) {
return null;
}
return new \League\OAuth2\Server\Entities\UserEntity($_SESSION['user_id']);
}
private function validateSession($session): bool
{
// Session timeout check
if (time() - $session['created_at'] > 3600) { // 1 hour
return false;
}
// IP address check (session hijacking protection)
if ($session['ip_address'] !== $_SERVER['REMOTE_ADDR']) {
return false;
}
// User-Agent check
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
{
// Get client IP considering proxy environment
$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));
// Send critical security events to external monitoring system
if (in_array($event, ['authorization_request_failed', 'token_intrusion_attempt'])) {
$this->sendSecurityAlert($logEntry);
}
}
private function sendSecurityAlert(array $logEntry)
{
// Implementation for sending to security monitoring system
// Example: Slack notification, email sending, SIEM integration, etc.
}
}
// Secure implementation usage example
$secureAuthServer = new SecureAuthServer();
try {
$response = $secureAuthServer->handlePKCEAuthorizationRequest();
// Send response
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 Implementation and Scope Management
<?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']
);
// Custom JWT response type configuration
$responseType = new CustomJWTBearerTokenResponse();
$responseType->setPrivateKey($this->jwtConfiguration['private_key']);
$responseType->setEncryptionKey($this->jwtConfiguration['encryption_key']);
$this->authorizationServer->setDefaultResponseType($responseType);
$this->setupGrantTypesWithScopes();
}
private function setupGrantTypesWithScopes()
{
// Scoped 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')
);
// Scoped Client Credentials Grant
$this->authorizationServer->enableGrantType(
new \League\OAuth2\Server\Grant\ClientCredentialsGrant(),
new \DateInterval('PT2H') // Longer TTL for service-to-service communication
);
}
public function handleScopedTokenRequest()
{
$request = \Laminas\Diactoros\ServerRequestFactory::fromGlobals();
try {
// Pre-validate scopes
$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'] ?? '');
// Defined scopes
$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}");
}
}
// Privileged scope restrictions
$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'] ?? '';
// Restrict clients that can request privileged scopes
$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 implementation usage example
$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();
}
?>