Loading classes/Locator.php +4 −0 Original line number Diff line number Diff line Loading @@ -94,6 +94,10 @@ class Locator { return new ClientAuthChecker($this); } public function getTokenExchanger(): TokenExchanger { return new TokenExchanger($this); } /** * Retrieve the SessionData object from the $_SESSION PHP variable. Create a * new one if it is necessary. Loading classes/OAuth2RequestHandler.php +2 −0 Original line number Diff line number Diff line Loading @@ -106,6 +106,8 @@ class OAuth2RequestHandler { return $this->handleClientCredentialsRequest($headers); case "refresh_token": return $this->handleRefreshTokenRequest($params, $headers); case "urn:ietf:params:oauth:grant-type:token-exchange": return $this->locator->getTokenExchanger()->exchangeToken($params, $headers); default: throw new \RAP\BadRequestException("Unsupported grant type " . $params['grant_type']); } Loading classes/TokenBuilder.php +24 −0 Original line number Diff line number Diff line Loading @@ -117,6 +117,30 @@ class TokenBuilder { return $audiences; } public function generateToken(array $claims) { $iat = time(); // basic payload $payload = array( 'iss' => $this->locator->config->jwtIssuer, 'iat' => $iat ); // copy claims passed as parameter foreach ($claims as $key => $value) { $payload[$key] = $value; } // set expiration claim if it doesn't exist if (!array_key_exists('exp', $payload)) { $payload['exp'] = $iat + 3600; } $keyPair = $this->locator->getJWKSDAO()->getNewestKeyPair(); return JWT::encode($payload, $keyPair->privateKey, $keyPair->alg, $keyPair->keyId); } /** * @param int $lifespan in hours * @param string $audience target service Loading classes/TokenChecker.php +9 −3 Original line number Diff line number Diff line Loading @@ -26,10 +26,10 @@ class TokenChecker { throw new BadRequestException("Invalid token type"); } return $this->attemptJWTTokenValidation($token); return $this->getValidTokenObject($token); } private function attemptJWTTokenValidation($jwt): object { public function getValidTokenObject(string $jwt): object { $jwtParts = explode('.', $jwt); if (count($jwtParts) === 0) { Loading @@ -47,7 +47,13 @@ class TokenChecker { } try { return JWT::decode($jwt, $keyPair->publicKey, [$keyPair->alg]); $token = JWT::decode($jwt, $keyPair->publicKey, [$keyPair->alg]); if (!isset($token->sub)) { throw new UnauthorizedException("Invalid token: missing subject claim"); } return $token; } catch (\Firebase\JWT\ExpiredException $ex) { throw new UnauthorizedException("Access token is expired"); } Loading classes/TokenExchanger.php +59 −3 Original line number Diff line number Diff line Loading @@ -26,6 +26,9 @@ namespace RAP; use \Firebase\JWT\JWT; /** * See https://tools.ietf.org/html/rfc8693 */ class TokenExchanger { private $locator; Loading @@ -34,9 +37,62 @@ class TokenExchanger { $this->locator = $locator; } public function exchangeToken(string $token) { public function exchangeToken(array $params, array $headers): array { $this->locator->getClientAuthChecker()->validateClientAuth($headers); if ($params['subject_token'] === null) { throw new BadRequestException("subject_token is required"); } if ($params['subject_token_type'] === null) { throw new BadRequestException("subject_token_type is required"); } if (strtolower($params['subject_token_type']) !== 'bearer') { throw new BadRequestException("subject_token_type " . $params['subject_token_type'] . " not supported"); } $subjectToken = $this->locator->getTokenChecker()->getValidTokenObject($params['subject_token']); $claims = array( 'sub' => $subjectToken->sub ); if ($params['resource'] !== null) { $claims['resource'] = $params['resource']; } if ($params['audience'] !== null) { $claims['aud'] = $this->getAudienceClaim($params['audience']); } if ($params['scope'] !== null) { $claims['scope'] = $params['scope']; } $accessToken = $this->locator->getTokenBuilder()->generateToken($claims); $data = []; $data['access_token'] = $accessToken; $data['issued_token_type'] = "urn:ietf:params:oauth:token-type:jwt"; $data['token_type'] = 'Bearer'; return $data; } private function getAudienceClaim($audienceParam) { $audiences = explode(' ', $audienceParam); if (count($audiences) === 1) { // according to RFC 7519 audience can be a single value or an array return $audiences[0]; } return $audiences; } /** * DEPRECATED (currently used by portals: to be removed) */ public function exchangeTokenOld(string $token) { $key = $this->getKeyForToken($token); $key = $this->getExternalKeyForToken($token); $decoded = JWT::decode($token, $key->key, ['RS256']); $subject = $decoded->sub; Loading @@ -52,7 +108,7 @@ class TokenExchanger { return $data; } private function getKeyForToken(string $token): PublicJWK { private function getExternalKeyForToken(string $token): PublicJWK { $keys = $this->locator->getJWKSDAO()->getAllPublicJWK(); Loading Loading
classes/Locator.php +4 −0 Original line number Diff line number Diff line Loading @@ -94,6 +94,10 @@ class Locator { return new ClientAuthChecker($this); } public function getTokenExchanger(): TokenExchanger { return new TokenExchanger($this); } /** * Retrieve the SessionData object from the $_SESSION PHP variable. Create a * new one if it is necessary. Loading
classes/OAuth2RequestHandler.php +2 −0 Original line number Diff line number Diff line Loading @@ -106,6 +106,8 @@ class OAuth2RequestHandler { return $this->handleClientCredentialsRequest($headers); case "refresh_token": return $this->handleRefreshTokenRequest($params, $headers); case "urn:ietf:params:oauth:grant-type:token-exchange": return $this->locator->getTokenExchanger()->exchangeToken($params, $headers); default: throw new \RAP\BadRequestException("Unsupported grant type " . $params['grant_type']); } Loading
classes/TokenBuilder.php +24 −0 Original line number Diff line number Diff line Loading @@ -117,6 +117,30 @@ class TokenBuilder { return $audiences; } public function generateToken(array $claims) { $iat = time(); // basic payload $payload = array( 'iss' => $this->locator->config->jwtIssuer, 'iat' => $iat ); // copy claims passed as parameter foreach ($claims as $key => $value) { $payload[$key] = $value; } // set expiration claim if it doesn't exist if (!array_key_exists('exp', $payload)) { $payload['exp'] = $iat + 3600; } $keyPair = $this->locator->getJWKSDAO()->getNewestKeyPair(); return JWT::encode($payload, $keyPair->privateKey, $keyPair->alg, $keyPair->keyId); } /** * @param int $lifespan in hours * @param string $audience target service Loading
classes/TokenChecker.php +9 −3 Original line number Diff line number Diff line Loading @@ -26,10 +26,10 @@ class TokenChecker { throw new BadRequestException("Invalid token type"); } return $this->attemptJWTTokenValidation($token); return $this->getValidTokenObject($token); } private function attemptJWTTokenValidation($jwt): object { public function getValidTokenObject(string $jwt): object { $jwtParts = explode('.', $jwt); if (count($jwtParts) === 0) { Loading @@ -47,7 +47,13 @@ class TokenChecker { } try { return JWT::decode($jwt, $keyPair->publicKey, [$keyPair->alg]); $token = JWT::decode($jwt, $keyPair->publicKey, [$keyPair->alg]); if (!isset($token->sub)) { throw new UnauthorizedException("Invalid token: missing subject claim"); } return $token; } catch (\Firebase\JWT\ExpiredException $ex) { throw new UnauthorizedException("Access token is expired"); } Loading
classes/TokenExchanger.php +59 −3 Original line number Diff line number Diff line Loading @@ -26,6 +26,9 @@ namespace RAP; use \Firebase\JWT\JWT; /** * See https://tools.ietf.org/html/rfc8693 */ class TokenExchanger { private $locator; Loading @@ -34,9 +37,62 @@ class TokenExchanger { $this->locator = $locator; } public function exchangeToken(string $token) { public function exchangeToken(array $params, array $headers): array { $this->locator->getClientAuthChecker()->validateClientAuth($headers); if ($params['subject_token'] === null) { throw new BadRequestException("subject_token is required"); } if ($params['subject_token_type'] === null) { throw new BadRequestException("subject_token_type is required"); } if (strtolower($params['subject_token_type']) !== 'bearer') { throw new BadRequestException("subject_token_type " . $params['subject_token_type'] . " not supported"); } $subjectToken = $this->locator->getTokenChecker()->getValidTokenObject($params['subject_token']); $claims = array( 'sub' => $subjectToken->sub ); if ($params['resource'] !== null) { $claims['resource'] = $params['resource']; } if ($params['audience'] !== null) { $claims['aud'] = $this->getAudienceClaim($params['audience']); } if ($params['scope'] !== null) { $claims['scope'] = $params['scope']; } $accessToken = $this->locator->getTokenBuilder()->generateToken($claims); $data = []; $data['access_token'] = $accessToken; $data['issued_token_type'] = "urn:ietf:params:oauth:token-type:jwt"; $data['token_type'] = 'Bearer'; return $data; } private function getAudienceClaim($audienceParam) { $audiences = explode(' ', $audienceParam); if (count($audiences) === 1) { // according to RFC 7519 audience can be a single value or an array return $audiences[0]; } return $audiences; } /** * DEPRECATED (currently used by portals: to be removed) */ public function exchangeTokenOld(string $token) { $key = $this->getKeyForToken($token); $key = $this->getExternalKeyForToken($token); $decoded = JWT::decode($token, $key->key, ['RS256']); $subject = $decoded->sub; Loading @@ -52,7 +108,7 @@ class TokenExchanger { return $data; } private function getKeyForToken(string $token): PublicJWK { private function getExternalKeyForToken(string $token): PublicJWK { $keys = $this->locator->getJWKSDAO()->getAllPublicJWK(); Loading