diff --git a/classes/ClientAuthChecker.php b/classes/ClientAuthChecker.php index cacb8e3ceee6adb1e253cdcaaa899a76958c240f..8bb33e2fdd76623d02559202b51209dab079d099 100644 --- a/classes/ClientAuthChecker.php +++ b/classes/ClientAuthChecker.php @@ -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"); } diff --git a/classes/JWKSHandler.php b/classes/JWKSHandler.php index 966873fe7cfc8cd27225f19d1218c0cf5c874b2b..d7598164ce1be87705f729caff54f3969bc53675 100644 --- a/classes/JWKSHandler.php +++ b/classes/JWKSHandler.php @@ -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); diff --git a/classes/OAuth2RequestHandler.php b/classes/OAuth2RequestHandler.php index 12da4751b44284126ad32fe8364d3072984e844d..8adc55158db553395669c8bbaf91a969cad3c5c4 100644 --- a/classes/OAuth2RequestHandler.php +++ b/classes/OAuth2RequestHandler.php @@ -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(); - $result['refresh_token'] = $this->buildRefreshToken($tokenData); + 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); diff --git a/classes/TokenBuilder.php b/classes/TokenBuilder.php index d42103f813787875793ad5bf4e8eac05a2602766..27bf313791e01bbe4e4d2b766e7b4f84c50643b5 100644 --- a/classes/TokenBuilder.php +++ b/classes/TokenBuilder.php @@ -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]; diff --git a/classes/datalayer/OAuth2ClientDAO.php b/classes/datalayer/OAuth2ClientDAO.php index ffa4520b21fb64a213166d854de9f104776e4ba1..944ff6e7589e0a9ccb90844e7066a744782f23d0 100644 --- a/classes/datalayer/OAuth2ClientDAO.php +++ b/classes/datalayer/OAuth2ClientDAO.php @@ -20,4 +20,6 @@ interface OAuth2ClientDAO { * the secret, not the database id). */ function getOAuth2ClientByClientId($clientId): ?OAuth2Client; + + function getCliClient(string $clientId, string $secret): ?CliClient; } diff --git a/classes/datalayer/mysql/MySQLOAuth2ClientDAO.php b/classes/datalayer/mysql/MySQLOAuth2ClientDAO.php index 7890e95c8ee1b5b48fa4814a1f03a81d81c7070a..c005b692b15461025ed51790ce3e756c130222d8 100644 --- a/classes/datalayer/mysql/MySQLOAuth2ClientDAO.php +++ b/classes/datalayer/mysql/MySQLOAuth2ClientDAO.php @@ -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; + } + } diff --git a/classes/model/AccessTokenData.php b/classes/model/AccessTokenData.php index 3826e22a14a040dceb6464a8b2969f58c891e9e8..a73d06b359a4df37ec0ac8aad93c3b5b35a64943 100644 --- a/classes/model/AccessTokenData.php +++ b/classes/model/AccessTokenData.php @@ -19,6 +19,7 @@ class AccessTokenData { public $redirectUri; public $clientId; public $scope; + public $audience; public function __construct() { $this->creationTime = time(); diff --git a/classes/model/CliClient.php b/classes/model/CliClient.php new file mode 100644 index 0000000000000000000000000000000000000000..d10f719177f276a71e4cc62a7dcd41ced7ecca32 --- /dev/null +++ b/classes/model/CliClient.php @@ -0,0 +1,11 @@ +<?php + +namespace RAP; + +class CliClient { + + public $id; + public $scope; + public $audience; + +} diff --git a/include/front-controller.php b/include/front-controller.php index 7f5714b068e82e90757494ae90f34ab3c872e29c..540a16d929acb033c2ab0fb829c5e3870bc606f3 100644 --- a/include/front-controller.php +++ b/include/front-controller.php @@ -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); diff --git a/sql/setup-database.sql b/sql/setup-database.sql index 5b3842b0e1b0db1004bbb5b76e743c1eb605e5f8..6e8b7f6e3b5e9ddb6a6b021114f7791c52aad3d3 100644 --- a/sql/setup-database.sql +++ b/sql/setup-database.sql @@ -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,