diff --git a/README.md b/README.md index c0eb7bb..ca4564a 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,8 @@ Example: $keycloak = new \Fschmtt\Keycloak\Keycloak( baseUrl: 'http://keycloak:8080', username: 'admin', - password: 'admin' + password: 'admin', + realm: 'master' ); $serverInfo = $keycloak->serverInfo()->get(); @@ -51,6 +52,8 @@ will print e.g. Keycloak 26.0.0 is running on Linux/5.10.25-linuxkit (amd64) with OpenJDK 64-Bit Server VM/11.0.11 since 0 days, 2 hours, 37 minutes, 7 seconds and is currently using 139 MB of 512 MB (28 %) memory. ``` +You can authenticate against a specific realm by passing it via the `realm` parameter when constructing the `Keycloak` instance. + More examples can be found in the [examples](examples) directory. ## Customization @@ -163,23 +166,27 @@ $myCustomRepresentation = $myCustomResource->myCustomEndpoint(); ### [Users](https://www.keycloak.org/docs-api/26.0.0/rest-api/index.html#_users) -| Endpoint | Response | API | -|---------------------------------------------------------|-----------------------------------------------------------------|----------------------------------------------------------------| -| `GET /admin/realms/{realm}/users` | [UserCollection](src/Collection/UserCollection.php) | [Users::all()](src/Resource/Users.php) | -| `POST /admin/realms/{realm}/users` | `n/a` | [Users::create()](src/Resource/Users.php) | -| `GET /admin/realms/{realm}/users/{userId}` | [User](src/Representation/User.php) | [Users::get()](src/Resource/Users.php) | -| `PUT /admin/realms/{realm}/users/{userId}` | `n/a` | [Users::update()](src/Resource/Users.php) | -| `DELETE /admin/realms/{realm}/users/{userId}` | `n/a` | [Users::delete()](src/Resource/Users.php) | -| `GET /admin/realms/{realm}/users` | [UserCollection](src/Collection/UserCollection.php) | [Users::search()](src/Resource/Users.php) | -| `PUT /{realm}/users/{id}/groups/{groupId}` | `n/a` | [Users::joinGroup()](src/Resource/Users.php) | -| `DELETE /{realm}/users/{id}/groups/{groupId}` | `n/a` | [Users::leaveGroup()](src/Resource/Users.php) | -| `GET /{realm}/users/{id}/groups` | [GroupCollection](src/Collection/GroupCollection.php) | [Users::retrieveGroups()](src/Resource/Users.php) | -| `GET /{realm}/users/{id}/role-mappings/realm` | [RoleCollection](src/Collection/RoleCollection.php) | [Users::retrieveRealmRoles()](src/Resource/Users.php) | -| `GET /{realm}/users/{id}/role-mappings/realm/available` | [RoleCollection](src/Collection/RoleCollection.php) | [Users::retrieveAvailableRealmRoles()](src/Resource/Users.php) | -| `POST /{realm}/users/{id}/role-mappings/realm` | `n/a` | [Users::addRealmRoles()](src/Resource/Users.php) | -| `DELETE /{realm}/users/{id}/role-mappings/realm` | `n/a` | [Users::removeRealmRoles()](src/Resource/Users.php) | -| `PUT /{realm}/users/{id}/execute-actions-email` | `n/a` | [Users::executeActionsEmail()](src/Resource/Users.php) | -| `GET /admin/realms/{realm}/users/{userId}/credentials` | [CredentialCollection](src/Collection/CredentialCollection.php) | [Users::credentials()](src/Resource/Users.php) | +| Endpoint | Response | API | +|--------------------------------------------------------------------------------------|-----------------------------------------------------------------|----------------------------------------------------------------| +| `GET /admin/realms/{realm}/users` | [UserCollection](src/Collection/UserCollection.php) | [Users::all()](src/Resource/Users.php) | +| `POST /admin/realms/{realm}/users` | `n/a` | [Users::create()](src/Resource/Users.php) | +| `GET /admin/realms/{realm}/users/{userId}` | [User](src/Representation/User.php) | [Users::get()](src/Resource/Users.php) | +| `PUT /admin/realms/{realm}/users/{userId}` | `n/a` | [Users::update()](src/Resource/Users.php) | +| `DELETE /admin/realms/{realm}/users/{userId}` | `n/a` | [Users::delete()](src/Resource/Users.php) | +| `GET /admin/realms/{realm}/users` | [UserCollection](src/Collection/UserCollection.php) | [Users::search()](src/Resource/Users.php) | +| `PUT /admin/realms/{realm}/users/{userId}/groups/{groupId}` | `n/a` | [Users::joinGroup()](src/Resource/Users.php) | +| `DELETE /admin/realms/{realm}/users/{userId}/groups/{groupId}` | `n/a` | [Users::leaveGroup()](src/Resource/Users.php) | +| `GET /admin/realms/{realm}/users/{userId}/groups` | [GroupCollection](src/Collection/GroupCollection.php) | [Users::retrieveGroups()](src/Resource/Users.php) | +| `GET /admin/realms/{realm}/users/{userId}/role-mappings/realm` | [RoleCollection](src/Collection/RoleCollection.php) | [Users::retrieveRealmRoles()](src/Resource/Users.php) | +| `GET /admin/realms/{realm}/users/{userId}/role-mappings/realm/available` | [RoleCollection](src/Collection/RoleCollection.php) | [Users::retrieveAvailableRealmRoles()](src/Resource/Users.php) | +| `POST /admin/realms/{realm}/users/{userId}/role-mappings/realm` | `n/a` | [Users::addRealmRoles()](src/Resource/Users.php) | +| `DELETE /admin/realms/{realm}/users/{userId}/role-mappings/realm` | `n/a` | [Users::removeRealmRoles()](src/Resource/Users.php) | +| `GET /admin/realms/{realm}/users/{userId}/role-mappings/clients/{clientUuid}` | [RoleCollection](src/Collection/RoleCollection.php) | [Users::retrieveClientRoles()](src/Resource/Users.php) | +| `GET /admin/realms/{realm}/users/{userId}/role-mappings/clients/{clientUuid}/available` | [RoleCollection](src/Collection/RoleCollection.php) | [Users::retrieveAvailableClientRoles()](src/Resource/Users.php) | +| `POST /admin/realms/{realm}/users/{userId}/role-mappings/clients/{clientUuid}` | `n/a` | [Users::addClientRoles()](src/Resource/Users.php) | +| `DELETE /admin/realms/{realm}/users/{userId}/role-mappings/clients/{clientUuid}` | `n/a` | [Users::removeClientRoles()](src/Resource/Users.php) | +| `PUT /admin/realms/{realm}/users/{userId}/execute-actions-email` | `n/a` | [Users::executeActionsEmail()](src/Resource/Users.php) | +| `GET /admin/realms/{realm}/users/{userId}/credentials` | [CredentialCollection](src/Collection/CredentialCollection.php) | [Users::credentials()](src/Resource/Users.php) | ### [Roles](https://www.keycloak.org/docs-api/26.0.0/rest-api/index.html#_roles) diff --git a/src/Http/Client.php b/src/Http/Client.php index d67d8c6..ef60c15 100644 --- a/src/Http/Client.php +++ b/src/Http/Client.php @@ -71,7 +71,7 @@ private function fetchTokens(): array try { $response = $this->httpClient->request( 'POST', - $this->keycloak->getBaseUrl() . '/realms/master/protocol/openid-connect/token', + $this->keycloak->getBaseUrl() . '/realms/' . $this->keycloak->getRealm() . '/protocol/openid-connect/token', [ 'form_params' => [ 'refresh_token' => $this->tokenStorage->retrieveRefreshToken()?->toString(), @@ -83,7 +83,7 @@ private function fetchTokens(): array } catch (ClientException $e) { $response = $this->httpClient->request( 'POST', - $this->keycloak->getBaseUrl() . '/realms/master/protocol/openid-connect/token', + $this->keycloak->getBaseUrl() . '/realms/' . $this->keycloak->getRealm() . '/protocol/openid-connect/token', [ 'form_params' => [ 'username' => $this->keycloak->getUsername(), diff --git a/src/Keycloak.php b/src/Keycloak.php index e156cde..5f05a26 100644 --- a/src/Keycloak.php +++ b/src/Keycloak.php @@ -37,6 +37,7 @@ public function __construct( private readonly string $baseUrl, private readonly string $username, private readonly string $password, + private readonly string $realm = 'master', private readonly TokenStorageInterface $tokenStorage = new InMemory(), ?ClientInterface $guzzleClient = new GuzzleClient(), ) { @@ -144,4 +145,9 @@ private function fetchVersion(): void $this->serializer = new Serializer($this->version); $this->commandExecutor = new CommandExecutor($this->client, $this->serializer); } + + public function getRealm(): string + { + return $this->realm; + } } diff --git a/tests/Unit/Http/ClientTest.php b/tests/Unit/Http/ClientTest.php index 7189bbc..fdb433a 100644 --- a/tests/Unit/Http/ClientTest.php +++ b/tests/Unit/Http/ClientTest.php @@ -12,6 +12,11 @@ use GuzzleHttp\ClientInterface; use GuzzleHttp\Exception\ClientException; use GuzzleHttp\Psr7\Response; +use GuzzleHttp\Psr7\Request; +use GuzzleHttp\Client as GuzzleClient; +use GuzzleHttp\Handler\MockHandler; +use GuzzleHttp\HandlerStack; +use GuzzleHttp\Middleware; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; @@ -114,4 +119,75 @@ public function testIsAuthorizedIfTokenStorageContainsUnexpiredAccessToken(): vo static::assertTrue($client->isAuthorized()); } + + public function testAuthenticatesUsingConfiguredRealm(): void + { + $accessToken = $this->generateToken((new DateTimeImmutable())->modify('+1 hour')); + $refreshToken = $this->generateToken((new DateTimeImmutable())->modify('+1 hour')); + + $authorizationResponse = new Response( + status: 200, + body: json_encode( + value: [ + 'access_token' => $accessToken->toString(), + 'refresh_token' => $refreshToken->toString(), + ], + flags: JSON_THROW_ON_ERROR, + ), + ); + + $realmsResponse = new Response( + status: 200, + body: json_encode( + value: [ + 'realms' => [], + ], + flags: JSON_THROW_ON_ERROR, + ), + ); + + $history = []; + $historyMiddleware = Middleware::history($history); + + $tokenRequest = new Request('POST', 'http://keycloak:8080/realms/custom-realm/protocol/openid-connect/token'); + $mockHandler = new MockHandler([ + new ClientException('Bad Request', $tokenRequest, new Response(400)), + $authorizationResponse, + $realmsResponse, + ]); + + $handlerStack = HandlerStack::create($mockHandler); + $handlerStack->push($historyMiddleware); + + $httpClient = new GuzzleClient(['handler' => $handlerStack]); + + $keycloak = new Keycloak( + 'http://keycloak:8080', + 'admin', + 'admin', + realm: 'custom-realm', + ); + + $client = new Client($keycloak, $httpClient, new InMemoryTokenStorage()); + + $client->request('GET', '/admin/realms'); + + // Verify that any token endpoint call uses the configured realm in the URL + static::assertNotEmpty($history); + $tokenCalls = array_values(array_filter((array) $history, function (array $transaction): bool { + /** @var Request $request */ + $request = $transaction['request']; + return str_contains((string) $request->getUri(), '/protocol/openid-connect/token'); + })); + static::assertNotEmpty($tokenCalls); + foreach ($tokenCalls as $transaction) { + /** @var Request $request */ + $request = $transaction['request']; + $authUri = (string) $request->getUri(); + static::assertStringContainsString('/realms/custom-realm/', $authUri); + static::assertStringNotContainsString('/realms/master/', $authUri); + } + + static::assertTrue($client->isAuthorized()); + } }