Select Git revision
OAuth2RequestHandler.php
-
Sonia Zorba authoredSonia Zorba authored
OAuth2RequestHandler.php 11.18 KiB
<?php
namespace RAP;
class OAuth2RequestHandler {
private $locator;
public function __construct(\RAP\Locator $locator) {
$this->locator = $locator;
}
public function handleAuthorizeRequest($params) {
if ($params['client_id'] === null) {
throw new BadRequestException("Client id is required");
}
if ($params['redirect_uri'] === null) {
throw new BadRequestException("Redirect URI is required");
}
$client = $this->locator->getBrowserBasedOAuth2ClientById($params['client_id']);
if ($client->redirectUrl !== $params['redirect_uri']) {
throw new BadRequestException("Invalid client redirect URI: " . $params['redirect_uri']);
}
$alg = $params['alg'];
if ($alg === null) {
$alg = "RS256";
}
$state = $params['state'];
$nonce = $params['nonce'];
if ($state === null && $nonce === null) {
throw new BadRequestException("State or nonce is required");
}
// Storing OAuth2 data in session
$oauth2Data = new OAuth2RequestData();
$oauth2Data->clientId = $client->client;
$oauth2Data->redirectUrl = $client->redirectUrl;
$oauth2Data->state = $state;
$oauth2Data->nonce = $nonce;
$scope = $params['scope'];
if ($scope !== null) {
$oauth2Data->scope = explode(' ', $scope);
}
$session = $this->locator->getSession();
$session->setOAuth2RequestData($oauth2Data);
}
public function getRedirectResponseUrl(): string {
$session = $this->locator->getSession();
$code = base64_encode(bin2hex(openssl_random_pseudo_bytes(64)));
$tokenData = new AccessTokenData();
// Code is stored in hashed format inside the database, as a basic
// security measure in order to prevent issues in case of data breach.
$tokenData->codeHash = hash('sha256', $code);
$tokenData->userId = $session->getUser()->id;
$tokenData->clientId = $session->getOAuth2RequestData()->clientId;
$tokenData->redirectUri = $session->getOAuth2RequestData()->redirectUrl;
$tokenData->scope = $session->getOAuth2RequestData()->scope;
$this->locator->getAccessTokenDAO()->createTokenData($tokenData);
$state = $session->getOAuth2RequestData()->state;
$nonce = $session->getOAuth2RequestData()->nonce;
if ($state !== null) {
// Authorization code grant flow
$redirectUrl = $session->getOAuth2RequestData()->redirectUrl
. '?code=' . $code;
$scope = $tokenData->scope;
if ($scope !== null && count($scope) > 0) {
$redirectUrl .= '&scope=' . implode("%20", $scope);
}
$redirectUrl .= '&state=' . $state;
} else {
// Implicit grant flow
$idToken = $this->locator->getTokenBuilder()->getIdToken($tokenData, function(& $jwt) use($nonce) {
$jwt['nonce'] = $nonce;
});
$redirectUrl = $session->getOAuth2RequestData()->redirectUrl . "#id_token=" . $idToken;
}
return $redirectUrl;
}
public function handleAccessTokenRequest(array $params, array $headers): array {
if ($params['grant_type'] === null) {
throw new \RAP\BadRequestException("grant_type is required");
}
switch ($params['grant_type']) {
case "authorization_code":
return $this->handleGetTokenFromCodeRequest($params, $headers);
case "client_credentials":
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']);
}
}
private function handleGetTokenFromCodeRequest(array $params, array $headers): array {
$this->locator->getClientAuthChecker()->validateClientAuth($headers);
if ($params['code'] === null) {
throw new BadRequestException("code id is required");
}
if ($params['redirect_uri'] === null) {
throw new BadRequestException("Redirect URI is required");
}
// Note: theorically the standard wants also the client_id here,
// however some clients don't send it (e.g. Spring Security library)
//
$codeHash = hash('sha256', $params['code']);
$tokenData = $this->locator->getAccessTokenDAO()->retrieveTokenDataFromCode($codeHash);
if ($tokenData === null) {
throw new BadRequestException("No token for given code");
}
if ($tokenData->redirectUri !== $params['redirect_uri']) {
throw new BadRequestException("Invalid redirect URI: " . $params['redirect_uri']);
}
$response = $this->getAccessTokenResponse($tokenData);
$this->locator->getAccessTokenDAO()->deleteTokenData($codeHash);
return $response;
}
private function handleClientCredentialsRequest(array $headers): array {
$client = $this->locator->getClientAuthChecker()->validateCliClientAuth($headers);
$accessTokenData = new AccessTokenData();
$accessTokenData->clientId = $client->id;
$accessTokenData->userId = $client->id;
$accessTokenData->scope = $client->scope;
$accessTokenData->audience = $client->audience;
return $this->getAccessTokenResponse($accessTokenData, false);
}
private function handleRefreshTokenRequest(array $params, array $headers): array {
$this->locator->getClientAuthChecker()->validateClientAuth($headers);
if ($params['refresh_token'] === null) {
throw new BadRequestException("refresh_token is required");
}
$tokenHash = hash('sha256', $params['refresh_token']);
$refreshToken = $this->locator->getRefreshTokenDAO()->getRefreshTokenData($tokenHash);
if ($refreshToken === null || $refreshToken->isExpired()) {
throw new UnauthorizedException("Invalid refresh token");
}
$scope = $this->getScope($params, $refreshToken);
// Generating a new access token
$accessTokenData = new AccessTokenData();
$accessTokenData->clientId = $refreshToken->clientId;
$accessTokenData->userId = $refreshToken->userId;
$accessTokenData->scope = $scope;
$accessTokenData = $this->locator->getAccessTokenDAO()->createTokenData($accessTokenData);
return $this->getAccessTokenResponse($accessTokenData);
}
/**
* We can request a new access token with a scope that is a subset (or the
* same set) of the scope defined for the refresh token.
*/
private function getScope(array $params, RefreshTokenData $refreshToken): ?array {
$scope = $refreshToken->scope;
if ($params['scope'] !== null) {
$newScopeValues = explode(' ', $params['scope']);
foreach ($newScopeValues as $newScopeValue) {
$found = false;
foreach ($scope as $oldScopeValue) {
if ($oldScopeValue === $newScopeValue) {
$found = true;
break;
}
}
if (!$found) {
throw new BadRequestException("Scope " . $newScopeValue . " was not defined for the given refresh token");
}
}
$scope = $newScopeValues;
}
return $scope;
}
private function getAccessTokenResponse(AccessTokenData $tokenData, bool $refreshToken = true) {
$result = [];
$result['access_token'] = $this->locator->getTokenBuilder()->getAccessToken($tokenData);
$result['token_type'] = 'Bearer';
$result['expires_in'] = $tokenData->expirationTime - time();
if ($refreshToken) {
$result['refresh_token'] = $this->buildRefreshToken($tokenData);
}
if ($tokenData->scope !== null && in_array('openid', $tokenData->scope)) {
$result['id_token'] = $this->locator->getTokenBuilder()->getIdToken($tokenData);
}
return $result;
}
/**
* 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(array $headers): array {
$jwt = $this->locator->getTokenChecker()->validateToken();
$tokenData = $this->getTokenDataFromJwtObject($jwt);
$result = [];
$result['exp'] = $tokenData->expirationTime - time();
$result['user_name'] = $tokenData->userId;
$result['client_id'] = $tokenData->clientId;
// copy received access token
$result['access_token'] = explode(" ", $headers['Authorization'])[1];
$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;
}
private function getTokenDataFromJwtObject($jwt): AccessTokenData {
$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 getClientIdFromAudience(object $jwt): string {
if (!(isset($jwt->aud))) {
throw new UnauthorizedException("Missing 'aud' claim in token");
}
$audience = $jwt->aud;
if (is_array($audience)) {
if (count($audience) === 0) {
throw new UnauthorizedException("Token has empty audience");
}
return $audience[0];
}
return $audience;
}
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);
}
}