Skip to content

Commit d438fdb

Browse files
Merge pull request #175 from php-opencloud/cache-token
Auth token caching
2 parents 6479d36 + d22ce13 commit d438fdb

File tree

7 files changed

+310
-3
lines changed

7 files changed

+310
-3
lines changed

doc/services/identity/v3/tokens.rst

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,36 @@ Revoke token
4343

4444
.. sample:: identity/v3/tokens/revoke_token.php
4545
.. refdoc:: OpenStack/Identity/v3/Service.html#method_revokeToken
46+
47+
Cache authentication token
48+
--------------------------
49+
50+
Use case
51+
~~~~~~~~
52+
53+
Before the SDK performs an API call, it will first authenticate to the OpenStack Identity service using the provided
54+
credentials.
55+
56+
If the user's credential is valid, credentials are valid, the Identity service returns an authentication token. The SDK
57+
will then use this authentication token and service catalog in all subsequent API calls.
58+
59+
This setup typically works well for CLI applications. However, for web-based applications, performance
60+
is undesirable since authentication step adds ~100ms to the response time.
61+
62+
In order to improve performance, the SDK allows users to export and store authentication tokens, and re-use until they
63+
expire.
64+
65+
66+
Generate token and persist to file
67+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
68+
69+
.. sample:: identity/v3/tokens/export_authentication_token.php
70+
71+
72+
For scalability, it is recommended that cached tokens are stored in persistent storage such as memcache or redis instead
73+
of a local file.
74+
75+
Initialize Open Stack using cached authentication token
76+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
77+
78+
.. sample:: identity/v3/tokens/use_cached_authentication_token.php
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
require 'vendor/autoload.php';
4+
5+
$params = [
6+
'authUrl' => '{authUrl}',
7+
'region' => '{region}',
8+
'user' => [
9+
'name' => '{username}',
10+
'password' => '{password}',
11+
'domain' => ['id' => '{domainId}']
12+
],
13+
'scope' => [
14+
'project' => ['id' => '{projectId}']
15+
]
16+
];
17+
18+
$openstack = new OpenStack\OpenStack($params);
19+
20+
$identity = $openstack->identityV3();
21+
22+
$token = $identity->generateToken($params);
23+
24+
// Display token expiry
25+
echo sprintf('Token expires at %s'. PHP_EOL, $token->expires->format('c'));
26+
27+
// Save token to file
28+
file_put_contents('token.json', json_encode($token->export()));
29+
30+
31+
// Alternatively, one may persist token to memcache or redis
32+
// Redis and memcache then can purge the entry when token expires.
33+
34+
/**@var \Memcached $memcache */
35+
$memcache->set('token', $token->export(), $token->expires->format('U'));
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
require 'vendor/autoload.php';
4+
5+
$params = [
6+
'authUrl' => '{authUrl}',
7+
'region' => '{region}',
8+
'user' => [
9+
'name' => '{username}',
10+
'password' => '{password}',
11+
'domain' => ['id' => '{domainId}']
12+
],
13+
'scope' => [
14+
'project' => ['id' => '{projectId}']
15+
]
16+
];
17+
18+
$token = json_decode(file_get_contents('token.json'), true);
19+
20+
// Inject cached token to params if token is still fresh
21+
if ((new \DateTimeImmutable($token['expires_at'])) > (new \DateTimeImmutable('now'))) {
22+
$params['cachedToken'] = $token;
23+
}
24+
25+
$openstack = new OpenStack\OpenStack($params);

src/Identity/v3/Models/Token.php

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace OpenStack\Identity\v3\Models;
44

55
use OpenStack\Common\Resource\Alias;
6+
use OpenStack\Common\Transport\Utils;
67
use Psr\Http\Message\ResponseInterface;
78
use OpenStack\Common\Resource\OperatorResource;
89
use OpenStack\Common\Resource\Creatable;
@@ -16,7 +17,7 @@ class Token extends OperatorResource implements Creatable, Retrievable, \OpenSta
1617
/** @var array */
1718
public $methods;
1819

19-
/** @var []Role */
20+
/** @var Role[] */
2021
public $roles;
2122

2223
/** @var \DateTimeImmutable */
@@ -43,6 +44,8 @@ class Token extends OperatorResource implements Creatable, Retrievable, \OpenSta
4344
protected $resourceKey = 'token';
4445
protected $resourcesKey = 'tokens';
4546

47+
protected $cachedToken;
48+
4649
/**
4750
* @inheritdoc
4851
*/
@@ -116,6 +119,27 @@ public function create(array $data): Creatable
116119
}
117120

118121
$response = $this->execute($this->api->postTokens(), $data);
119-
return $this->populateFromResponse($response);
122+
$token = $this->populateFromResponse($response);
123+
124+
// Cache response as an array to export if needed.
125+
// Added key `id` which is auth token from HTTP header X-Subject-Token
126+
$this->cachedToken = Utils::flattenJson(Utils::jsonDecode($response), $this->resourceKey);
127+
$this->cachedToken['id'] = $token->id;
128+
129+
return $token;
130+
}
131+
132+
/**
133+
* Returns a serialized representation of an authentication token.
134+
*
135+
* Initialize OpenStack object using $params['cachedToken'] to reduce the amount of HTTP calls.
136+
*
137+
* This array is a modified version of response from `/auth/tokens`. Do not manually modify this array.
138+
*
139+
* @return array
140+
*/
141+
public function export(): array
142+
{
143+
return $this->cachedToken;
120144
}
121145
}

src/Identity/v3/Service.php

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,15 @@ public function authenticate(array $options): array
3131
{
3232
$authOptions = array_intersect_key($options, $this->api->postTokens()['params']);
3333

34-
$token = $this->generateToken($authOptions);
34+
if (!empty($options['cachedToken'])) {
35+
$token = $this->generateTokenFromCache($options['cachedToken']);
36+
37+
if ($token->hasExpired()) {
38+
throw new \RuntimeException(sprintf('Cached token has expired on "%s".', $token->expires->format(\DateTime::ISO8601)));
39+
}
40+
} else {
41+
$token = $this->generateToken($authOptions);
42+
}
3543

3644
$name = $options['catalogName'];
3745
$type = $options['catalogType'];
@@ -46,6 +54,18 @@ public function authenticate(array $options): array
4654
$type, $name, $region, $interface));
4755
}
4856

57+
/**
58+
* Generates authentication token from cached token using `$token->export()`
59+
*
60+
* @param array $cachedToken {@see \OpenStack\Identity\v3\Models\Token::export}
61+
*
62+
* @return Models\Token
63+
*/
64+
public function generateTokenFromCache(array $cachedToken): Models\Token
65+
{
66+
return $this->model(Models\Token::class)->populateFromArray($cachedToken);
67+
}
68+
4969
/**
5070
* Generates a new authentication token
5171
*

src/OpenStack.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ class OpenStack
3232
* ['logger'] = (LoggerInterface) Must set if debugLog is true [OPTIONAL]
3333
* ['messageFormatter'] = (MessageFormatter) Must set if debugLog is true [OPTIONAL]
3434
* ['requestOptions'] = (array) Guzzle Http request options [OPTIONAL]
35+
* ['cachedToken'] = (array) Cached token credential [OPTIONAL]
3536
*
3637
* @param Builder $builder
3738
*/

tests/unit/Identity/v3/ServiceTest.php

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@
1010
use OpenStack\Identity\v3\Service;
1111
use OpenStack\Test\TestCase;
1212
use Prophecy\Argument;
13+
use Psr\Http\Message\ResponseInterface;
1314

1415
class ServiceTest extends TestCase
1516
{
17+
/** @var Service */
1618
private $service;
1719

1820
public function setUp()
@@ -64,6 +66,156 @@ public function test_it_authenticates()
6466
$this->assertEquals('http://example.org:8080/v1/AUTH_e00abf65afca49609eedd163c515cf10', $url);
6567
}
6668

69+
public function test_it_authenticates_using_cache_token()
70+
{
71+
$cachedToken = [
72+
'is_domain' => false,
73+
'methods' => [
74+
'password',
75+
],
76+
'roles' => [
77+
0 => [
78+
'id' => 'ce40dfb7a1b14f8a875194fe2944e00c',
79+
'name' => 'admin',
80+
],
81+
],
82+
'expires_at' => '2199-11-24T04:47:49.000000Z',
83+
'project' => [
84+
'domain' => [
85+
'id' => 'default',
86+
'name' => 'Default',
87+
],
88+
'id' => 'c41b19de8aac4ecdb0f04ede718206c5',
89+
'name' => 'admin',
90+
],
91+
'catalog' => [
92+
[
93+
'endpoints' => [
94+
[
95+
'region_id' => 'RegionOne',
96+
'url' => 'http://example.org:8080/v1/AUTH_e00abf65afca49609eedd163c515cf10',
97+
'region' => 'RegionOne',
98+
'interface' => 'public',
99+
'id' => 'hhh',
100+
]
101+
],
102+
'type' => 'object-store',
103+
'id' => 'aaa',
104+
'name' => 'swift',
105+
],
106+
],
107+
'user' => [
108+
'domain' => [
109+
'id' => 'default',
110+
'name' => 'Default',
111+
],
112+
'id' => '37a36374b074428985165e80c9ab28c8',
113+
'name' => 'admin',
114+
],
115+
'audit_ids' => [
116+
'X0oY7ouSQ32vEpbgDJTDpA',
117+
],
118+
'issued_at' => '2017-11-24T03:47:49.000000Z',
119+
'id' => 'bb4f74cfb73847ec9ca947fa61d799d3',
120+
];
121+
122+
$userOptions = [
123+
'user' => [
124+
'id' => '{userId}',
125+
'password' => '{userPassword}',
126+
'domain' => ['id' => '{domainId}']
127+
],
128+
'scope' => [
129+
'project' => ['id' => '{projectId}']
130+
],
131+
'catalogName' => 'swift',
132+
'catalogType' => 'object-store',
133+
'region' => 'RegionOne',
134+
'cachedToken' => $cachedToken
135+
];
136+
137+
list($token, $url) = $this->service->authenticate($userOptions);
138+
139+
$this->assertInstanceOf(Models\Token::class, $token);
140+
$this->assertEquals('http://example.org:8080/v1/AUTH_e00abf65afca49609eedd163c515cf10', $url);
141+
}
142+
143+
144+
/**
145+
* @expectedException \RuntimeException
146+
* @expectedExceptionMessage Cached token has expired
147+
*/
148+
public function test_it_authenticates_and_throws_exception_when_authenticate_with_expired_cached_token()
149+
{
150+
$cachedToken = [
151+
'is_domain' => false,
152+
'methods' => [
153+
'password',
154+
],
155+
'roles' => [
156+
0 => [
157+
'id' => 'ce40dfb7a1b14f8a875194fe2944e00c',
158+
'name' => 'admin',
159+
],
160+
],
161+
'expires_at' => '2000-11-24T04:47:49.000000Z',
162+
'project' => [
163+
'domain' => [
164+
'id' => 'default',
165+
'name' => 'Default',
166+
],
167+
'id' => 'c41b19de8aac4ecdb0f04ede718206c5',
168+
'name' => 'admin',
169+
],
170+
'catalog' => [
171+
[
172+
'endpoints' => [
173+
[
174+
'region_id' => 'RegionOne',
175+
'url' => 'http://example.org:8080/v1/AUTH_e00abf65afca49609eedd163c515cf10',
176+
'region' => 'RegionOne',
177+
'interface' => 'public',
178+
'id' => 'hhh',
179+
]
180+
],
181+
'type' => 'object-store',
182+
'id' => 'aaa',
183+
'name' => 'swift',
184+
],
185+
],
186+
'user' => [
187+
'domain' => [
188+
'id' => 'default',
189+
'name' => 'Default',
190+
],
191+
'id' => '37a36374b074428985165e80c9ab28c8',
192+
'name' => 'admin',
193+
],
194+
'audit_ids' => [
195+
'X0oY7ouSQ32vEpbgDJTDpA',
196+
],
197+
'issued_at' => '2017-11-24T03:47:49.000000Z',
198+
'id' => 'bb4f74cfb73847ec9ca947fa61d799d3',
199+
];
200+
201+
$userOptions = [
202+
'user' => [
203+
'id' => '{userId}',
204+
'password' => '{userPassword}',
205+
'domain' => ['id' => '{domainId}']
206+
],
207+
'scope' => [
208+
'project' => ['id' => '{projectId}']
209+
],
210+
'catalogName' => 'swift',
211+
'catalogType' => 'object-store',
212+
'region' => 'RegionOne',
213+
'cachedToken' => $cachedToken
214+
];
215+
216+
$this->service->authenticate($userOptions);
217+
}
218+
67219
/**
68220
* @expectedException \RuntimeException
69221
*/
@@ -513,6 +665,23 @@ public function test_it_generates_token_with_token_id()
513665
$this->assertInstanceOf(Models\Token::class, $token);
514666
}
515667

668+
public function test_it_generates_token_from_cache()
669+
{
670+
$cache = [
671+
'id' => 'some-token-id'
672+
];
673+
674+
$this->client
675+
->request('POST', 'auth/tokens', Argument::any())
676+
->shouldNotBeCalled()
677+
->willReturn($this->getFixture('token-get'));
678+
679+
$token = $this->service->generateTokenFromCache($cache);
680+
681+
$this->assertInstanceOf(Models\Token::class, $token);
682+
$this->assertEquals('some-token-id', $token->id);
683+
}
684+
516685
public function test_it_lists_endpoints()
517686
{
518687
$this->listTest($this->createFn($this->service, 'listEndpoints', []), 'endpoints', 'Endpoint');

0 commit comments

Comments
 (0)