Skip to content
Snippets Groups Projects
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
No related branches found
No related tags found
No related merge requests found
<?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");
}
}
}
......@@ -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.
......
......@@ -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);
}
}
......@@ -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);
}
......
<?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");
}
}
......@@ -71,7 +71,6 @@ class UserHandler {
// Call Grouper for moving groups and privileges from one user to the other
if (isset($this->locator->config->gms)) {
// TODO: change with new GMS
//create cURL connection
$conn = curl_init($this->locator->config->gms->joinEndpoint);
......@@ -81,7 +80,7 @@ class UserHandler {
curl_setopt($conn, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($conn, CURLOPT_FOLLOWLOCATION, 1);
curl_setopt($conn, CURLOPT_HTTPHEADER, ['Authorization: Bearer '
. $this->getJoinIdToken($userId1, $userId2)]);
. $this->getJoinAccessToken($userId1, $userId2)]);
//set data to be posted
curl_setopt($conn, CURLOPT_POST, 1);
......@@ -113,19 +112,20 @@ class UserHandler {
return $user1;
}
private function getJoinIdToken(int $userId1, int $userId2): string {
private function getJoinAccessToken(int $userId1, int $userId2): string {
$gmsId = $this->locator->config->gms->id;
$accessToken = new AccessToken();
$accessToken = new AccessTokenData();
$accessToken->clientId = $gmsId;
$accessToken->userId = $userId1;
$accessToken->joinUser = $userId2;
// shorter expiration
$accessToken->expirationTime = $accessToken->creationTime + 100;
$accessToken->scope = ['openid'];
return $this->locator->getTokenBuilder()->getIdToken($accessToken);
return $this->locator->getTokenBuilder()->getAccessToken($accessToken, function(& $jwt) use($userId2) {
$jwt['alt_sub'] = strval($userId2);
});
}
}
......@@ -24,7 +24,7 @@ class MySQLOAuth2ClientDAO extends BaseMySQLDAO implements OAuth2ClientDAO {
return $clients;
}
private function getClientsMap(PDO $dbh): array {
private function getClientsMap(\PDO $dbh): array {
// Load clients info
$queryClient = "SELECT id, title, icon, client, secret, redirect_url, scope, home_page, show_in_home FROM oauth2_client";
......@@ -50,7 +50,7 @@ class MySQLOAuth2ClientDAO extends BaseMySQLDAO implements OAuth2ClientDAO {
return $clientsMap;
}
private function loadAuthenticationMethods(PDO $dbh, array $clientsMap): void {
private function loadAuthenticationMethods(\PDO $dbh, array $clientsMap): void {
$queryAuthNMethods = "SELECT client_id, auth_method FROM oauth2_client_auth_methods";
......@@ -63,7 +63,7 @@ class MySQLOAuth2ClientDAO extends BaseMySQLDAO implements OAuth2ClientDAO {
}
}
private function loadScopeAudienceMapping(PDO $dbh, array $clientsMap): void {
private function loadScopeAudienceMapping(\PDO $dbh, array $clientsMap): void {
$query = "SELECT client_id, scope, audience FROM oauth2_client_scope_audience_mapping";
......
......@@ -85,7 +85,6 @@ class LoginHandler {
$action = $session->getAction();
if ($action === 'join') {
if ($session->getUser()->id !== $user->id) {
$user = $this->locator->getUserHandler()->joinUsers($session->getUser(), $user);
}
......
......@@ -49,11 +49,6 @@ Flight::route('/', function() {
$authPageModel = new \RAP\AuthPageModel($locator, $client);
renderMainPage($authPageModel);
break;
/*case "admin":
$client = new \RAP\InternalClient('admin');
$authPageModel = new \RAP\AuthPageModel($locator, $client);
renderMainPage($authPageModel);
break;*/
default:
session_destroy();
$clients = $locator->getOAuth2ClientDAO()->getOAuth2Clients();
......@@ -127,25 +122,8 @@ Flight::route('POST /auth/oauth2/check_token', function() {
global $locator;
$headers = apache_request_headers();
if (!isset($headers['Authorization'])) {
throw new \RAP\BadRequestException("Missing Authorization header");
}
$authorizationHeader = explode(" ", $headers['Authorization']);
if ($authorizationHeader[0] === "Bearer") {
$token = $authorizationHeader[1];
} else {
throw new \RAP\BadRequestException("Invalid token type");
}
if ($token === null) {
throw new \RAP\BadRequestException("Access token is required");
}
$requestHandler = new \RAP\OAuth2RequestHandler($locator);
$result = $requestHandler->handleCheckTokenRequest($token);
$result = $requestHandler->handleCheckTokenRequest();
Flight::json($result);
});
......
......@@ -13,7 +13,8 @@ Flight::route('GET ' . $WS_PREFIX . '/user/@userId', function($userId) {
global $locator;
$locator->getOAuth2RequestHandler()->validateToken();
$token = $locator->getTokenChecker()->validateToken();
$locator->getTokenChecker()->checkScope($token, 'read:rap');
$user = $locator->getUserDAO()->findUserById($userId);
if ($user !== null) {
......@@ -31,7 +32,8 @@ Flight::route('GET ' . $WS_PREFIX . '/user', function() {
global $locator;
$locator->getOAuth2RequestHandler()->validateToken();
$token = $locator->getTokenChecker()->validateToken();
$locator->getTokenChecker()->checkScope($token, 'read:rap');
$searchText = Flight::request()->query['search'];
if ($searchText !== null) {
......@@ -51,13 +53,14 @@ Flight::route('GET ' . $WS_PREFIX . '/user', function() {
/**
* Create new user from identity data. Return the new user encoded in JSON.
* This can be used to automatically import users without they explicitly
* register (this is done for INAF eduGAIN users readling directly from LDAP).
* register (this is done for INAF eduGAIN users reading directly from LDAP).
*/
Flight::route('POST ' . $WS_PREFIX . '/user', function() {
global $locator;
$locator->getOAuth2RequestHandler()->validateToken();
$token = $locator->getTokenChecker()->validateToken();
$locator->getTokenChecker()->checkScope($token, 'write:rap');
$postData = Flight::request()->data;
......
......@@ -20,7 +20,7 @@ include 'include/header.php';
<div class="col-sm-2">
<div class="row">
<div class="col-sm-12">
<a class="btn btn-success disabled" id="join-btn" href="<?php echo $contextRoot; ?>?action=join" title="Perform an additional login to join your identities" data-toggle="tooltip" data-placement="bottom">
<a class="btn btn-success" id="join-btn" href="<?php echo $contextRoot; ?>?action=join" title="Perform an additional login to join your identities" data-toggle="tooltip" data-placement="bottom">
Join with another identity
</a>
</div>
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment