Skip to content

Commit 6204ce2

Browse files
committed
Use storage-backed bearer auth to keep refreshed tokens and add coverage for StorageAwareAuthenticationPlugin
1 parent 123020f commit 6204ce2

3 files changed

Lines changed: 138 additions & 1 deletion

File tree

src/Options.php

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Geocaching\Enum\Environment;
99
use Geocaching\Plugin\CircuitBreakerPlugin;
1010
use Geocaching\Plugin\GeocachingHttpLoggerPlugin;
11+
use Geocaching\Plugin\StorageAwareAuthenticationPlugin;
1112
use Geocaching\Plugin\ReliabilityPlugin;
1213
use Geocaching\Plugin\RetryPlugin;
1314
use Geocaching\Reliability\CircuitBreaker;
@@ -58,7 +59,7 @@ public function __construct(array $options = [])
5859
$this->getClientBuilder()->setBaseUri($baseUri->value);
5960

6061
$this->getClientBuilder()->addPlugin(new BaseUriPlugin($this->getUri()));
61-
$this->getClientBuilder()->addPlugin(new AuthenticationPlugin(new Bearer($this->getAccessToken())));
62+
$this->addAuthenticationPlugin();
6263
}
6364

6465
private function configureOptions(OptionsResolver $resolver): void
@@ -68,6 +69,8 @@ private function configureOptions(OptionsResolver $resolver): void
6869
[
6970
'client_builder' => new ClientBuilder(),
7071
'uri_factory' => Psr17FactoryDiscovery::findUriFactory(),
72+
'token_storage' => null,
73+
'reference_code' => null,
7174
]
7275
);
7376

@@ -76,6 +79,8 @@ private function configureOptions(OptionsResolver $resolver): void
7679
$resolver->setAllowedTypes('client_builder', ClientBuilder::class);
7780
$resolver->setAllowedTypes('uri_factory', UriFactoryInterface::class);
7881
$resolver->setAllowedTypes('uri', 'string');
82+
$resolver->setAllowedTypes('token_storage', [TokenStorageInterface::class, 'null']);
83+
$resolver->setAllowedTypes('reference_code', ['string', 'null']);
7984
}
8085

8186
public function getClientBuilder(): ClientBuilder
@@ -97,6 +102,31 @@ public function getAccessToken(): string
97102
{
98103
return $this->options['access_token'];
99104
}
105+
106+
/**
107+
* Register an authentication plugin that always uses the freshest token.
108+
*
109+
* If a TokenStorage is provided, we pull the current token from storage so
110+
* retries after refresh use the updated access token. Otherwise we fall
111+
* back to a static Bearer token.
112+
*/
113+
private function addAuthenticationPlugin(): void
114+
{
115+
$storage = $this->options['token_storage'];
116+
$referenceCode = $this->options['reference_code'];
117+
118+
if ($storage && $referenceCode) {
119+
$plugin = new StorageAwareAuthenticationPlugin(
120+
$storage,
121+
$referenceCode,
122+
$this->getAccessToken()
123+
);
124+
} else {
125+
$plugin = new AuthenticationPlugin(new Bearer($this->getAccessToken()));
126+
}
127+
128+
$this->getClientBuilder()->addPlugin($plugin);
129+
}
100130

101131
/**
102132
* Enable HTTP logging for all API requests/responses with a pre-configured logger.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Geocaching\Plugin;
6+
7+
use Http\Client\Common\Plugin;
8+
use Http\Message\Authentication\Bearer;
9+
use Http\Promise\Promise;
10+
use League\OAuth2\Client\Token\TokenStorageInterface;
11+
use Psr\Http\Message\RequestInterface;
12+
13+
/**
14+
* Authentication plugin that always applies the freshest token from storage.
15+
*
16+
* Falls back to the initially configured access token when storage is empty.
17+
*/
18+
final class StorageAwareAuthenticationPlugin implements Plugin
19+
{
20+
public function __construct(
21+
private TokenStorageInterface $storage,
22+
private string $referenceCode,
23+
private string $fallbackAccessToken
24+
) {
25+
}
26+
27+
public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise
28+
{
29+
$tokens = $this->storage->getTokens($this->referenceCode);
30+
31+
if ($tokens) {
32+
$request = $request->withHeader('Authorization', $tokens->getAuthorizationHeader());
33+
} else {
34+
// Fall back to the original token so the first call still works
35+
$request = (new Bearer($this->fallbackAccessToken))->authenticate($request);
36+
}
37+
38+
return $next($request);
39+
}
40+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Plugin;
6+
7+
use Geocaching\Plugin\StorageAwareAuthenticationPlugin;
8+
use Http\Promise\FulfilledPromise;
9+
use League\OAuth2\Client\Token\TokenSet;
10+
use League\OAuth2\Client\Token\TokenStorageInterface;
11+
use Nyholm\Psr7\Request;
12+
use PHPUnit\Framework\TestCase;
13+
14+
final class StorageAwareAuthenticationPluginTest extends TestCase
15+
{
16+
public function testUsesTokenFromStorageWhenAvailable(): void
17+
{
18+
$tokens = TokenSet::create('fresh-access', 'refresh-token', 3600);
19+
20+
$storage = $this->createMock(TokenStorageInterface::class);
21+
$storage->expects($this->once())
22+
->method('getTokens')
23+
->with('user-123')
24+
->willReturn($tokens);
25+
26+
$plugin = new StorageAwareAuthenticationPlugin($storage, 'user-123', 'fallback-access');
27+
28+
$capturedRequest = null;
29+
$next = function ($request) use (&$capturedRequest) {
30+
$capturedRequest = $request;
31+
return new FulfilledPromise('ok');
32+
};
33+
34+
$plugin->handleRequest(new Request('GET', 'https://example.com'), $next, $next)->wait();
35+
36+
self::assertSame(
37+
'Bearer fresh-access',
38+
$capturedRequest->getHeaderLine('Authorization'),
39+
'Should apply authorization header from storage tokens'
40+
);
41+
}
42+
43+
public function testFallsBackToProvidedAccessTokenWhenStorageEmpty(): void
44+
{
45+
$storage = $this->createMock(TokenStorageInterface::class);
46+
$storage->expects($this->once())
47+
->method('getTokens')
48+
->with('user-123')
49+
->willReturn(null);
50+
51+
$plugin = new StorageAwareAuthenticationPlugin($storage, 'user-123', 'fallback-access');
52+
53+
$capturedRequest = null;
54+
$next = function ($request) use (&$capturedRequest) {
55+
$capturedRequest = $request;
56+
return new FulfilledPromise('ok');
57+
};
58+
59+
$plugin->handleRequest(new Request('GET', 'https://example.com'), $next, $next)->wait();
60+
61+
self::assertSame(
62+
'Bearer fallback-access',
63+
$capturedRequest->getHeaderLine('Authorization'),
64+
'Should fall back to initial bearer when storage has no tokens'
65+
);
66+
}
67+
}

0 commit comments

Comments
 (0)