Commit 46229f4e authored by Sonia Zorba's avatar Sonia Zorba
Browse files

Improvements on token exchange implementation

parent 0ba9802b
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -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.
+2 −0
Original line number Diff line number Diff line
@@ -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']);
        }
+24 −0
Original line number Diff line number Diff line
@@ -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
+9 −3
Original line number Diff line number Diff line
@@ -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) {
@@ -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");
        }
+59 −3
Original line number Diff line number Diff line
@@ -26,6 +26,9 @@ namespace RAP;

use \Firebase\JWT\JWT;

/**
 * See https://tools.ietf.org/html/rfc8693
 */
class TokenExchanger {

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