Loading classes/ClientAuthChecker.php 0 → 100644 +47 −0 Original line number Diff line number Diff line <?php namespace RAP; /** * RFC 6749 specify that in some situations the client must send an Authorization * Basic header containing its credentials (access token in the authorization code * flow and refresh token requests). */ class ClientAuthChecker { private $locator; public function __construct(Locator $locator) { $this->locator = $locator; } public function validateClientAuth(): void { $headers = apache_request_headers(); if (!isset($headers['Authorization'])) { throw new UnauthorizedException("Missing Authorization header"); } $authorizationHeader = explode(" ", $headers['Authorization']); if ($authorizationHeader[0] === "Basic") { $basic = explode(':', base64_decode($authorizationHeader[1])); if (count($basic) !== 2) { throw new BadRequestException("Malformed Basic-Auth header"); } $clientId = $basic[0]; $clientSecret = $basic[1]; $client = $this->locator->getOAuth2ClientDAO()->getOAuth2ClientByClientId($clientId); if ($client === null) { throw new UnauthorizedException("Client '$clientId' not configured"); } if ($clientSecret !== $client->secret) { throw new UnauthorizedException("Invalid client secret"); } } else { throw new UnauthorizedException("Expected Basic authorization header"); } } } classes/Locator.php +8 −0 Original line number Diff line number Diff line Loading @@ -102,6 +102,14 @@ class Locator { return new TokenBuilder($this); } public function getTokenChecker(): TokenChecker { return new TokenChecker($this); } public function getClientAuthChecker(): ClientAuthChecker { return new ClientAuthChecker($this); } /** * Retrieve the SessionData object from the $_SESSION PHP variable. Create a * new one if it is necessary. Loading classes/OAuth2RequestHandler.php +58 −69 Original line number Diff line number Diff line Loading @@ -2,8 +2,6 @@ namespace RAP; use \Firebase\JWT\JWT; class OAuth2RequestHandler { private $locator; Loading Loading @@ -84,7 +82,9 @@ class OAuth2RequestHandler { . '?code=' . $code . '&scope=profile&state=' . $state; } else { // Implicit grant flow $idToken = $this->locator->getTokenBuilder()->getIdToken($tokenData, $nonce); $idToken = $this->locator->getTokenBuilder()->getIdToken($tokenData, function(& $jwt) use($nonce) { $jwt['nonce'] = $nonce; }); $redirectUrl = $session->getOAuth2RequestData()->redirectUrl . "#id_token=" . $idToken; } Loading @@ -93,6 +93,8 @@ class OAuth2RequestHandler { public function handleAccessTokenRequest($params): array { $this->locator->getClientAuthChecker()->validateClientAuth(); if ($params['code'] === null) { throw new BadRequestException("code id is required"); } Loading Loading @@ -123,6 +125,8 @@ class OAuth2RequestHandler { public function handleRefreshTokenRequest($params): array { $this->locator->getClientAuthChecker()->validateClientAuth(); if ($params['refresh_token'] === null) { throw new BadRequestException("refresh_token is required"); } Loading @@ -138,7 +142,6 @@ class OAuth2RequestHandler { // Generating a new access token $accessTokenData = new AccessTokenData(); $accessTokenData->token = base64_encode(bin2hex(openssl_random_pseudo_bytes(128))); $accessTokenData->clientId = $refreshToken->clientId; $accessTokenData->userId = $refreshToken->userId; $accessTokenData->scope = $scope; Loading Loading @@ -186,10 +189,7 @@ class OAuth2RequestHandler { $result['token_type'] = 'Bearer'; $result['expires_in'] = $tokenData->expirationTime - time(); $refreshToken = base64_encode(bin2hex(openssl_random_pseudo_bytes(128))); $refreshTokenHash = hash('sha256', $refreshToken); $this->storeRefreshTokenData($tokenData, $refreshTokenHash); $result['refresh_token'] = $refreshToken; $result['refresh_token'] = $this->buildRefreshToken($tokenData); if ($tokenData->scope !== null && in_array('openid', $tokenData->scope)) { $result['id_token'] = $this->locator->getTokenBuilder()->getIdToken($tokenData); Loading @@ -198,93 +198,82 @@ class OAuth2RequestHandler { return $result; } private function storeRefreshTokenData(AccessTokenData $accessTokenData, string $refreshTokenHash): void { $refreshToken = new RefreshTokenData(); $refreshToken->tokenHash = $refreshTokenHash; $refreshToken->clientId = $accessTokenData->clientId; $refreshToken->userId = $accessTokenData->userId; $refreshToken->scope = $accessTokenData->scope; $this->locator->getRefreshTokenDAO()->createRefreshTokenData($refreshToken); } /** * Token introspection endpoint shouldn't be necessary when using OIDC (since * tokens are self-contained JWT). This function is kept here for compatibility * with some libraries (e.g. Spring Security) but it could be removed in the * future. */ public function handleCheckTokenRequest($token): array { // TODO: validate the token and expose data $accessToken = $this->locator->getAccessTokenDAO()->getAccessToken($token); if ($accessToken === null) { throw new UnauthorizedException("Invalid access token"); } public function handleCheckTokenRequest(): array { $user = $this->locator->getUserDAO()->findUserById($accessToken->userId); $jwt = $this->locator->getTokenChecker()->validateToken(); $tokenData = $this->getTokenDataFromJwtObject($jwt); $result = []; $result['exp'] = $accessToken->expirationTime - time(); $result['user_name'] = $user->id; $result['client_id'] = $accessToken->clientId; $result['refresh_token'] = $this->storeRefreshTokenData($accessToken); if ($accessToken->scope !== null) { $result['scope'] = $accessToken->scope; if (in_array('openid', $accessToken->scope)) { $result['id_token'] = $this->locator->getTokenBuilder()->getIdToken($accessToken); $result['exp'] = $tokenData->expirationTime - time(); $result['user_name'] = $tokenData->userId; $result['client_id'] = $tokenData->clientId; $result['access_token'] = $this->copyReceivedAccessToken(); $result['refresh_token'] = $this->buildRefreshToken($tokenData); if (isset($tokenData->scope) && count($tokenData->scope) > 0) { $result['scope'] = $tokenData->scope; if (in_array('openid', $tokenData->scope)) { $result['id_token'] = $this->locator->getTokenBuilder()->getIdToken($tokenData); } } return $result; } public function validateToken(): void { private function copyReceivedAccessToken(): string { $headers = apache_request_headers(); if (!isset($headers['Authorization'])) { throw new BadRequestException("Missing Authorization header"); return explode(" ", $headers['Authorization'])[1]; } $authorizationHeader = explode(" ", $headers['Authorization']); if ($authorizationHeader[0] === "Bearer") { $bearer_token = $authorizationHeader[1]; } else { throw new BadRequestException("Invalid token type"); } private function getTokenDataFromJwtObject($jwt): AccessTokenData { $accessToken = $this->locator->getAccessTokenDAO()->getAccessToken($bearer_token); if ($accessToken === null) { $this->attemptJWTTokenValidation($bearer_token); } else if ($accessToken->isExpired()) { throw new UnauthorizedException("Access token is expired"); } $tokenData = new AccessTokenData(); $tokenData->clientId = $this->getClientIdFromAudience($jwt); $tokenData->userId = $jwt->sub; $tokenData->creationTime = $jwt->iat; $tokenData->expirationTime = $jwt->exp; $tokenData->scope = explode(' ', $jwt->scope); return $tokenData; } private function attemptJWTTokenValidation($jwt): void { private function getClientIdFromAudience(object $jwt): string { $jwtParts = explode('.', $jwt); if (count($jwtParts) === 0) { throw new UnauthorizedException("Invalid token"); if (!(isset($jwt->aud))) { throw new UnauthorizedException("Missing 'aud' claim in token"); } $header = JWT::jsonDecode(JWT::urlsafeB64Decode($jwtParts[0])); if (!isset($header->kid)) { throw new UnauthorizedException("Invalid token: missing kid in header"); $audience = $jwt->aud; if (is_array($audience)) { if (count($audience) === 0) { throw new UnauthorizedException("Token has empty audience"); } $keyPair = $this->locator->getJWKSDAO()->getRSAKeyPairById($header->kid); if ($keyPair === null) { throw new UnauthorizedException("Invalid kid: no key found"); return $audience[0]; } return $audience; } try { JWT::decode($jwt, $keyPair->publicKey, [$keyPair->alg]); } catch (\Firebase\JWT\ExpiredException $ex) { throw new UnauthorizedException("Access token is expired"); private function buildRefreshToken(AccessTokenData $tokenData): string { $refreshToken = base64_encode(bin2hex(openssl_random_pseudo_bytes(128))); $refreshTokenHash = hash('sha256', $refreshToken); $this->storeRefreshTokenData($tokenData, $refreshTokenHash); return $refreshToken; } private function storeRefreshTokenData(AccessTokenData $accessTokenData, string $refreshTokenHash): void { $refreshToken = new RefreshTokenData(); $refreshToken->tokenHash = $refreshTokenHash; $refreshToken->clientId = $accessTokenData->clientId; $refreshToken->userId = $accessTokenData->userId; $refreshToken->scope = $accessTokenData->scope; $this->locator->getRefreshTokenDAO()->createRefreshTokenData($refreshToken); } } classes/TokenBuilder.php +14 −12 Original line number Diff line number Diff line Loading @@ -12,16 +12,16 @@ class TokenBuilder { $this->locator = $locator; } public function getIdToken(AccessTokenData $tokenData, string $nonce = null): string { public function getIdToken(AccessTokenData $tokenData, \Closure $jwtCustomizer = null): string { $keyPair = $this->locator->getJWKSDAO()->getNewestKeyPair(); $payload = $this->createIdTokenPayloadArray($tokenData, $nonce); $payload = $this->createIdTokenPayloadArray($tokenData, $jwtCustomizer); return JWT::encode($payload, $keyPair->privateKey, $keyPair->alg, $keyPair->keyId); } private function createIdTokenPayloadArray(AccessTokenData $tokenData, string $nonce = null) { private function createIdTokenPayloadArray(AccessTokenData $tokenData, \Closure $jwtCustomizer = null) { $user = $this->locator->getUserDAO()->findUserById($tokenData->userId); Loading @@ -34,10 +34,6 @@ class TokenBuilder { 'aud' => $tokenData->clientId ); if ($nonce !== null) { $payloadArr['nonce'] = $nonce; } if (in_array("email", $tokenData->scope)) { $payloadArr['email'] = $user->getPrimaryEmail(); } Loading @@ -49,14 +45,15 @@ class TokenBuilder { } } /*if ($tokenData->joinUser !== null) { $payloadArr['alt_sub'] = strval($tokenData->joinUser); }*/ if ($jwtCustomizer !== null) { // Add additional custom claims $jwtCustomizer($payloadArr); } return $payloadArr; } public function getAccessToken(AccessTokenData $tokenData) { public function getAccessToken(AccessTokenData $tokenData, \Closure $jwtCustomizer = null) { $keyPair = $this->locator->getJWKSDAO()->getNewestKeyPair(); Loading @@ -67,8 +64,13 @@ class TokenBuilder { 'sub' => strval($user->id), 'iat' => intval($tokenData->creationTime), 'exp' => intval($tokenData->expirationTime), 'aud' => $this->getAudience($tokenData) 'aud' => $this->getAudience($tokenData), 'scope' => implode(' ', $tokenData->scope) ); if ($jwtCustomizer !== null) { // Add additional custom claims $jwtCustomizer($payload); } return JWT::encode($payload, $keyPair->privateKey, $keyPair->alg, $keyPair->keyId); } Loading classes/TokenChecker.php 0 → 100644 +73 −0 Original line number Diff line number Diff line <?php namespace RAP; use \Firebase\JWT\JWT; class TokenChecker { private $locator; public function __construct(Locator $locator) { $this->locator = $locator; } public function validateToken(): object { $headers = apache_request_headers(); if (!isset($headers['Authorization'])) { throw new BadRequestException("Missing Authorization header"); } $authorizationHeader = explode(" ", $headers['Authorization']); if ($authorizationHeader[0] === "Bearer") { $token = $authorizationHeader[1]; } else { throw new BadRequestException("Invalid token type"); } return $this->attemptJWTTokenValidation($token); } private function attemptJWTTokenValidation($jwt): object { $jwtParts = explode('.', $jwt); if (count($jwtParts) === 0) { throw new UnauthorizedException("Invalid token"); } $header = JWT::jsonDecode(JWT::urlsafeB64Decode($jwtParts[0])); if (!isset($header->kid)) { throw new UnauthorizedException("Invalid token: missing kid in header"); } $keyPair = $this->locator->getJWKSDAO()->getRSAKeyPairById($header->kid); if ($keyPair === null) { throw new UnauthorizedException("Invalid kid: no key found"); } try { return JWT::decode($jwt, $keyPair->publicKey, [$keyPair->alg]); } catch (\Firebase\JWT\ExpiredException $ex) { throw new UnauthorizedException("Access token is expired"); } } public function checkScope(object $tokenData, string $desiredScope): void { if (!(isset($tokenData->scope))) { throw new UnauthorizedException("Missing 'scope' claim in access token"); } $scopes = explode(' ', $tokenData->scope); foreach ($scopes as $scope) { if ($scope === $desiredScope) { return; } } throw new UnauthorizedException("Scope '$desiredScope' is required for performing this action"); } } Loading
classes/ClientAuthChecker.php 0 → 100644 +47 −0 Original line number Diff line number Diff line <?php namespace RAP; /** * RFC 6749 specify that in some situations the client must send an Authorization * Basic header containing its credentials (access token in the authorization code * flow and refresh token requests). */ class ClientAuthChecker { private $locator; public function __construct(Locator $locator) { $this->locator = $locator; } public function validateClientAuth(): void { $headers = apache_request_headers(); if (!isset($headers['Authorization'])) { throw new UnauthorizedException("Missing Authorization header"); } $authorizationHeader = explode(" ", $headers['Authorization']); if ($authorizationHeader[0] === "Basic") { $basic = explode(':', base64_decode($authorizationHeader[1])); if (count($basic) !== 2) { throw new BadRequestException("Malformed Basic-Auth header"); } $clientId = $basic[0]; $clientSecret = $basic[1]; $client = $this->locator->getOAuth2ClientDAO()->getOAuth2ClientByClientId($clientId); if ($client === null) { throw new UnauthorizedException("Client '$clientId' not configured"); } if ($clientSecret !== $client->secret) { throw new UnauthorizedException("Invalid client secret"); } } else { throw new UnauthorizedException("Expected Basic authorization header"); } } }
classes/Locator.php +8 −0 Original line number Diff line number Diff line Loading @@ -102,6 +102,14 @@ class Locator { return new TokenBuilder($this); } public function getTokenChecker(): TokenChecker { return new TokenChecker($this); } public function getClientAuthChecker(): ClientAuthChecker { return new ClientAuthChecker($this); } /** * Retrieve the SessionData object from the $_SESSION PHP variable. Create a * new one if it is necessary. Loading
classes/OAuth2RequestHandler.php +58 −69 Original line number Diff line number Diff line Loading @@ -2,8 +2,6 @@ namespace RAP; use \Firebase\JWT\JWT; class OAuth2RequestHandler { private $locator; Loading Loading @@ -84,7 +82,9 @@ class OAuth2RequestHandler { . '?code=' . $code . '&scope=profile&state=' . $state; } else { // Implicit grant flow $idToken = $this->locator->getTokenBuilder()->getIdToken($tokenData, $nonce); $idToken = $this->locator->getTokenBuilder()->getIdToken($tokenData, function(& $jwt) use($nonce) { $jwt['nonce'] = $nonce; }); $redirectUrl = $session->getOAuth2RequestData()->redirectUrl . "#id_token=" . $idToken; } Loading @@ -93,6 +93,8 @@ class OAuth2RequestHandler { public function handleAccessTokenRequest($params): array { $this->locator->getClientAuthChecker()->validateClientAuth(); if ($params['code'] === null) { throw new BadRequestException("code id is required"); } Loading Loading @@ -123,6 +125,8 @@ class OAuth2RequestHandler { public function handleRefreshTokenRequest($params): array { $this->locator->getClientAuthChecker()->validateClientAuth(); if ($params['refresh_token'] === null) { throw new BadRequestException("refresh_token is required"); } Loading @@ -138,7 +142,6 @@ class OAuth2RequestHandler { // Generating a new access token $accessTokenData = new AccessTokenData(); $accessTokenData->token = base64_encode(bin2hex(openssl_random_pseudo_bytes(128))); $accessTokenData->clientId = $refreshToken->clientId; $accessTokenData->userId = $refreshToken->userId; $accessTokenData->scope = $scope; Loading Loading @@ -186,10 +189,7 @@ class OAuth2RequestHandler { $result['token_type'] = 'Bearer'; $result['expires_in'] = $tokenData->expirationTime - time(); $refreshToken = base64_encode(bin2hex(openssl_random_pseudo_bytes(128))); $refreshTokenHash = hash('sha256', $refreshToken); $this->storeRefreshTokenData($tokenData, $refreshTokenHash); $result['refresh_token'] = $refreshToken; $result['refresh_token'] = $this->buildRefreshToken($tokenData); if ($tokenData->scope !== null && in_array('openid', $tokenData->scope)) { $result['id_token'] = $this->locator->getTokenBuilder()->getIdToken($tokenData); Loading @@ -198,93 +198,82 @@ class OAuth2RequestHandler { return $result; } private function storeRefreshTokenData(AccessTokenData $accessTokenData, string $refreshTokenHash): void { $refreshToken = new RefreshTokenData(); $refreshToken->tokenHash = $refreshTokenHash; $refreshToken->clientId = $accessTokenData->clientId; $refreshToken->userId = $accessTokenData->userId; $refreshToken->scope = $accessTokenData->scope; $this->locator->getRefreshTokenDAO()->createRefreshTokenData($refreshToken); } /** * Token introspection endpoint shouldn't be necessary when using OIDC (since * tokens are self-contained JWT). This function is kept here for compatibility * with some libraries (e.g. Spring Security) but it could be removed in the * future. */ public function handleCheckTokenRequest($token): array { // TODO: validate the token and expose data $accessToken = $this->locator->getAccessTokenDAO()->getAccessToken($token); if ($accessToken === null) { throw new UnauthorizedException("Invalid access token"); } public function handleCheckTokenRequest(): array { $user = $this->locator->getUserDAO()->findUserById($accessToken->userId); $jwt = $this->locator->getTokenChecker()->validateToken(); $tokenData = $this->getTokenDataFromJwtObject($jwt); $result = []; $result['exp'] = $accessToken->expirationTime - time(); $result['user_name'] = $user->id; $result['client_id'] = $accessToken->clientId; $result['refresh_token'] = $this->storeRefreshTokenData($accessToken); if ($accessToken->scope !== null) { $result['scope'] = $accessToken->scope; if (in_array('openid', $accessToken->scope)) { $result['id_token'] = $this->locator->getTokenBuilder()->getIdToken($accessToken); $result['exp'] = $tokenData->expirationTime - time(); $result['user_name'] = $tokenData->userId; $result['client_id'] = $tokenData->clientId; $result['access_token'] = $this->copyReceivedAccessToken(); $result['refresh_token'] = $this->buildRefreshToken($tokenData); if (isset($tokenData->scope) && count($tokenData->scope) > 0) { $result['scope'] = $tokenData->scope; if (in_array('openid', $tokenData->scope)) { $result['id_token'] = $this->locator->getTokenBuilder()->getIdToken($tokenData); } } return $result; } public function validateToken(): void { private function copyReceivedAccessToken(): string { $headers = apache_request_headers(); if (!isset($headers['Authorization'])) { throw new BadRequestException("Missing Authorization header"); return explode(" ", $headers['Authorization'])[1]; } $authorizationHeader = explode(" ", $headers['Authorization']); if ($authorizationHeader[0] === "Bearer") { $bearer_token = $authorizationHeader[1]; } else { throw new BadRequestException("Invalid token type"); } private function getTokenDataFromJwtObject($jwt): AccessTokenData { $accessToken = $this->locator->getAccessTokenDAO()->getAccessToken($bearer_token); if ($accessToken === null) { $this->attemptJWTTokenValidation($bearer_token); } else if ($accessToken->isExpired()) { throw new UnauthorizedException("Access token is expired"); } $tokenData = new AccessTokenData(); $tokenData->clientId = $this->getClientIdFromAudience($jwt); $tokenData->userId = $jwt->sub; $tokenData->creationTime = $jwt->iat; $tokenData->expirationTime = $jwt->exp; $tokenData->scope = explode(' ', $jwt->scope); return $tokenData; } private function attemptJWTTokenValidation($jwt): void { private function getClientIdFromAudience(object $jwt): string { $jwtParts = explode('.', $jwt); if (count($jwtParts) === 0) { throw new UnauthorizedException("Invalid token"); if (!(isset($jwt->aud))) { throw new UnauthorizedException("Missing 'aud' claim in token"); } $header = JWT::jsonDecode(JWT::urlsafeB64Decode($jwtParts[0])); if (!isset($header->kid)) { throw new UnauthorizedException("Invalid token: missing kid in header"); $audience = $jwt->aud; if (is_array($audience)) { if (count($audience) === 0) { throw new UnauthorizedException("Token has empty audience"); } $keyPair = $this->locator->getJWKSDAO()->getRSAKeyPairById($header->kid); if ($keyPair === null) { throw new UnauthorizedException("Invalid kid: no key found"); return $audience[0]; } return $audience; } try { JWT::decode($jwt, $keyPair->publicKey, [$keyPair->alg]); } catch (\Firebase\JWT\ExpiredException $ex) { throw new UnauthorizedException("Access token is expired"); private function buildRefreshToken(AccessTokenData $tokenData): string { $refreshToken = base64_encode(bin2hex(openssl_random_pseudo_bytes(128))); $refreshTokenHash = hash('sha256', $refreshToken); $this->storeRefreshTokenData($tokenData, $refreshTokenHash); return $refreshToken; } private function storeRefreshTokenData(AccessTokenData $accessTokenData, string $refreshTokenHash): void { $refreshToken = new RefreshTokenData(); $refreshToken->tokenHash = $refreshTokenHash; $refreshToken->clientId = $accessTokenData->clientId; $refreshToken->userId = $accessTokenData->userId; $refreshToken->scope = $accessTokenData->scope; $this->locator->getRefreshTokenDAO()->createRefreshTokenData($refreshToken); } }
classes/TokenBuilder.php +14 −12 Original line number Diff line number Diff line Loading @@ -12,16 +12,16 @@ class TokenBuilder { $this->locator = $locator; } public function getIdToken(AccessTokenData $tokenData, string $nonce = null): string { public function getIdToken(AccessTokenData $tokenData, \Closure $jwtCustomizer = null): string { $keyPair = $this->locator->getJWKSDAO()->getNewestKeyPair(); $payload = $this->createIdTokenPayloadArray($tokenData, $nonce); $payload = $this->createIdTokenPayloadArray($tokenData, $jwtCustomizer); return JWT::encode($payload, $keyPair->privateKey, $keyPair->alg, $keyPair->keyId); } private function createIdTokenPayloadArray(AccessTokenData $tokenData, string $nonce = null) { private function createIdTokenPayloadArray(AccessTokenData $tokenData, \Closure $jwtCustomizer = null) { $user = $this->locator->getUserDAO()->findUserById($tokenData->userId); Loading @@ -34,10 +34,6 @@ class TokenBuilder { 'aud' => $tokenData->clientId ); if ($nonce !== null) { $payloadArr['nonce'] = $nonce; } if (in_array("email", $tokenData->scope)) { $payloadArr['email'] = $user->getPrimaryEmail(); } Loading @@ -49,14 +45,15 @@ class TokenBuilder { } } /*if ($tokenData->joinUser !== null) { $payloadArr['alt_sub'] = strval($tokenData->joinUser); }*/ if ($jwtCustomizer !== null) { // Add additional custom claims $jwtCustomizer($payloadArr); } return $payloadArr; } public function getAccessToken(AccessTokenData $tokenData) { public function getAccessToken(AccessTokenData $tokenData, \Closure $jwtCustomizer = null) { $keyPair = $this->locator->getJWKSDAO()->getNewestKeyPair(); Loading @@ -67,8 +64,13 @@ class TokenBuilder { 'sub' => strval($user->id), 'iat' => intval($tokenData->creationTime), 'exp' => intval($tokenData->expirationTime), 'aud' => $this->getAudience($tokenData) 'aud' => $this->getAudience($tokenData), 'scope' => implode(' ', $tokenData->scope) ); if ($jwtCustomizer !== null) { // Add additional custom claims $jwtCustomizer($payload); } return JWT::encode($payload, $keyPair->privateKey, $keyPair->alg, $keyPair->keyId); } Loading
classes/TokenChecker.php 0 → 100644 +73 −0 Original line number Diff line number Diff line <?php namespace RAP; use \Firebase\JWT\JWT; class TokenChecker { private $locator; public function __construct(Locator $locator) { $this->locator = $locator; } public function validateToken(): object { $headers = apache_request_headers(); if (!isset($headers['Authorization'])) { throw new BadRequestException("Missing Authorization header"); } $authorizationHeader = explode(" ", $headers['Authorization']); if ($authorizationHeader[0] === "Bearer") { $token = $authorizationHeader[1]; } else { throw new BadRequestException("Invalid token type"); } return $this->attemptJWTTokenValidation($token); } private function attemptJWTTokenValidation($jwt): object { $jwtParts = explode('.', $jwt); if (count($jwtParts) === 0) { throw new UnauthorizedException("Invalid token"); } $header = JWT::jsonDecode(JWT::urlsafeB64Decode($jwtParts[0])); if (!isset($header->kid)) { throw new UnauthorizedException("Invalid token: missing kid in header"); } $keyPair = $this->locator->getJWKSDAO()->getRSAKeyPairById($header->kid); if ($keyPair === null) { throw new UnauthorizedException("Invalid kid: no key found"); } try { return JWT::decode($jwt, $keyPair->publicKey, [$keyPair->alg]); } catch (\Firebase\JWT\ExpiredException $ex) { throw new UnauthorizedException("Access token is expired"); } } public function checkScope(object $tokenData, string $desiredScope): void { if (!(isset($tokenData->scope))) { throw new UnauthorizedException("Missing 'scope' claim in access token"); } $scopes = explode(' ', $tokenData->scope); foreach ($scopes as $scope) { if ($scope === $desiredScope) { return; } } throw new UnauthorizedException("Scope '$desiredScope' is required for performing this action"); } }