Skip to content

Commit c76c1c5

Browse files
authored
Merge pull request #163 from phillipfickl/make-auth-realm-configurable
Add configurable realm support for token authentication
2 parents c33a25f + 9c9e799 commit c76c1c5

4 files changed

Lines changed: 109 additions & 20 deletions

File tree

README.md

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ Example:
2525
$keycloak = new \Fschmtt\Keycloak\Keycloak(
2626
baseUrl: 'http://keycloak:8080',
2727
username: 'admin',
28-
password: 'admin'
28+
password: 'admin',
29+
realm: 'master'
2930
);
3031

3132
$serverInfo = $keycloak->serverInfo()->get();
@@ -51,6 +52,8 @@ will print e.g.
5152
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.
5253
```
5354

55+
You can authenticate against a specific realm by passing it via the `realm` parameter when constructing the `Keycloak` instance.
56+
5457
More examples can be found in the [examples](examples) directory.
5558

5659
## Customization
@@ -164,23 +167,27 @@ $myCustomRepresentation = $myCustomResource->myCustomEndpoint();
164167

165168
### [Users](https://www.keycloak.org/docs-api/26.0.0/rest-api/index.html#_users)
166169

167-
| Endpoint | Response | API |
168-
|---------------------------------------------------------|-----------------------------------------------------------------|----------------------------------------------------------------|
169-
| `GET /admin/realms/{realm}/users` | [UserCollection](src/Collection/UserCollection.php) | [Users::all()](src/Resource/Users.php) |
170-
| `POST /admin/realms/{realm}/users` | `n/a` | [Users::create()](src/Resource/Users.php) |
171-
| `GET /admin/realms/{realm}/users/{userId}` | [User](src/Representation/User.php) | [Users::get()](src/Resource/Users.php) |
172-
| `PUT /admin/realms/{realm}/users/{userId}` | `n/a` | [Users::update()](src/Resource/Users.php) |
173-
| `DELETE /admin/realms/{realm}/users/{userId}` | `n/a` | [Users::delete()](src/Resource/Users.php) |
174-
| `GET /admin/realms/{realm}/users` | [UserCollection](src/Collection/UserCollection.php) | [Users::search()](src/Resource/Users.php) |
175-
| `PUT /{realm}/users/{id}/groups/{groupId}` | `n/a` | [Users::joinGroup()](src/Resource/Users.php) |
176-
| `DELETE /{realm}/users/{id}/groups/{groupId}` | `n/a` | [Users::leaveGroup()](src/Resource/Users.php) |
177-
| `GET /{realm}/users/{id}/groups` | [GroupCollection](src/Collection/GroupCollection.php) | [Users::retrieveGroups()](src/Resource/Users.php) |
178-
| `GET /{realm}/users/{id}/role-mappings/realm` | [RoleCollection](src/Collection/RoleCollection.php) | [Users::retrieveRealmRoles()](src/Resource/Users.php) |
179-
| `GET /{realm}/users/{id}/role-mappings/realm/available` | [RoleCollection](src/Collection/RoleCollection.php) | [Users::retrieveAvailableRealmRoles()](src/Resource/Users.php) |
180-
| `POST /{realm}/users/{id}/role-mappings/realm` | `n/a` | [Users::addRealmRoles()](src/Resource/Users.php) |
181-
| `DELETE /{realm}/users/{id}/role-mappings/realm` | `n/a` | [Users::removeRealmRoles()](src/Resource/Users.php) |
182-
| `PUT /{realm}/users/{id}/execute-actions-email` | `n/a` | [Users::executeActionsEmail()](src/Resource/Users.php) |
183-
| `GET /admin/realms/{realm}/users/{userId}/credentials` | [CredentialCollection](src/Collection/CredentialCollection.php) | [Users::credentials()](src/Resource/Users.php) |
170+
| Endpoint | Response | API |
171+
|--------------------------------------------------------------------------------------|-----------------------------------------------------------------|----------------------------------------------------------------|
172+
| `GET /admin/realms/{realm}/users` | [UserCollection](src/Collection/UserCollection.php) | [Users::all()](src/Resource/Users.php) |
173+
| `POST /admin/realms/{realm}/users` | `n/a` | [Users::create()](src/Resource/Users.php) |
174+
| `GET /admin/realms/{realm}/users/{userId}` | [User](src/Representation/User.php) | [Users::get()](src/Resource/Users.php) |
175+
| `PUT /admin/realms/{realm}/users/{userId}` | `n/a` | [Users::update()](src/Resource/Users.php) |
176+
| `DELETE /admin/realms/{realm}/users/{userId}` | `n/a` | [Users::delete()](src/Resource/Users.php) |
177+
| `GET /admin/realms/{realm}/users` | [UserCollection](src/Collection/UserCollection.php) | [Users::search()](src/Resource/Users.php) |
178+
| `PUT /admin/realms/{realm}/users/{userId}/groups/{groupId}` | `n/a` | [Users::joinGroup()](src/Resource/Users.php) |
179+
| `DELETE /admin/realms/{realm}/users/{userId}/groups/{groupId}` | `n/a` | [Users::leaveGroup()](src/Resource/Users.php) |
180+
| `GET /admin/realms/{realm}/users/{userId}/groups` | [GroupCollection](src/Collection/GroupCollection.php) | [Users::retrieveGroups()](src/Resource/Users.php) |
181+
| `GET /admin/realms/{realm}/users/{userId}/role-mappings/realm` | [RoleCollection](src/Collection/RoleCollection.php) | [Users::retrieveRealmRoles()](src/Resource/Users.php) |
182+
| `GET /admin/realms/{realm}/users/{userId}/role-mappings/realm/available` | [RoleCollection](src/Collection/RoleCollection.php) | [Users::retrieveAvailableRealmRoles()](src/Resource/Users.php) |
183+
| `POST /admin/realms/{realm}/users/{userId}/role-mappings/realm` | `n/a` | [Users::addRealmRoles()](src/Resource/Users.php) |
184+
| `DELETE /admin/realms/{realm}/users/{userId}/role-mappings/realm` | `n/a` | [Users::removeRealmRoles()](src/Resource/Users.php) |
185+
| `GET /admin/realms/{realm}/users/{userId}/role-mappings/clients/{clientUuid}` | [RoleCollection](src/Collection/RoleCollection.php) | [Users::retrieveClientRoles()](src/Resource/Users.php) |
186+
| `GET /admin/realms/{realm}/users/{userId}/role-mappings/clients/{clientUuid}/available` | [RoleCollection](src/Collection/RoleCollection.php) | [Users::retrieveAvailableClientRoles()](src/Resource/Users.php) |
187+
| `POST /admin/realms/{realm}/users/{userId}/role-mappings/clients/{clientUuid}` | `n/a` | [Users::addClientRoles()](src/Resource/Users.php) |
188+
| `DELETE /admin/realms/{realm}/users/{userId}/role-mappings/clients/{clientUuid}` | `n/a` | [Users::removeClientRoles()](src/Resource/Users.php) |
189+
| `PUT /admin/realms/{realm}/users/{userId}/execute-actions-email` | `n/a` | [Users::executeActionsEmail()](src/Resource/Users.php) |
190+
| `GET /admin/realms/{realm}/users/{userId}/credentials` | [CredentialCollection](src/Collection/CredentialCollection.php) | [Users::credentials()](src/Resource/Users.php) |
184191

185192
### [Roles](https://www.keycloak.org/docs-api/26.0.0/rest-api/index.html#_roles)
186193

src/Http/Client.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ private function fetchTokens(): array
7171
try {
7272
$response = $this->httpClient->request(
7373
'POST',
74-
$this->keycloak->getBaseUrl() . '/realms/master/protocol/openid-connect/token',
74+
$this->keycloak->getBaseUrl() . '/realms/' . $this->keycloak->getRealm() . '/protocol/openid-connect/token',
7575
[
7676
'form_params' => [
7777
'refresh_token' => $this->tokenStorage->retrieveRefreshToken()?->toString(),
@@ -83,7 +83,7 @@ private function fetchTokens(): array
8383
} catch (ClientException $e) {
8484
$response = $this->httpClient->request(
8585
'POST',
86-
$this->keycloak->getBaseUrl() . '/realms/master/protocol/openid-connect/token',
86+
$this->keycloak->getBaseUrl() . '/realms/' . $this->keycloak->getRealm() . '/protocol/openid-connect/token',
8787
[
8888
'form_params' => [
8989
'username' => $this->keycloak->getUsername(),

src/Keycloak.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ public function __construct(
3737
private readonly string $baseUrl,
3838
private readonly string $username,
3939
private readonly string $password,
40+
private readonly string $realm = 'master',
4041
private readonly TokenStorageInterface $tokenStorage = new InMemory(),
4142
?ClientInterface $guzzleClient = new GuzzleClient(),
4243
) {
@@ -144,4 +145,9 @@ private function fetchVersion(): void
144145
$this->serializer = new Serializer($this->version);
145146
$this->commandExecutor = new CommandExecutor($this->client, $this->serializer);
146147
}
148+
149+
public function getRealm(): string
150+
{
151+
return $this->realm;
152+
}
147153
}

tests/Unit/Http/ClientTest.php

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@
1212
use GuzzleHttp\ClientInterface;
1313
use GuzzleHttp\Exception\ClientException;
1414
use GuzzleHttp\Psr7\Response;
15+
use GuzzleHttp\Psr7\Request;
16+
use GuzzleHttp\Client as GuzzleClient;
17+
use GuzzleHttp\Handler\MockHandler;
18+
use GuzzleHttp\HandlerStack;
19+
use GuzzleHttp\Middleware;
1520
use PHPUnit\Framework\Attributes\CoversClass;
1621
use PHPUnit\Framework\TestCase;
1722

@@ -114,4 +119,75 @@ public function testIsAuthorizedIfTokenStorageContainsUnexpiredAccessToken(): vo
114119

115120
static::assertTrue($client->isAuthorized());
116121
}
122+
123+
public function testAuthenticatesUsingConfiguredRealm(): void
124+
{
125+
$accessToken = $this->generateToken((new DateTimeImmutable())->modify('+1 hour'));
126+
$refreshToken = $this->generateToken((new DateTimeImmutable())->modify('+1 hour'));
127+
128+
$authorizationResponse = new Response(
129+
status: 200,
130+
body: json_encode(
131+
value: [
132+
'access_token' => $accessToken->toString(),
133+
'refresh_token' => $refreshToken->toString(),
134+
],
135+
flags: JSON_THROW_ON_ERROR,
136+
),
137+
);
138+
139+
$realmsResponse = new Response(
140+
status: 200,
141+
body: json_encode(
142+
value: [
143+
'realms' => [],
144+
],
145+
flags: JSON_THROW_ON_ERROR,
146+
),
147+
);
148+
149+
$history = [];
150+
$historyMiddleware = Middleware::history($history);
151+
152+
$tokenRequest = new Request('POST', 'http://keycloak:8080/realms/custom-realm/protocol/openid-connect/token');
153+
$mockHandler = new MockHandler([
154+
new ClientException('Bad Request', $tokenRequest, new Response(400)),
155+
$authorizationResponse,
156+
$realmsResponse,
157+
]);
158+
159+
$handlerStack = HandlerStack::create($mockHandler);
160+
$handlerStack->push($historyMiddleware);
161+
162+
$httpClient = new GuzzleClient(['handler' => $handlerStack]);
163+
164+
$keycloak = new Keycloak(
165+
'http://keycloak:8080',
166+
'admin',
167+
'admin',
168+
realm: 'custom-realm',
169+
);
170+
171+
$client = new Client($keycloak, $httpClient, new InMemoryTokenStorage());
172+
173+
$client->request('GET', '/admin/realms');
174+
175+
// Verify that any token endpoint call uses the configured realm in the URL
176+
static::assertNotEmpty($history);
177+
$tokenCalls = array_values(array_filter((array) $history, function (array $transaction): bool {
178+
/** @var Request $request */
179+
$request = $transaction['request'];
180+
return str_contains((string) $request->getUri(), '/protocol/openid-connect/token');
181+
}));
182+
static::assertNotEmpty($tokenCalls);
183+
foreach ($tokenCalls as $transaction) {
184+
/** @var Request $request */
185+
$request = $transaction['request'];
186+
$authUri = (string) $request->getUri();
187+
static::assertStringContainsString('/realms/custom-realm/', $authUri);
188+
static::assertStringNotContainsString('/realms/master/', $authUri);
189+
}
190+
191+
static::assertTrue($client->isAuthorized());
192+
}
117193
}

0 commit comments

Comments
 (0)