Commit c295f8e7 authored by Sonia Zorba's avatar Sonia Zorba Committed by zonia3000
Browse files

#5 Completed refactoring of access token management. Fixed some issues

parent 5d10d9f6
Loading
Loading
Loading
Loading
+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");
        }
    }

}
+8 −0
Original line number Diff line number Diff line
@@ -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.
+58 −69
Original line number Diff line number Diff line
@@ -2,8 +2,6 @@

namespace RAP;

use \Firebase\JWT\JWT;

class OAuth2RequestHandler {

    private $locator;
@@ -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;
        }

@@ -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");
        }
@@ -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");
        }
@@ -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;
@@ -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);
@@ -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);
    }

}
+14 −12
Original line number Diff line number Diff line
@@ -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);

@@ -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();
        }
@@ -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();

@@ -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);
    }
+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