Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
100.00% |
10 / 10 |
|
100.00% |
2 / 2 |
CRAP | |
100.00% |
1 / 1 |
| JwksEndpoint | |
100.00% |
10 / 10 |
|
100.00% |
2 / 2 |
4 | |
100.00% |
1 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| handle | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
3 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace LambdaTwelve\OneRecord\Auth; |
| 6 | |
| 7 | use LambdaTwelve\OneRecord\Auth\Jwt\Rs256Signer; |
| 8 | use Psr\Http\Message\ResponseFactoryInterface; |
| 9 | use Psr\Http\Message\ResponseInterface; |
| 10 | use Psr\Http\Message\ServerRequestInterface; |
| 11 | use Psr\Http\Message\StreamFactoryInterface; |
| 12 | use Psr\Http\Server\RequestHandlerInterface; |
| 13 | |
| 14 | /** |
| 15 | * Serves the JWKS document for the keys this host signs with, so partners |
| 16 | * can verify its tokens with JwksKeyResolver and key rotation needs no |
| 17 | * out-of-band exchange. Mount it at /.well-known/jwks.json (or wherever the |
| 18 | * issuer's metadata says). Several signers mean several keys: the current |
| 19 | * one and the one being retired. |
| 20 | */ |
| 21 | final class JwksEndpoint implements RequestHandlerInterface |
| 22 | { |
| 23 | /** @var list<Rs256Signer> */ |
| 24 | private readonly array $signers; |
| 25 | |
| 26 | /** |
| 27 | * @param int $maxAgeSeconds how long verifiers may cache the document; keep it shorter than a rotation overlap |
| 28 | */ |
| 29 | public function __construct( |
| 30 | private readonly ResponseFactoryInterface $responses, |
| 31 | private readonly StreamFactoryInterface $streams, |
| 32 | private readonly int $maxAgeSeconds = 3600, |
| 33 | Rs256Signer ...$signers, |
| 34 | ) { |
| 35 | $this->signers = array_values($signers); |
| 36 | } |
| 37 | |
| 38 | public function handle(ServerRequestInterface $request): ResponseInterface |
| 39 | { |
| 40 | if (!\in_array(strtoupper($request->getMethod()), ['GET', 'HEAD'], true)) { |
| 41 | return $this->responses->createResponse(405)->withHeader('Allow', 'GET, HEAD'); |
| 42 | } |
| 43 | $document = ['keys' => array_map(static fn(Rs256Signer $signer): array => $signer->publicJwk(), $this->signers)]; |
| 44 | $response = $this->responses->createResponse(200) |
| 45 | ->withHeader('Content-Type', 'application/json; charset=utf-8') |
| 46 | ->withHeader('Cache-Control', 'public, max-age=' . $this->maxAgeSeconds); |
| 47 | |
| 48 | return strtoupper($request->getMethod()) === 'HEAD' |
| 49 | ? $response |
| 50 | : $response->withBody($this->streams->createStream(json_encode($document, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES))); |
| 51 | } |
| 52 | } |