Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
JwtAuthenticator
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
3 / 3
8
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 authenticate
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
5
 bearerToken
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace LambdaTwelve\OneRecord\Auth;
6
7use InvalidArgumentException;
8use LambdaTwelve\OneRecord\Auth\Jwt\JwtException;
9use LambdaTwelve\OneRecord\Auth\Jwt\Rs256Verifier;
10use LambdaTwelve\OneRecord\Rdf\Iri;
11use LambdaTwelve\OneRecord\Server\Spi\Agent;
12use LambdaTwelve\OneRecord\Server\Spi\Authenticator;
13use Psr\Http\Message\ServerRequestInterface;
14use Psr\Log\LoggerInterface;
15use Psr\Log\NullLogger;
16
17/**
18 * The spec's authentication: a bearer JWT from a trusted issuer whose
19 * logistics_agent_uri claim names the calling organisation. Tokens without
20 * that claim identify nobody on the ONE Record network and are refused, as
21 * NE:ONE does.
22 */
23final class JwtAuthenticator implements Authenticator
24{
25    public function __construct(
26        private readonly Rs256Verifier $verifier,
27        private readonly LoggerInterface $logger = new NullLogger(),
28    ) {}
29
30    public function authenticate(ServerRequestInterface $request): ?Agent
31    {
32        $token = self::bearerToken($request);
33        if ($token === null) {
34            return null;
35        }
36        try {
37            $claims = $this->verifier->verify($token);
38        } catch (JwtException $e) {
39            $this->logger->info('Bearer token refused', ['reason' => $e->reason]);
40
41            return null;
42        }
43        $agentUri = $claims->logisticsAgentUri();
44        if ($agentUri === null) {
45            $this->logger->info('Bearer token refused', ['reason' => 'missing_logistics_agent_uri', 'issuer' => $claims->issuer()]);
46
47            return null;
48        }
49        try {
50            $iri = new Iri($agentUri);
51        } catch (InvalidArgumentException) {
52            $this->logger->info('Bearer token refused', ['reason' => 'invalid_logistics_agent_uri', 'issuer' => $claims->issuer()]);
53
54            return null;
55        }
56
57        return new Agent($iri, $claims->issuer(), $claims->all);
58    }
59
60    public static function bearerToken(ServerRequestInterface $request): ?string
61    {
62        $header = $request->getHeaderLine('Authorization');
63        if (preg_match('/^Bearer\s+([A-Za-z0-9._~+\/=-]+)\s*$/i', $header, $m) !== 1) {
64            return null;
65        }
66
67        return $m[1];
68    }
69}