Skip to content
Snippets Groups Projects
Select Git revision
  • af79b22083198016788f0934c17a701c976420e2
  • main default protected
  • Kelvinrr-patch-5
  • Kelvinrr-patch-4
  • Kelvinrr-patch-3
  • update_release_doc
  • Kelvinrr-patch-2
  • Kelvinrr-patch-1
  • spice_docs
  • ale_testing
  • changelog_docs
  • 1.0.1
  • 1.0.0
13 results

Map Projecting Images.md

Blame
  • 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);
        }
    
    }