diff --git a/classes/ClientAuthChecker.php b/classes/ClientAuthChecker.php index 497f9bbc385eb2e8b3845cb6c8532b3474904f4b..89598f8acc49bbea450eb498e73fd93ff2f4458d 100644 --- a/classes/ClientAuthChecker.php +++ b/classes/ClientAuthChecker.php @@ -15,19 +15,19 @@ class ClientAuthChecker { $this->locator = $locator; } - public function validateClientAuth(): void { + public function validateClientAuth(array $headers): BrowserBasedOAuth2Client { - $basic = $this->getBasicAuthArray(); + $basic = $this->getBasicAuthArray($headers); $clientId = $basic[0]; $clientSecret = $basic[1]; - $this->locator->getBrowserBasedOAuth2ClientByIdAndSecret($clientId, $clientSecret); + return $this->locator->getBrowserBasedOAuth2ClientByIdAndSecret($clientId, $clientSecret); } - public function validateCliClientAuth(): CliOAuth2Client { + public function validateCliClientAuth(array $headers): CliOAuth2Client { - $basic = $this->getBasicAuthArray(); + $basic = $this->getBasicAuthArray($headers); $clientId = $basic[0]; $clientSecret = $basic[1]; @@ -35,8 +35,7 @@ class ClientAuthChecker { return $this->locator->getCliClientByIdAndSecret($clientId, $clientSecret); } - private function getBasicAuthArray(): array { - $headers = apache_request_headers(); + private function getBasicAuthArray($headers): array { if (!isset($headers['Authorization'])) { throw new UnauthorizedException("Missing Authorization header"); diff --git a/classes/ClientsLocator.php b/classes/ClientsLocator.php index 5ebe6230fff1552dd4193caba0c408fd84412b73..184f39cf6e21fee6f547883fe5efc5974844f01e 100644 --- a/classes/ClientsLocator.php +++ b/classes/ClientsLocator.php @@ -31,7 +31,7 @@ trait ClientsLocator { private function getClientConfigFromListByIdAndSecret(array $clients, string $clientId, string $secret): object { $client = $this->getClientConfigFromListById($clients, $clientId); $secretHash = hash('sha256', $secret); - if ($client->secretHash !== $secretHash) { + if ($client->secret !== $secretHash) { throw new UnauthorizedException("Wrong secret provided for client '$clientId'"); } return $client; diff --git a/classes/JWKSHandler.php b/classes/JWKSHandler.php index d7598164ce1be87705f729caff54f3969bc53675..2f2d5bc96748730a8a1e592114c5827e1810992b 100644 --- a/classes/JWKSHandler.php +++ b/classes/JWKSHandler.php @@ -79,8 +79,10 @@ class JWKSHandler { public function loadAllJWKS(): array { - foreach ($this->locator->config->jwksUrls as $url) { - $this->loadJWKS($url); + foreach ($this->locator->getBrowserBasedOAuth2Clients() as $client) { + if ($client->jwks !== null) { + $this->loadJWKS($client->jwks); + } } $dao = $this->locator->getJWKSDAO(); diff --git a/classes/Locator.php b/classes/Locator.php index 956b273cc485b26d45af2e6da0d54c6ea75cd4cf..4b34c0c888642f12204180881d88799b3da10fac 100644 --- a/classes/Locator.php +++ b/classes/Locator.php @@ -74,18 +74,10 @@ class Locator { } } - public function getCallbackHandler(): CallbackHandler { - return new CallbackHandler($this); - } - public function getUserHandler(): UserHandler { return new UserHandler($this); } - public function getMailSender(): MailSender { - return new MailSender($_SERVER['HTTP_HOST'], $this->getBasePath()); - } - public function getOAuth2RequestHandler(): OAuth2RequestHandler { return new OAuth2RequestHandler($this); } diff --git a/classes/OAuth2RequestHandler.php b/classes/OAuth2RequestHandler.php index 084d43c2c7f8cb7ef8353cfb73ab14ebc78c240a..2b45d367145fccd8a32d64a99aa4f495748738a4 100644 --- a/classes/OAuth2RequestHandler.php +++ b/classes/OAuth2RequestHandler.php @@ -76,7 +76,12 @@ class OAuth2RequestHandler { if ($state !== null) { // Authorization code grant flow $redirectUrl = $session->getOAuth2RequestData()->redirectUrl - . '?code=' . $code . '&scope=profile&state=' . $state; + . '?code=' . $code; + $scope = $tokenData->scope; + if ($scope !== null && count($scope) > 0) { + $redirectUrl .= '&scope=' . implode("%20", $scope); + } + $redirectUrl .= '&state=' . $state; } else { // Implicit grant flow $idToken = $this->locator->getTokenBuilder()->getIdToken($tokenData, function(& $jwt) use($nonce) { @@ -88,9 +93,27 @@ class OAuth2RequestHandler { return $redirectUrl; } - public function handleGetTokenFromCodeRequest($params): array { + public function handleAccessTokenRequest(array $params, array $headers): array { + + if ($params['grant_type'] === null) { + throw new \RAP\BadRequestException("grant_type is required"); + } + + switch ($params['grant_type']) { + case "authorization_code": + return $this->handleGetTokenFromCodeRequest($params, $headers); + case "client_credentials": + return $this->handleClientCredentialsRequest($headers); + case "refresh_token": + return $this->handleRefreshTokenRequest($params, $headers); + default: + throw new \RAP\BadRequestException("Unsupported grant type " . $params['grant_type']); + } + } + + private function handleGetTokenFromCodeRequest(array $params, array $headers): array { - $this->locator->getClientAuthChecker()->validateClientAuth(); + $this->locator->getClientAuthChecker()->validateClientAuth($headers); if ($params['code'] === null) { throw new BadRequestException("code id is required"); @@ -120,9 +143,9 @@ class OAuth2RequestHandler { return $response; } - public function handleClientCredentialsRequest($params): array { + private function handleClientCredentialsRequest(array $headers): array { - $client = $this->locator->getClientAuthChecker()->validateCliClientAuth(); + $client = $this->locator->getClientAuthChecker()->validateCliClientAuth($headers); $accessTokenData = new AccessTokenData(); $accessTokenData->clientId = $client->id; @@ -133,9 +156,9 @@ class OAuth2RequestHandler { return $this->getAccessTokenResponse($accessTokenData, false); } - public function handleRefreshTokenRequest($params): array { + private function handleRefreshTokenRequest(array $params, array $headers): array { - $this->locator->getClientAuthChecker()->validateClientAuth(); + $this->locator->getClientAuthChecker()->validateClientAuth($headers); if ($params['refresh_token'] === null) { throw new BadRequestException("refresh_token is required"); diff --git a/classes/TokenBuilder.php b/classes/TokenBuilder.php index e64162d936305e9d695c2094a7c5f2d1ad469795..912c13dea92f5e622d6276fcb45138e2d502ae40 100644 --- a/classes/TokenBuilder.php +++ b/classes/TokenBuilder.php @@ -53,7 +53,7 @@ class TokenBuilder { return $payloadArr; } - public function getAccessToken(AccessTokenData $tokenData, \Closure $jwtCustomizer = null) { + public function getAccessToken(AccessTokenData $tokenData, \Closure $jwtCustomizer = null): string { $keyPair = $this->locator->getJWKSDAO()->getNewestKeyPair(); @@ -95,11 +95,13 @@ class TokenBuilder { $audiences = [$tokenData->clientId]; - foreach ($tokenData->scope as $scope) { - if (array_key_exists($scope, $client->scopeAudienceMap)) { - $audience = $client->scopeAudienceMap[$scope]; - if (!in_array($audience, $audiences)) { - array_push($audiences, $audience); + if ($client->scopeAudienceMap !== null) { + foreach ($tokenData->scope as $scope) { + if (array_key_exists($scope, $client->scopeAudienceMap)) { + $audience = $client->scopeAudienceMap[$scope]; + if (!in_array($audience, $audiences)) { + array_push($audiences, $audience); + } } } } diff --git a/config-example.yaml b/config-example.yaml index fcd8cdb97e965b761012abff8f2c7ed7d8b2904b..a850423765872916b1e03d384932a5bb7ed80626 100644 --- a/config-example.yaml +++ b/config-example.yaml @@ -52,7 +52,7 @@ tokenIssuer: clients: - label: "GMS Test (localhost)" id: gms - secret: "XXXXXX" + secret: 2a97516c354b68848cdbd8f54a226a0a55b21ed138e207ad6c5cbb9c00aa5aea redirect: http://localhost:8082/gms/login scope: "openid email" home: http://localhost:8082/gms @@ -62,7 +62,7 @@ clients: jwks: - label: "Asiago Astrophysical Observatory (localhost)" id: aao-dev - secret: "XXXXXX" + secret: 2a97516c354b68848cdbd8f54a226a0a55b21ed138e207ad6c5cbb9c00aa5aea redirect: http://localhost:8081/aao/login scope: "openid read:userspace write:userspace read:fileserver write:fileserver read:gms" home: http://localhost:8081/aao @@ -72,6 +72,6 @@ clients: jwks: http://localhost:8081/aao/jwks cliClients: - id: gms_cli - secret: "XXXXXX" + secret: 2a97516c354b68848cdbd8f54a226a0a55b21ed138e207ad6c5cbb9c00aa5aea scope: "read:gms write:gms read:rap" audience: gms \ No newline at end of file diff --git a/include/front-controller.php b/include/front-controller.php index b7f9b2cd17b324f0fd73ce0a21c356472e82a250..56b2021328db081c0060904d57a757fa96852529 100644 --- a/include/front-controller.php +++ b/include/front-controller.php @@ -102,25 +102,10 @@ Flight::route('POST /auth/oauth2/token', function() { "scope" => filter_input(INPUT_POST, "scope", FILTER_SANITIZE_STRING) ]; - if ($params['grant_type'] === null) { - throw new \RAP\BadRequestException("grant_type is required"); - } + $headers = apache_request_headers(); $requestHandler = new \RAP\OAuth2RequestHandler($locator); - - switch ($params['grant_type']) { - case "authorization_code": - $token = $requestHandler->handleGetTokenFromCodeRequest($params); - break; - case "client_credentials": - $token = $requestHandler->handleClientCredentialsRequest($params); - break; - case "refresh_token": - $token = $requestHandler->handleRefreshTokenRequest($params); - break; - default: - throw new \RAP\BadRequestException("Unsupported grant type " . $params['grant_type']); - } + $token = $requestHandler->handleAccessTokenRequest($params, $headers); Flight::json($token); }); diff --git a/tests/ClientAuthCheckerTest.php b/tests/ClientAuthCheckerTest.php new file mode 100644 index 0000000000000000000000000000000000000000..f98919a951e1d25e2c3f9eb0c46fc59ec0f06385 --- /dev/null +++ b/tests/ClientAuthCheckerTest.php @@ -0,0 +1,33 @@ +<?php + +use PHPUnit\Framework\TestCase; + +class ClientAuthCheckerTest extends TestCase { + + public function testValidateClientAuth() { + + $locatorStub = $this->createMock(\RAP\Locator::class); + + $locatorStub->expects($this->once()) + ->method('getBrowserBasedOAuth2ClientByIdAndSecret')->with('client', 'secret'); + + $authChecker = new \RAP\ClientAuthChecker($locatorStub); + $authChecker->validateClientAuth([ + "Authorization" => "Basic " . base64_encode("client:secret") + ]); + } + + public function testValidateCliClientAuth() { + + $locatorStub = $this->createMock(\RAP\Locator::class); + + $locatorStub->expects($this->once()) + ->method('getCliClientByIdAndSecret')->with('client', 'secret'); + + $authChecker = new \RAP\ClientAuthChecker($locatorStub); + $authChecker->validateCliClientAuth([ + "Authorization" => "Basic " . base64_encode("client:secret") + ]); + } + +} diff --git a/tests/LocatorTest.php b/tests/LocatorTest.php new file mode 100644 index 0000000000000000000000000000000000000000..d33b8c7329fd3bc6d0c492b0352bce26016c78cd --- /dev/null +++ b/tests/LocatorTest.php @@ -0,0 +1,36 @@ +<?php + +use PHPUnit\Framework\TestCase; + +class LocatorTest extends TestCase { + + public function testLocator() { + + define('ROOT', dirname(dirname(__FILE__))); + + $config = yaml_parse_file(ROOT . '/config-example.yaml'); + $config = json_decode(json_encode($config), FALSE); + + $locator = new \RAP\Locator($config); + + $this->assertNotNull($locator->getUserDAO()); + $this->assertNotNull($locator->getJWKSDAO()); + $this->assertNotNull($locator->getAccessTokenDAO()); + $this->assertNotNull($locator->getRefreshTokenDAO()); + $this->assertNotNull($locator->getUserHandler()); + $this->assertNotNull($locator->getOAuth2RequestHandler()); + $this->assertNotNull($locator->getTokenBuilder()); + $this->assertNotNull($locator->getTokenChecker()); + $this->assertNotNull($locator->getClientAuthChecker()); + $this->assertNotNull($locator->getSession()); + $this->assertNotNull($locator->getServiceLogger()); + $this->assertNotNull($locator->getAuditLogger()); + $this->assertNotNull($locator->getJWKSHandler()); + + $this->assertNotNull($locator->getBrowserBasedOAuth2Clients()); + $this->assertNotNull($locator->getBrowserBasedOAuth2ClientById("gms", false)); + $this->assertNotNull($locator->getBrowserBasedOAuth2ClientByIdAndSecret("gms", "demo")); + $this->assertNotNull($locator->getCliClientByIdAndSecret("gms_cli", "demo")); + } + +} diff --git a/tests/OAuth2RequestHandlerTest.php b/tests/OAuth2RequestHandlerTest.php index f970eede895acc7deb2992b96d721b3b8f45b9e8..16cd7235f3489f522d67a2b78a1f41982c52b067 100644 --- a/tests/OAuth2RequestHandlerTest.php +++ b/tests/OAuth2RequestHandlerTest.php @@ -105,4 +105,151 @@ final class OAuth2RequestHandlerTest extends TestCase { $this->assertEquals('id-token', $result['id_token']); } + public function testHandleGetTokenFromCodeRequest(): void { + + $authCheckerStub = $this->createMock(\RAP\ClientAuthChecker::class); + + $tokenData = new \RAP\AccessTokenData(); + $tokenData->redirectUri = "redirect"; + $tokenData->scope = ['openid']; + + $tokenDaoStub = $this->createMock(\RAP\AccessTokenDAO::class); + $tokenDaoStub->method('retrieveTokenDataFromCode')->willReturn($tokenData); + + $tokenBuilderStub = $this->createMock(\RAP\TokenBuilder::class); + $tokenBuilderStub->method('getAccessToken')->willReturn('<access_token>'); + + $refreshTokenDaoStub = $this->createMock(\RAP\RefreshTokenDAO::class); + + $locatorStub = $this->createMock(\RAP\Locator::class); + $locatorStub->method('getClientAuthChecker')->willReturn($authCheckerStub); + $locatorStub->method('getAccessTokenDAO')->willReturn($tokenDaoStub); + $locatorStub->method('getTokenBuilder')->willReturn($tokenBuilderStub); + $locatorStub->method('getRefreshTokenDAO')->willReturn($refreshTokenDaoStub); + + $authCheckerStub->expects($this->once()) + ->method('validateClientAuth')->with($this->anything()); + + $tokenDaoStub->expects($this->once()) + ->method('retrieveTokenDataFromCode')->with($this->anything()); + + $tokenDaoStub->expects($this->once()) + ->method('deleteTokenData')->with($this->anything()); + + $requestHandler = new \RAP\OAuth2RequestHandler($locatorStub); + + $params = [ + "grant_type" => "authorization_code", + "redirect_uri" => "redirect", + "code" => "123" + ]; + $result = $requestHandler->handleAccessTokenRequest($params, []); + + $this->assertEquals(3600, $result['expires_in']); + $this->assertEquals("Bearer", $result['token_type']); + $this->assertEquals("<access_token>", $result['access_token']); + $this->assertNotNull($result['refresh_token']); + } + + public function testHandleClientCredentialsRequest(): void { + + $authCheckerStub = $this->createMock(\RAP\ClientAuthChecker::class); + + $tokenBuilderStub = $this->createMock(\RAP\TokenBuilder::class); + $tokenBuilderStub->method('getAccessToken')->willReturn('<access_token>'); + + $locatorStub = $this->createMock(\RAP\Locator::class); + $locatorStub->method('getClientAuthChecker')->willReturn($authCheckerStub); + $locatorStub->method('getTokenBuilder')->willReturn($tokenBuilderStub); + + $authCheckerStub->expects($this->once()) + ->method('validateCliClientAuth')->with($this->anything()); + + $requestHandler = new \RAP\OAuth2RequestHandler($locatorStub); + + $params = [ + "grant_type" => "client_credentials" + ]; + $result = $requestHandler->handleAccessTokenRequest($params, []); + + $this->assertEquals(3600, $result['expires_in']); + $this->assertEquals("Bearer", $result['token_type']); + $this->assertEquals("<access_token>", $result['access_token']); + $this->assertFalse(isset($result['refresh_token'])); + } + + public function testHandleRefreshTokenRequest(): void { + + $refreshTokenData = new \RAP\RefreshTokenData(); + $refreshTokenData->scope = ['openid', 'email']; + + $authCheckerStub = $this->createMock(\RAP\ClientAuthChecker::class); + + $tokenBuilderStub = $this->createMock(\RAP\TokenBuilder::class); + $tokenBuilderStub->method('getAccessToken')->willReturn('<access_token>'); + + $refreshTokenDaoStub = $this->createMock(\RAP\RefreshTokenDAO::class); + $refreshTokenDaoStub->method('getRefreshTokenData')->willReturn($refreshTokenData); + + $tokenDaoStub = $this->createMock(\RAP\AccessTokenDAO::class); + $tokenDaoStub->method('createTokenData')->will($this->returnArgument(0)); + + $locatorStub = $this->createMock(\RAP\Locator::class); + $locatorStub->method('getClientAuthChecker')->willReturn($authCheckerStub); + $locatorStub->method('getTokenBuilder')->willReturn($tokenBuilderStub); + $locatorStub->method('getRefreshTokenDAO')->willReturn($refreshTokenDaoStub); + $locatorStub->method('getAccessTokenDAO')->willReturn($tokenDaoStub); + + $authCheckerStub->expects($this->once()) + ->method('validateClientAuth')->with($this->anything()); + + $requestHandler = new \RAP\OAuth2RequestHandler($locatorStub); + + $params = [ + "grant_type" => "refresh_token", + "refresh_token" => "<refresh_token>", + "scope" => "openid email" + ]; + $result = $requestHandler->handleAccessTokenRequest($params, []); + + $this->assertEquals(3600, $result['expires_in']); + $this->assertEquals("Bearer", $result['token_type']); + $this->assertEquals("<access_token>", $result['access_token']); + $this->assertNotNull($result['refresh_token']); + } + + public function testGetRedirectResponseUrlForAuthorizationCodeFlow(): void { + + $user = new \RAP\User(); + $user->id = "123"; + + $requestData = new \RAP\OAuth2RequestData(); + $requestData->clientId = "<client-id>"; + $requestData->redirectUrl = "<base-path>"; + $requestData->scope = ["openid", "profile"]; + $requestData->state = "<state>"; + + $sessionStub = $this->createMock(\RAP\SessionData::class); + $sessionStub->method('getUser')->willReturn($user); + $sessionStub->method('getOAuth2RequestData')->willReturn($requestData); + + $locatorStub = $this->createMock(\RAP\Locator::class); + $locatorStub->method('getSession')->willReturn($sessionStub); + + $requestHandler = new \RAP\OAuth2RequestHandler($locatorStub); + + $result = $requestHandler->getRedirectResponseUrl(); + + $url_components = parse_url($result); + $this->assertEquals('<base-path>', $url_components['path']); + + $params = []; + parse_str($url_components['query'], $params); + + $this->assertEquals("<state>", $params['state']); + $this->assertEquals("openid profile", $params['scope']); + + $this->assertNotNull($params['code']); + } + } diff --git a/tests/TokenBuilderTest.php b/tests/TokenBuilderTest.php index 61618878dd51cebe6623b302bf67e93ab09c6bfc..8e1636da844edf847325e48de627c2bc17e7b1b1 100644 --- a/tests/TokenBuilderTest.php +++ b/tests/TokenBuilderTest.php @@ -17,15 +17,7 @@ final class TokenBuilderTest extends TestCase { $jwksDAOStub->method('getNewestKeyPair')->willReturn($keyPair); - $user = new \RAP\User(); - $user->id = "user_id"; - $identity = new \RAP\Identity(\RAP\Identity::EDU_GAIN); - $identity->email = "name@inaf.it"; - $identity->name = "Name"; - $identity->surname = "Surname"; - $identity->primary = true; - $identity->institution = "INAF"; - $user->addIdentity($identity); + $user = $this->getUser(); $daoStub = $this->createMock(\RAP\UserDAO::class); $locatorStub->method('getUserDAO')->willReturn($daoStub); @@ -48,9 +40,67 @@ final class TokenBuilderTest extends TestCase { $this->assertEquals("issuer", $payload->iss); $this->assertEquals($user->id, $payload->sub); $this->assertEquals($user->getCompleteName(), $payload->name); + $identity = $user->identities[0]; $this->assertEquals($identity->name, $payload->given_name); $this->assertEquals($identity->surname, $payload->family_name); $this->assertEquals($identity->institution, $payload->org); } + public function testGetAccessToken(): void { + + $jwksDAOStub = $this->createMock(\RAP\JWKSDAO::class); + + $locatorStub = $this->createMock(\RAP\Locator::class); + $locatorStub->method('getJWKSDAO')->willReturn($jwksDAOStub); + + $jwksHandler = new \RAP\JWKSHandler($locatorStub); + $keyPair = $jwksHandler->generateKeyPair(); + + $jwksDAOStub->method('getNewestKeyPair')->willReturn($keyPair); + + $userDaoStub = $this->createMock(\RAP\UserDAO::class); + $locatorStub->method('getUserDAO')->willReturn($userDaoStub); + + $user = $this->getUser(); + $userDaoStub->method('findUserById')->willReturn($user); + + $client = new \RAP\BrowserBasedOAuth2Client((object) [ + "id" => "aao", + "secret" => "2a97516c354b68848cdbd8f54a226a0a55b21ed138e207ad6c5cbb9c00aa5aea", + "redirect" => "redirect", + "scope" => "openid email", + "methods" => [], + "scopeAudienceMap" => ["read:gms" => "gms"] + ]); + + $locatorStub->method('getBrowserBasedOAuth2ClientById')->willReturn($client); + + $locatorStub->config = json_decode('{"jwtIssuer": "issuer"}'); + + $tokenBuilder = new \RAP\TokenBuilder($locatorStub); + + $tokenData = new \RAP\AccessTokenData(); + $tokenData->token = "ttt"; + $tokenData->scope = ["openid", "read:gms"]; + $tokenData->userId = "user_id"; + $tokenData->clientId = "aao"; + + $jwt = $tokenBuilder->getAccessToken($tokenData); + + $this->assertNotNull($jwt); + } + + private function getUser(): \RAP\User { + $user = new \RAP\User(); + $user->id = "user_id"; + $identity = new \RAP\Identity(\RAP\Identity::EDU_GAIN); + $identity->email = "name@inaf.it"; + $identity->name = "Name"; + $identity->surname = "Surname"; + $identity->primary = true; + $identity->institution = "INAF"; + $user->addIdentity($identity); + return $user; + } + }