Skip to content
Snippets Groups Projects
Commit 46229f4e authored by Sonia Zorba's avatar Sonia Zorba
Browse files

Improvements on token exchange implementation

parent 0ba9802b
No related branches found
No related tags found
No related merge requests found
......@@ -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.
......
......@@ -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']);
}
......
......@@ -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
......
......@@ -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");
}
......
......@@ -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();
......
......@@ -99,7 +99,12 @@ Flight::route('POST /auth/oauth2/token', function() {
"code" => filter_input(INPUT_POST, "code", FILTER_SANITIZE_STRING),
"redirect_uri" => filter_input(INPUT_POST, "redirect_uri", FILTER_SANITIZE_STRING),
"refresh_token" => filter_input(INPUT_POST, "refresh_token", FILTER_SANITIZE_STRING),
"scope" => filter_input(INPUT_POST, "scope", FILTER_SANITIZE_STRING)
"scope" => filter_input(INPUT_POST, "scope", FILTER_SANITIZE_STRING),
// For token exchange
"resource" => filter_input(INPUT_POST, "resource", FILTER_SANITIZE_STRING),
"audience" => filter_input(INPUT_POST, "audience", FILTER_SANITIZE_STRING),
"subject_token" => filter_input(INPUT_POST, "subject_token", FILTER_SANITIZE_STRING),
"subject_token_type" => filter_input(INPUT_POST, "subject_token_type", FILTER_SANITIZE_STRING)
];
$headers = apache_request_headers();
......
......@@ -98,5 +98,5 @@ Flight::route('POST ' . $WS_PREFIX . '/exchange', function() {
$exchanger = new \RAP\TokenExchanger($locator);
Flight::json($exchanger->exchangeToken($subjectToken));
Flight::json($exchanger->exchangeTokenOld($subjectToken));
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment