Skip to content

Commit ff2ffe6

Browse files
committed
Abstract away JWS fetcher
1 parent c078f83 commit ff2ffe6

File tree

8 files changed

+236
-76
lines changed

8 files changed

+236
-76
lines changed

src/Codebooks/ContentTypesEnum.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@
66

77
enum ContentTypesEnum: string
88
{
9+
case ApplicationJwt = 'application/jwt';
910
case ApplicationEntityStatementJwt = 'application/entity-statement+jwt';
1011
}

src/Federation.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,8 @@ public function jwsSerializerManager(): JwsSerializerManager
103103
public function entityStatementFetcher(): EntityStatementFetcher
104104
{
105105
return $this->entityStatementFetcher ??= new EntityStatementFetcher(
106-
$this->artifactFetcher(),
107106
$this->entityStatementFactory(),
107+
$this->artifactFetcher(),
108108
$this->maxCacheDurationDecorator,
109109
$this->helpers(),
110110
$this->logger,

src/Federation/EntityStatementFetcher.php

Lines changed: 38 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,35 @@
88
use SimpleSAML\OpenID\Codebooks\ClaimsEnum;
99
use SimpleSAML\OpenID\Codebooks\ContentTypesEnum;
1010
use SimpleSAML\OpenID\Codebooks\EntityTypesEnum;
11-
use SimpleSAML\OpenID\Codebooks\HttpHeadersEnum;
12-
use SimpleSAML\OpenID\Codebooks\HttpMethodsEnum;
1311
use SimpleSAML\OpenID\Codebooks\WellKnownEnum;
14-
use SimpleSAML\OpenID\Decorators\CacheDecorator;
1512
use SimpleSAML\OpenID\Decorators\DateIntervalDecorator;
16-
use SimpleSAML\OpenID\Decorators\HttpClientDecorator;
1713
use SimpleSAML\OpenID\Exceptions\FetchException;
1814
use SimpleSAML\OpenID\Exceptions\JwsException;
1915
use SimpleSAML\OpenID\Federation\Factories\EntityStatementFactory;
2016
use SimpleSAML\OpenID\Helpers;
17+
use SimpleSAML\OpenID\Jws\JwsFetcher;
2118
use SimpleSAML\OpenID\Utils\ArtifactFetcher;
22-
use Throwable;
2319

24-
class EntityStatementFetcher
20+
class EntityStatementFetcher extends JwsFetcher
2521
{
2622
public function __construct(
27-
protected readonly ArtifactFetcher $artifactFetcher,
28-
protected readonly EntityStatementFactory $entityStatementFactory,
29-
protected readonly DateIntervalDecorator $maxCacheDuration,
30-
protected readonly Helpers $helpers,
31-
protected readonly ?LoggerInterface $logger = null,
23+
private readonly EntityStatementFactory $parsedJwsFactory,
24+
ArtifactFetcher $artifactFetcher,
25+
DateIntervalDecorator $maxCacheDuration,
26+
Helpers $helpers,
27+
?LoggerInterface $logger = null,
3228
) {
29+
parent::__construct($parsedJwsFactory, $artifactFetcher, $maxCacheDuration, $helpers, $logger);
30+
}
31+
32+
protected function buildJwsInstance(string $token): EntityStatement
33+
{
34+
return $this->parsedJwsFactory->fromToken($token);
35+
}
36+
37+
protected function getExpectedContentTypeHttpHeader(): string
38+
{
39+
return ContentTypesEnum::ApplicationEntityStatementJwt->value;
3340
}
3441

3542
/**
@@ -104,27 +111,27 @@ public function fromCacheOrNetwork(string $uri): EntityStatement
104111
* @param string $uri
105112
* @return \SimpleSAML\OpenID\Federation\EntityStatement|null
106113
* @throws \SimpleSAML\OpenID\Exceptions\JwsException
114+
* @throws \SimpleSAML\OpenID\Exceptions\FetchException
107115
*/
108116
public function fromCache(string $uri): ?EntityStatement
109117
{
110-
$this->logger?->debug(
111-
'Trying to get entity statement token from cache.',
112-
compact('uri'),
113-
);
118+
$entityStatement = parent::fromCache($uri);
114119

115-
$jws = $this->artifactFetcher->fromCacheAsString($uri);
116-
117-
if (!is_string($jws)) {
118-
$this->logger?->debug('Entity statement token not found in cache.', compact('uri'));
120+
if (is_null($entityStatement)) {
119121
return null;
120122
}
121123

122-
$this->logger?->debug(
123-
'Entity statement token found in cache, trying to build instance.',
124-
compact('uri'),
124+
if (is_a($entityStatement, EntityStatement::class)) {
125+
return $entityStatement;
126+
}
127+
128+
$message = 'Unexpected entity statement instance encountered for cache fetch.';
129+
$this->logger?->error(
130+
$message,
131+
compact('uri', 'entityStatement'),
125132
);
126133

127-
return $this->prepareEntityStatement($jws);
134+
throw new FetchException($message);
128135
}
129136

130137
/**
@@ -135,57 +142,18 @@ public function fromCache(string $uri): ?EntityStatement
135142
*/
136143
public function fromNetwork(string $uri): EntityStatement
137144
{
138-
$response = $this->artifactFetcher->fromNetwork($uri);
139-
140-
if ($response->getStatusCode() !== 200) {
141-
$message = sprintf(
142-
'Unexpected HTTP response for entity statement fetch, status code: %s, reason: %s. URI %s',
143-
$response->getStatusCode(),
144-
$response->getReasonPhrase(),
145-
$uri,
146-
);
147-
$this->logger?->error($message);
148-
throw new FetchException($message);
149-
}
145+
$entityStatement = parent::fromNetwork($uri);
150146

151-
/** @psalm-suppress InvalidLiteralArgument */
152-
if (
153-
!str_contains(
154-
$response->getHeaderLine(HttpHeadersEnum::ContentType->value),
155-
ContentTypesEnum::ApplicationEntityStatementJwt->value,
156-
)
157-
) {
158-
$message = sprintf(
159-
'Unexpected content type in response for entity statement fetch: %s, expected: %s. URI %s',
160-
$response->getHeaderLine(HttpHeadersEnum::ContentType->value),
161-
ContentTypesEnum::ApplicationEntityStatementJwt->value,
162-
$uri,
163-
);
164-
$this->logger?->error($message);
165-
throw new FetchException($message);
147+
if (is_a($entityStatement, EntityStatement::class)) {
148+
return $entityStatement;
166149
}
167150

168-
$token = $response->getBody()->getContents();
169-
$this->logger?->debug('Successful HTTP response for entity statement fetch.', compact('uri', 'token'));
170-
$this->logger?->debug('Proceeding to EntityStatement instance building.');
171-
172-
$entityStatement = $this->entityStatementFactory->fromToken($token);
173-
$this->logger?->debug('Entity Statement instance built, saving its token to cache.', compact('uri', 'token'));
174-
175-
$cacheTtl = $this->maxCacheDuration->lowestInSecondsComparedToExpirationTime(
176-
$entityStatement->getExpirationTime(),
151+
$message = 'Unexpected entity statement instance encountered for network fetch.';
152+
$this->logger?->error(
153+
$message,
154+
compact('uri', 'entityStatement'),
177155
);
178-
$this->artifactFetcher->cacheIt($token, $cacheTtl, $uri);
179156

180-
$this->logger?->debug('Returning built Entity Statement instance.', compact('uri', 'token'));
181-
return $entityStatement;
182-
}
183-
184-
/**
185-
* @throws \SimpleSAML\OpenID\Exceptions\JwsException
186-
*/
187-
protected function prepareEntityStatement(string $jws): EntityStatement
188-
{
189-
return $this->entityStatementFactory->fromToken($jws);
157+
throw new FetchException($message);
190158
}
191159
}

src/Jws/AbstractJwsFetcher.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SimpleSAML\OpenID\Jws;
6+
7+
use Psr\Log\LoggerInterface;
8+
use SimpleSAML\OpenID\Decorators\DateIntervalDecorator;
9+
use SimpleSAML\OpenID\Helpers;
10+
use SimpleSAML\OpenID\Jws\Factories\ParsedJwsFactory;
11+
use SimpleSAML\OpenID\Utils\ArtifactFetcher;
12+
13+
abstract class AbstractJwsFetcher
14+
{
15+
public function __construct(
16+
protected readonly ArtifactFetcher $artifactFetcher,
17+
protected readonly DateIntervalDecorator $maxCacheDuration,
18+
protected readonly Helpers $helpers,
19+
protected readonly ?LoggerInterface $logger = null,
20+
) {
21+
}
22+
23+
/**
24+
* @throws \SimpleSAML\OpenID\Exceptions\JwsException
25+
*/
26+
abstract protected function buildJwsInstance(string $token): ParsedJws;
27+
abstract protected function getExpectedContentTypeHttpHeader(): ?string;
28+
}

src/Jws/JwsFetcher.php

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SimpleSAML\OpenID\Jws;
6+
7+
use Psr\Log\LoggerInterface;
8+
use SimpleSAML\OpenID\Codebooks\HttpHeadersEnum;
9+
use SimpleSAML\OpenID\Decorators\DateIntervalDecorator;
10+
use SimpleSAML\OpenID\Exceptions\FetchException;
11+
use SimpleSAML\OpenID\Helpers;
12+
use SimpleSAML\OpenID\Jws\Factories\ParsedJwsFactory;
13+
use SimpleSAML\OpenID\Utils\ArtifactFetcher;
14+
15+
class JwsFetcher extends AbstractJwsFetcher
16+
{
17+
public function __construct(
18+
private readonly ParsedJwsFactory $parsedJwsFactory,
19+
ArtifactFetcher $artifactFetcher,
20+
DateIntervalDecorator $maxCacheDuration,
21+
Helpers $helpers,
22+
?LoggerInterface $logger = null,
23+
) {
24+
parent::__construct($artifactFetcher, $maxCacheDuration, $helpers, $logger);
25+
}
26+
27+
/**
28+
* @throws \SimpleSAML\OpenID\Exceptions\JwsException
29+
*/
30+
protected function buildJwsInstance(string $token): ParsedJws
31+
{
32+
return $this->parsedJwsFactory->fromToken($token);
33+
}
34+
35+
protected function getExpectedContentTypeHttpHeader(): ?string
36+
{
37+
return null;
38+
}
39+
40+
/**
41+
* @throws \SimpleSAML\OpenID\Exceptions\JwsException
42+
* @throws \SimpleSAML\OpenID\Exceptions\FetchException
43+
*/
44+
public function fromCacheOrNetwork(string $uri): ParsedJws
45+
{
46+
return $this->fromCache($uri) ?? $this->fromNetwork($uri);
47+
}
48+
49+
/**
50+
* Fetch JWS from cache, if available. URI is used as cache key.
51+
*
52+
* @throws \SimpleSAML\OpenID\Exceptions\JwsException
53+
*/
54+
public function fromCache(string $uri): ?ParsedJws
55+
{
56+
$this->logger?->debug(
57+
'Trying to get JWS token from cache.',
58+
compact('uri'),
59+
);
60+
61+
$jws = $this->artifactFetcher->fromCacheAsString($uri);
62+
63+
if (!is_string($jws)) {
64+
$this->logger?->debug('JWS token not found in cache.', compact('uri'));
65+
return null;
66+
}
67+
68+
$this->logger?->debug(
69+
'JWS token found in cache, trying to build instance.',
70+
compact('uri'),
71+
);
72+
73+
return $this->buildJwsInstance($jws);
74+
}
75+
76+
/**
77+
* Fetch JWS from network. Each successful fetch will be cached, with URI being used as a cache key.
78+
*
79+
* @throws \SimpleSAML\OpenID\Exceptions\FetchException
80+
* @throws \SimpleSAML\OpenID\Exceptions\JwsException
81+
*/
82+
public function fromNetwork(string $uri): ParsedJws
83+
{
84+
$this->logger?->debug(
85+
'Trying to fetch JWS token from network.',
86+
compact('uri'),
87+
);
88+
89+
$response = $this->artifactFetcher->fromNetwork($uri);
90+
91+
if ($response->getStatusCode() !== 200) {
92+
$message = sprintf(
93+
'Unexpected HTTP response for JWS fetch, status code: %s, reason: %s. URI %s',
94+
$response->getStatusCode(),
95+
$response->getReasonPhrase(),
96+
$uri,
97+
);
98+
$this->logger?->error($message);
99+
throw new FetchException($message);
100+
}
101+
102+
/** @psalm-suppress InvalidLiteralArgument */
103+
if (
104+
is_string($expectedContentTypeHttpHeader = $this->getExpectedContentTypeHttpHeader()) &&
105+
(!str_contains(
106+
$response->getHeaderLine(HttpHeadersEnum::ContentType->value),
107+
$expectedContentTypeHttpHeader,
108+
))
109+
) {
110+
$message = sprintf(
111+
'Unexpected content type in response for JWS fetch: %s, expected: %s. URI %s',
112+
$response->getHeaderLine(HttpHeadersEnum::ContentType->value),
113+
$expectedContentTypeHttpHeader,
114+
$uri,
115+
);
116+
$this->logger?->error($message);
117+
throw new FetchException($message);
118+
}
119+
120+
$token = $response->getBody()->getContents();
121+
$this->logger?->debug('Successful HTTP response for JWS fetch.', compact('uri', 'token'));
122+
$this->logger?->debug('Proceeding to JWS instance building.');
123+
124+
$jwsInstance = $this->buildJwsInstance($token);
125+
$this->logger?->debug('JWS instance built, saving its token to cache.', compact('uri', 'token'));
126+
127+
$cacheTtl = is_int($expirationTime = $jwsInstance->getExpirationTime()) ?
128+
$this->maxCacheDuration->lowestInSecondsComparedToExpirationTime(
129+
$expirationTime,
130+
) :
131+
$this->maxCacheDuration->getInSeconds();
132+
133+
$this->artifactFetcher->cacheIt($token, $cacheTtl, $uri);
134+
135+
$this->logger?->debug('Returning built JWS instance.', compact('uri', 'token'));
136+
137+
return $jwsInstance;
138+
}
139+
}

tests/src/Federation/EntityStatementFetcherTest.php

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace SimpleSAML\Test\OpenID\Federation;
66

77
use PHPUnit\Framework\Attributes\CoversClass;
8+
use PHPUnit\Framework\Attributes\UsesClass;
89
use PHPUnit\Framework\MockObject\MockObject;
910
use PHPUnit\Framework\TestCase;
1011
use Psr\Log\LoggerInterface;
@@ -13,42 +14,46 @@
1314
use SimpleSAML\OpenID\Federation\EntityStatementFetcher;
1415
use SimpleSAML\OpenID\Federation\Factories\EntityStatementFactory;
1516
use SimpleSAML\OpenID\Helpers;
17+
use SimpleSAML\OpenID\Jws\AbstractJwsFetcher;
18+
use SimpleSAML\OpenID\Jws\JwsFetcher;
1619
use SimpleSAML\OpenID\Utils\ArtifactFetcher;
1720

1821
#[CoversClass(EntityStatementFetcher::class)]
22+
#[UsesClass(AbstractJwsFetcher::class)]
23+
#[UsesClass(JwsFetcher::class)]
1924
class EntityStatementFetcherTest extends TestCase
2025
{
21-
protected MockObject $artifactFetcherMock;
2226
protected MockObject $entityStatementFactoryMock;
27+
protected MockObject $artifactFetcherMock;
2328
protected MockObject $maxCacheDurationMock;
2429
protected MockObject $helpersMock;
2530
protected MockObject $loggerMock;
2631

2732
protected function setUp(): void
2833
{
29-
$this->artifactFetcherMock = $this->createMock(ArtifactFetcher::class);
3034
$this->entityStatementFactoryMock = $this->createMock(EntityStatementFactory::class);
35+
$this->artifactFetcherMock = $this->createMock(ArtifactFetcher::class);
3136
$this->maxCacheDurationMock = $this->createMock(DateIntervalDecorator::class);
3237
$this->helpersMock = $this->createMock(Helpers::class);
3338
$this->loggerMock = $this->createMock(LoggerInterface::class);
3439
}
3540

3641
protected function sut(
37-
?ArtifactFetcher $artifactFetcher = null,
3842
?EntityStatementFactory $entityStatementFactory = null,
43+
?ArtifactFetcher $artifactFetcher = null,
3944
?DateIntervalDecorator $maxCacheDuration = null,
4045
?Helpers $helpers = null,
4146
?LoggerInterface $logger = null,
4247
): EntityStatementFetcher {
43-
$artifactFetcher ??= $this->artifactFetcherMock;
4448
$entityStatementFactory ??= $this->entityStatementFactoryMock;
49+
$artifactFetcher ??= $this->artifactFetcherMock;
4550
$maxCacheDuration ??= $this->maxCacheDurationMock;
4651
$helpers ??= $this->helpersMock;
4752
$logger ??= $this->loggerMock;
4853

4954
return new EntityStatementFetcher(
50-
$artifactFetcher,
5155
$entityStatementFactory,
56+
$artifactFetcher,
5257
$maxCacheDuration,
5358
$helpers,
5459
$logger,

0 commit comments

Comments
 (0)