Skip to content

Commit

Permalink
Merge pull request #60 from MohammadWaleed/develop
Browse files Browse the repository at this point in the history
prepare for v0.24.0
  • Loading branch information
MohammadWaleed authored Jul 16, 2021
2 parents d09b890 + 65a811c commit ff77eef
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 33 deletions.
38 changes: 37 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ $client = Keycloak\Admin\KeycloakClient::factory([

By default, the token is saved at runtime. This means that the previous token is not used when creating a new client.

You can change customize how the token is stored in the client configuration by implementing your own `TokenStorage`,
You can customize how the token is stored in the client configuration by implementing your own `TokenStorage`,
an interface which describes how the token is stored and retrieved.
```php
class CustomTokenStorage implements TokenStorage
Expand All @@ -181,6 +181,42 @@ $client = Keycloak\Admin\KeycloakClient::factory([
]);
```

### Custom Keycloak endpoints

It is possible to inject [Guzzle Service Operations](https://guzzle3.readthedocs.io/webservice-client/guzzle-service-descriptions.html#operations)
in the keycloak client configuration using the `custom_operations` keyword. This way you can extend the built-in supported endpoints with custom.

```php
$client = KeycloakClient::factory([
...
'custom_operations' => [
'getUsersByAttribute' => [
'uri' => '/auth/realms/{realm}/userapi-rest/users/search-by-attr',
'description' => 'Get users by attribute Returns a list of users, filtered according to query parameters',
'httpMethod' => 'GET',
'parameters' => [
'realm' => [
'location' => 'uri',
'description' => 'The Realm name',
'type' => 'string',
'required' => true,
],
'attr' => [
'location' => 'query',
'type' => 'string',
'required' => true,
],
'value' => [
'location' => 'query',
'type' => 'string',
'required' => true,
],
],
],
]
]);
```


# Supported APIs

Expand Down
12 changes: 11 additions & 1 deletion src/Admin/KeycloakClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,17 @@ public static function factory($config = array())

$config['handler'] = $stack;

$description = new Description(include __DIR__ . "/Resources/{$file}");
$serviceDescription = include __DIR__ . "/Resources/{$file}";
$customOperations = isset($config["custom_operations"]) && is_array($config["custom_operations"]) ? $config["custom_operations"] : [];
foreach ($customOperations as $operationKey => $operation) {
// Do not override built-in functionality
if (isset($serviceDescription['operations'][$operationKey])) {
continue;
}
$serviceDescription['operations'][$operationKey] = $operation;
}
$description = new Description($serviceDescription);

// Create the new Keycloak Client with our Configuration
return new self(
new Client($config),
Expand Down
88 changes: 57 additions & 31 deletions src/Admin/Middleware/RefreshToken.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
namespace Keycloak\Admin\Middleware;

use GuzzleHttp\Client;
use GuzzleHttp\Promise\FulfilledPromise;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\Promise\RejectedPromise;
use GuzzleHttp\Promise\RejectionException;
use Keycloak\Admin\TokenStorages\TokenStorage;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
Expand All @@ -23,16 +27,18 @@ public function __invoke(callable $handler)
{
return function (RequestInterface $request, array $options) use ($handler) {
$token = $this->tokenStorage->getToken();
$cred = $this->refreshTokenIfNeeded($token, $options);
$this->tokenStorage->saveToken($cred);
$request = $request->withHeader('Authorization', 'Bearer ' . $cred['access_token']);
return $handler($request, $options)->then(function (ResponseInterface $response) {

return $this->refreshTokenIfNeeded($token, $options)->then(function (array $cred) use ($request, $handler, $options) {
$this->tokenStorage->saveToken($cred);
$request = $request->withHeader('Authorization', 'Bearer ' . $cred['access_token']);
return $handler($request, $options);
})->then(function (ResponseInterface $response) {
if ($response->getStatusCode() >= 400) {
$this->tokenStorage->saveToken([]);
}

return $response;
}, function ($reason) {
})->otherwise(function ($reason) {
$this->tokenStorage->saveToken([]);
throw $reason;
});
Expand All @@ -42,20 +48,22 @@ public function __invoke(callable $handler)
/**
* Check we need to refresh token and refresh if needed
*
* @param array $credentials
* @return array
* @param ?array $credentials
* @param $options
* @return PromiseInterface
*/
protected function refreshTokenIfNeeded($credentials, $options)
{
if (!is_array($credentials) || empty($credentials['access_token']) || empty($credentials['refresh_token'])) {
return $this->getAccessToken($credentials, false, $options);
}

$info = $this->parseAccessToken($credentials['access_token']);
$exp = $info['exp'] ?? 0;
if (!$this->tokenExpired($credentials['access_token'])) {
return new FulfilledPromise($credentials);
}

if (time() < $exp) {
return $credentials;
if ($this->tokenExpired($credentials['refresh_token'])) {
return $this->getAccessToken($credentials, false, $options);
}

return $this->getAccessToken($credentials, true, $options);
Expand All @@ -67,7 +75,7 @@ protected function refreshTokenIfNeeded($credentials, $options)
* @param string $token
* @return array
*/
public function parseAccessToken($token)
public function getTokenPayload($token)
{
if (!is_string($token)) {
return [];
Expand All @@ -79,18 +87,35 @@ public function parseAccessToken($token)
return json_decode($token, true);
}

/**
* Check token expiration
*
* @param string $token
* @return bool
*/
public function tokenExpired($token) {
$info = $this->getTokenPayload($token);
$exp = $info['exp'] ?? 0;
if (time() < $exp) {
return false;
}
return true;
}

/**
* Refresh access token
*
* @param string $refreshToken
* @return array
* @param array|null $credentials
* @param $refresh
* @param $options
* @return PromiseInterface
*/
public function getAccessToken($credentials, $refresh, $options)
{
if ($refresh && empty($credentials['refresh_token'])) {
return [];
return new RejectedPromise("cannot refresh token when the 'refresh_token' is missing");
}

$url = "auth/realms/{$options['realm']}/protocol/openid-connect/token";
$clientId = $options["client_id"] ?? "admin-cli";
$grantType = $refresh ? "refresh_token" : ($options["grant_type"] ?? "password");
Expand All @@ -101,7 +126,7 @@ public function getAccessToken($credentials, $refresh, $options)

if ($grantType === "refresh_token") {
$params['refresh_token'] = $credentials['refresh_token'];
} else if ($grantType === "password"){
} else if ($grantType === "password") {
$params['username'] = $options['username'];
$params['password'] = $options['password'];
} else if ($grantType === "client_credentials") {
Expand All @@ -112,23 +137,24 @@ public function getAccessToken($credentials, $refresh, $options)
$params['client_secret'] = $options['client_secret'];
}

$token = [];
$httpClient = new Client([
'base_uri' => $options['baseUri'],
'verify' => isset($options['verify']) ? $options['verify'] : true,
]);

try {
$httpClient = new Client([
'base_uri' => $options['baseUri'],
'verify' => isset($options['verify']) ? $options['verify'] : true,
]);
$response = $httpClient->request('POST', $url, ['form_params' => $params]);
return $httpClient->requestAsync('POST', $url, ['form_params' => $params])->then(function (ResponseInterface $response) {
if ($response->getStatusCode() !== 200) {
throw new RejectionException('expected to receive http status code 200 when requesting a token');
}

if ($response->getStatusCode() === 200) {
$token = $response->getBody()->getContents();
$token = json_decode($token, true);
$serializedToken = $response->getBody()->getContents();
$token = json_decode($serializedToken, true);

if (!$token) {
throw new RejectionException('token returned in the response body is not in a valid json');
}
} catch (GuzzleException $e) {
echo $e->getMessage();
}

return $token;
return $token;
});
}
}

0 comments on commit ff77eef

Please sign in to comment.