1 | <?php |
||
2 | |||
3 | declare(strict_types=1); |
||
4 | |||
5 | namespace Facile\OpenIDClient\Service; |
||
6 | |||
7 | use Facile\OpenIDClient\Client\ClientInterface as OpenIDClient; |
||
8 | use Facile\OpenIDClient\Exception\InvalidArgumentException; |
||
9 | use Facile\OpenIDClient\Exception\OAuth2Exception; |
||
10 | use Facile\OpenIDClient\Exception\RuntimeException; |
||
11 | use Facile\OpenIDClient\Token\TokenSetInterface; |
||
12 | use Facile\OpenIDClient\Token\TokenVerifierBuilderInterface; |
||
13 | use function http_build_query; |
||
14 | use function is_array; |
||
15 | use function json_decode; |
||
16 | use Psr\Http\Client\ClientExceptionInterface; |
||
17 | use Psr\Http\Client\ClientInterface; |
||
18 | use Psr\Http\Message\RequestFactoryInterface; |
||
19 | use function sprintf; |
||
20 | |||
21 | /** |
||
22 | * @psalm-import-type TokenSetClaimsType from TokenSetInterface |
||
23 | */ |
||
24 | final class UserInfoService |
||
25 | { |
||
26 | /** @var ClientInterface */ |
||
27 | private $client; |
||
28 | |||
29 | /** @var RequestFactoryInterface */ |
||
30 | private $requestFactory; |
||
31 | |||
32 | /** @var TokenVerifierBuilderInterface */ |
||
33 | private $userInfoVerifierBuilder; |
||
34 | |||
35 | public function __construct( |
||
36 | TokenVerifierBuilderInterface $userInfoVerifierBuilder, |
||
37 | ClientInterface $client, |
||
38 | RequestFactoryInterface $requestFactory |
||
39 | ) { |
||
40 | $this->userInfoVerifierBuilder = $userInfoVerifierBuilder; |
||
41 | $this->client = $client; |
||
42 | $this->requestFactory = $requestFactory; |
||
43 | } |
||
44 | |||
45 | /** |
||
46 | * @param OpenIDClient $client |
||
47 | * @param TokenSetInterface $tokenSet |
||
48 | * @param bool $useBody |
||
49 | * |
||
50 | * @return array<string, mixed> |
||
51 | */ |
||
52 | public function getUserInfo(OpenIDClient $client, TokenSetInterface $tokenSet, bool $useBody = false): array |
||
53 | { |
||
54 | $accessToken = $tokenSet->getAccessToken(); |
||
55 | |||
56 | if (null === $accessToken) { |
||
57 | throw new RuntimeException('Unable to get an access token from the token set'); |
||
58 | } |
||
59 | |||
60 | $clientMetadata = $client->getMetadata(); |
||
61 | $issuerMetadata = $client->getIssuer()->getMetadata(); |
||
62 | |||
63 | $mTLS = true === $clientMetadata->get('tls_client_certificate_bound_access_tokens'); |
||
64 | |||
65 | $endpointUri = $issuerMetadata->getUserinfoEndpoint(); |
||
66 | |||
67 | if ($mTLS) { |
||
68 | $endpointUri = $issuerMetadata->getMtlsEndpointAliases()['userinfo_endpoint'] ?? $endpointUri; |
||
69 | } |
||
70 | |||
71 | if (null === $endpointUri) { |
||
72 | throw new InvalidArgumentException('Invalid issuer userinfo endpoint'); |
||
73 | } |
||
74 | |||
75 | $expectJwt = null !== $clientMetadata->getUserinfoSignedResponseAlg() |
||
76 | || null !== $clientMetadata->getUserinfoEncryptedResponseAlg() |
||
77 | || null !== $clientMetadata->getUserinfoEncryptedResponseEnc(); |
||
78 | |||
79 | if ($useBody) { |
||
80 | $request = $this->requestFactory->createRequest('POST', $endpointUri) |
||
81 | ->withHeader('accept', $expectJwt ? 'application/jwt' : 'application/json') |
||
82 | ->withHeader('content-type', 'application/x-www-form-urlencoded'); |
||
83 | $request->getBody()->write(http_build_query(['access_token' => $accessToken])); |
||
84 | } else { |
||
85 | $request = $this->requestFactory->createRequest('GET', $endpointUri) |
||
86 | ->withHeader('accept', $expectJwt ? 'application/jwt' : 'application/json') |
||
87 | ->withHeader('authorization', 'Bearer ' . $accessToken); |
||
88 | } |
||
89 | |||
90 | $httpClient = $client->getHttpClient() ?? $this->client; |
||
91 | |||
92 | try { |
||
93 | $response = $httpClient->sendRequest($request); |
||
94 | } catch (ClientExceptionInterface $e) { |
||
95 | throw new RuntimeException('Unable to get userinfo', 0, $e); |
||
96 | } |
||
97 | |||
98 | if (200 !== $response->getStatusCode()) { |
||
99 | throw OAuth2Exception::fromResponse($response); |
||
100 | } |
||
101 | |||
102 | if ($expectJwt) { |
||
103 | /** @var TokenSetClaimsType $payload */ |
||
104 | $payload = $this->userInfoVerifierBuilder->build($client) |
||
105 | ->verify((string) $response->getBody()); |
||
106 | } else { |
||
107 | /** @var false|TokenSetClaimsType $payload */ |
||
108 | $payload = json_decode((string) $response->getBody(), true); |
||
109 | } |
||
110 | |||
111 | if (! is_array($payload)) { |
||
0 ignored issues
–
show
introduced
by
![]() |
|||
112 | throw new RuntimeException('Unable to parse userinfo claims'); |
||
113 | } |
||
114 | |||
115 | $idToken = $tokenSet->getIdToken(); |
||
116 | |||
117 | if (null === $idToken) { |
||
118 | return $payload; |
||
119 | } |
||
120 | |||
121 | // check expected sub |
||
122 | /** @var string|null $expectedSub */ |
||
123 | $expectedSub = $tokenSet->claims()['sub'] ?? null; |
||
124 | |||
125 | if (null === $expectedSub) { |
||
126 | throw new RuntimeException('Unable to get sub claim from id_token'); |
||
127 | } |
||
128 | |||
129 | if ($expectedSub !== ($payload['sub'] ?? null)) { |
||
130 | throw new RuntimeException( |
||
131 | sprintf('Userinfo sub mismatch, expected %s, got: %s', $expectedSub, $payload['sub'] ?? '') |
||
132 | ); |
||
133 | } |
||
134 | |||
135 | return $payload; |
||
136 | } |
||
137 | } |
||
138 |