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

Implemented client credentials grant type

parent b94e3eeb
No related branches found
No related tags found
No related merge requests found
......@@ -17,6 +17,35 @@ class ClientAuthChecker {
public function validateClientAuth(): void {
$basic = $this->getBasicAuthArray();
$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");
}
}
public function validateCliClientAuth(): CliClient {
$basic = $this->getBasicAuthArray();
$clientId = $basic[0];
$clientSecret = $basic[1];
$client = $this->locator->getOAuth2ClientDAO()->getCliClient($clientId, $clientSecret);
if ($client === null) {
throw new UnauthorizedException("Client '$clientId' not configured or wrong password");
}
return $client;
}
private function getBasicAuthArray(): array {
$headers = apache_request_headers();
if (!isset($headers['Authorization'])) {
......@@ -29,16 +58,7 @@ class ClientAuthChecker {
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");
}
return $basic;
} else {
throw new UnauthorizedException("Expected Basic authorization header");
}
......
......@@ -107,11 +107,7 @@ class JWKSHandler {
$dao->updatePublicJWK($jwk);
}
} else {
$errorMessage = 'Error while retrieving JWKS: ' . curl_error($conn);
error_log($result);
curl_close($conn);
http_response_code(500);
die($errorMessage);
error_log('Error while retrieving JWKS from ' . $url);
}
curl_close($conn);
......
......@@ -91,7 +91,7 @@ class OAuth2RequestHandler {
return $redirectUrl;
}
public function handleAccessTokenRequest($params): array {
public function handleGetTokenFromCodeRequest($params): array {
$this->locator->getClientAuthChecker()->validateClientAuth();
......@@ -123,6 +123,19 @@ class OAuth2RequestHandler {
return $response;
}
public function handleClientCredentialsRequest($params): array {
$client = $this->locator->getClientAuthChecker()->validateCliClientAuth();
$accessTokenData = new AccessTokenData();
$accessTokenData->clientId = $client->id;
$accessTokenData->userId = $client->id;
$accessTokenData->scope = $client->scope;
$accessTokenData->audience = $client->audience;
return $this->getAccessTokenResponse($accessTokenData, false);
}
public function handleRefreshTokenRequest($params): array {
$this->locator->getClientAuthChecker()->validateClientAuth();
......@@ -182,14 +195,16 @@ class OAuth2RequestHandler {
return $scope;
}
private function getAccessTokenResponse(AccessTokenData $tokenData) {
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);
......
......@@ -58,10 +58,16 @@ class TokenBuilder {
$keyPair = $this->locator->getJWKSDAO()->getNewestKeyPair();
$user = $this->locator->getUserDAO()->findUserById($tokenData->userId);
if ($user === null) {
// CLI client
$sub = $tokenData->clientId;
} else {
$sub = $user->id;
}
$payload = array(
'iss' => $this->locator->config->jwtIssuer,
'sub' => strval($user->id),
'sub' => strval($sub),
'iat' => intval($tokenData->creationTime),
'exp' => intval($tokenData->expirationTime),
'aud' => $this->getAudience($tokenData),
......@@ -77,7 +83,15 @@ class TokenBuilder {
private function getAudience(AccessTokenData $tokenData) {
if ($tokenData->audience !== null) {
return $this->getAudienceClaim($tokenData->audience);
}
$client = $this->locator->getOAuth2ClientDAO()->getOAuth2ClientByClientId($tokenData->clientId);
if ($client === null) {
// CLI client without audience
return null;
}
$audiences = [$tokenData->clientId];
......@@ -90,6 +104,10 @@ class TokenBuilder {
}
}
return $this->getAudienceClaim($audiences);
}
private function getAudienceClaim($audiences) {
if (count($audiences) === 1) {
// according to RFC 7519 audience can be a single value or an array
return $audiences[0];
......
......@@ -20,4 +20,6 @@ interface OAuth2ClientDAO {
* the secret, not the database id).
*/
function getOAuth2ClientByClientId($clientId): ?OAuth2Client;
function getCliClient(string $clientId, string $secret): ?CliClient;
}
......@@ -246,4 +246,41 @@ class MySQLOAuth2ClientDAO extends BaseMySQLDAO implements OAuth2ClientDAO {
return $client;
}
function getCliClient(string $clientId, string $secret): ?CliClient {
$dbh = $this->getDBHandler();
// Load clients info
$queryClient = "SELECT scope, audience FROM cli_client WHERE client_id = :client AND client_secret = PASSWORD(:secret)";
$stmtClient = $dbh->prepare($queryClient);
$stmtClient->bindParam(':client', $clientId);
$stmtClient->bindParam(':secret', $secret);
$stmtClient->execute();
$result = $stmtClient->fetchAll();
if (count($result) === 0) {
return null;
}
if (count($result) > 1) {
throw new \Exception("Found multiple clients associated to the same client id!");
}
$row = $result[0];
$client = new CliClient();
$client->id = $clientId;
if ($row['scope'] !== null) {
$client->scope = explode(' ', $row['scope']);
} else {
$client->scope = [];
}
if ($row['audience'] !== null) {
$client->audience = explode(' ', $row['audience']);
} else {
$client->audience = [];
}
return $client;
}
}
......@@ -19,6 +19,7 @@ class AccessTokenData {
public $redirectUri;
public $clientId;
public $scope;
public $audience;
public function __construct() {
$this->creationTime = time();
......
<?php
namespace RAP;
class CliClient {
public $id;
public $scope;
public $audience;
}
......@@ -106,7 +106,10 @@ Flight::route('POST /auth/oauth2/token', function() {
switch ($params['grant_type']) {
case "authorization_code":
$token = $requestHandler->handleAccessTokenRequest($params);
$token = $requestHandler->handleGetTokenFromCodeRequest($params);
break;
case "client_credentials":
$token = $requestHandler->handleClientCredentialsRequest($params);
break;
case "refresh_token":
$token = $requestHandler->handleRefreshTokenRequest($params);
......
......@@ -26,6 +26,14 @@ CREATE TABLE `oauth2_client_scope_audience_mapping` (
FOREIGN KEY (`client_id`) REFERENCES `oauth2_client`(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `cli_client` (
`client_id` varchar(255) NOT NULL,
`client_secret` varchar(255) NOT NULL,
`scope` text,
`audience` text,
PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`primary_identity` bigint(20) DEFAULT NULL,
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment