These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | |||
3 | declare(strict_types=1); |
||
4 | |||
5 | /* |
||
6 | * The MIT License (MIT) |
||
7 | * |
||
8 | * Copyright (c) 2014-2018 Spomky-Labs |
||
9 | * |
||
10 | * This software may be modified and distributed under the terms |
||
11 | * of the MIT license. See the LICENSE file for details. |
||
12 | */ |
||
13 | |||
14 | namespace OAuth2Framework\Component\OpenIdConnect; |
||
15 | |||
16 | use Base64Url\Base64Url; |
||
17 | use Jose\Component\Core\Converter\StandardConverter; |
||
18 | use Jose\Component\Core\JWK; |
||
19 | use Jose\Component\Core\JWKSet; |
||
20 | use Jose\Component\Encryption\JWEBuilder; |
||
21 | use Jose\Component\Encryption\Serializer\CompactSerializer as JweCompactSerializer; |
||
22 | use Jose\Component\KeyManagement\JKUFactory; |
||
23 | use Jose\Component\Signature\JWSBuilder; |
||
24 | use Jose\Component\Signature\Serializer\CompactSerializer as JwsCompactSerializer; |
||
25 | use OAuth2Framework\Component\AuthorizationCodeGrant\AuthorizationCodeId; |
||
26 | use OAuth2Framework\Component\AuthorizationCodeGrant\AuthorizationCodeRepository; |
||
27 | use OAuth2Framework\Component\Core\AccessToken\AccessToken; |
||
28 | use OAuth2Framework\Component\Core\AccessToken\AccessTokenId; |
||
29 | use OAuth2Framework\Component\Core\Client\Client; |
||
30 | use OAuth2Framework\Component\Core\Token\TokenId; |
||
31 | use OAuth2Framework\Component\Core\UserAccount\UserAccount; |
||
32 | use OAuth2Framework\Component\OpenIdConnect\UserInfo\UserInfo; |
||
33 | |||
34 | class IdTokenBuilder |
||
35 | { |
||
36 | private $issuer; |
||
37 | private $client; |
||
38 | private $userAccount; |
||
39 | private $redirectUri; |
||
40 | private $userinfo; |
||
41 | private $signatureKeys; |
||
42 | private $lifetime; |
||
43 | private $scope = null; |
||
44 | private $requestedClaims = []; |
||
45 | private $claimsLocales = null; |
||
46 | private $accessTokenId = null; |
||
47 | private $authorizationCodeId = null; |
||
48 | private $nonce = null; |
||
49 | private $withAuthenticationTime = false; |
||
50 | private $jwsBuilder = null; |
||
51 | private $signatureAlgorithm = null; |
||
52 | private $jweBuilder; |
||
53 | private $keyEncryptionAlgorithm = null; |
||
54 | private $contentEncryptionAlgorithm = null; |
||
55 | private $expiresAt = null; |
||
56 | private $jkuFactory = null; |
||
57 | private $authorizationCodeRepository = null; |
||
58 | |||
59 | public function __construct(string $issuer, UserInfo $userinfo, int $lifetime, Client $client, UserAccount $userAccount, string $redirectUri, ?JKUFactory $jkuFactory, ?AuthorizationCodeRepository $authorizationCodeRepository) |
||
60 | { |
||
61 | $this->issuer = $issuer; |
||
62 | $this->userinfo = $userinfo; |
||
63 | $this->lifetime = $lifetime; |
||
64 | $this->client = $client; |
||
65 | $this->userAccount = $userAccount; |
||
66 | $this->redirectUri = $redirectUri; |
||
67 | $this->jkuFactory = $jkuFactory; |
||
68 | $this->authorizationCodeRepository = $authorizationCodeRepository; |
||
69 | } |
||
70 | |||
71 | public function setAccessToken(AccessToken $accessToken): void |
||
72 | { |
||
73 | $this->accessTokenId = $accessToken->getTokenId(); |
||
74 | $this->expiresAt = $accessToken->getExpiresAt(); |
||
75 | $this->scope = $accessToken->getParameter()->has('scope') ? $accessToken->getParameter()->get('scope') : null; |
||
76 | |||
77 | if ($accessToken->getMetadata()->has('authorization_code_id') && null !== $this->authorizationCodeRepository) { |
||
78 | $authorizationCodeId = new AuthorizationCodeId($accessToken->getMetadata()->get('authorization_code_id')); |
||
79 | $authorizationCode = $this->authorizationCodeRepository->find($authorizationCodeId); |
||
80 | if (null === $authorizationCode) { |
||
81 | return; |
||
82 | } |
||
83 | $this->authorizationCodeId = $authorizationCodeId; |
||
84 | $queryParams = $authorizationCode->getQueryParams(); |
||
85 | foreach (['nonce' => 'nonce', 'claims_locales' => 'claimsLocales'] as $k => $v) { |
||
86 | if (\array_key_exists($k, $queryParams)) { |
||
87 | $this->$v = $queryParams[$k]; |
||
88 | } |
||
89 | } |
||
90 | $this->withAuthenticationTime = \array_key_exists('max_age', $authorizationCode->getQueryParams()); |
||
91 | } |
||
92 | } |
||
93 | |||
94 | public function withAccessTokenId(AccessTokenId $accessTokenId): void |
||
95 | { |
||
96 | $this->accessTokenId = $accessTokenId; |
||
97 | } |
||
98 | |||
99 | public function withAuthorizationCodeId(AuthorizationCodeId $authorizationCodeId): void |
||
100 | { |
||
101 | $this->authorizationCodeId = $authorizationCodeId; |
||
102 | } |
||
103 | |||
104 | public function withClaimsLocales(string $claimsLocales): void |
||
105 | { |
||
106 | $this->claimsLocales = $claimsLocales; |
||
107 | } |
||
108 | |||
109 | public function withAuthenticationTime(): void |
||
110 | { |
||
111 | $this->withAuthenticationTime = true; |
||
112 | } |
||
113 | |||
114 | public function withScope(string $scope): void |
||
115 | { |
||
116 | $this->scope = $scope; |
||
117 | } |
||
118 | |||
119 | public function withRequestedClaims(array $requestedClaims): void |
||
120 | { |
||
121 | $this->requestedClaims = $requestedClaims; |
||
122 | } |
||
123 | |||
124 | public function withNonce(string $nonce): void |
||
125 | { |
||
126 | $this->nonce = $nonce; |
||
127 | } |
||
128 | |||
129 | public function withExpirationAt(\DateTimeImmutable $expiresAt): void |
||
130 | { |
||
131 | $this->expiresAt = $expiresAt; |
||
132 | } |
||
133 | |||
134 | public function withoutAuthenticationTime(): void |
||
135 | { |
||
136 | $this->withAuthenticationTime = false; |
||
137 | } |
||
138 | |||
139 | public function withSignature(JWSBuilder $jwsBuilder, JWKSet $signatureKeys, string $signatureAlgorithm): void |
||
140 | { |
||
141 | if (!\in_array($signatureAlgorithm, $jwsBuilder->getSignatureAlgorithmManager()->list(), true)) { |
||
142 | throw new \InvalidArgumentException(\sprintf('Unsupported signature algorithm "%s". Please use one of the following one: %s', $signatureAlgorithm, \implode(', ', $jwsBuilder->getSignatureAlgorithmManager()->list()))); |
||
143 | } |
||
144 | if (0 === $signatureKeys->count()) { |
||
145 | throw new \InvalidArgumentException('The signature key set must contain at least one key.'); |
||
146 | } |
||
147 | $this->jwsBuilder = $jwsBuilder; |
||
148 | $this->signatureKeys = $signatureKeys; |
||
149 | $this->signatureAlgorithm = $signatureAlgorithm; |
||
150 | } |
||
151 | |||
152 | public function withEncryption(JWEBuilder $jweBuilder, string $keyEncryptionAlgorithm, string $contentEncryptionAlgorithm): void |
||
153 | { |
||
154 | if (!\in_array($keyEncryptionAlgorithm, $jweBuilder->getKeyEncryptionAlgorithmManager()->list(), true)) { |
||
155 | throw new \InvalidArgumentException(\sprintf('Unsupported key encryption algorithm "%s". Please use one of the following one: %s', $keyEncryptionAlgorithm, \implode(', ', $jweBuilder->getKeyEncryptionAlgorithmManager()->list()))); |
||
156 | } |
||
157 | if (!\in_array($contentEncryptionAlgorithm, $jweBuilder->getContentEncryptionAlgorithmManager()->list(), true)) { |
||
158 | throw new \InvalidArgumentException(\sprintf('Unsupported content encryption algorithm "%s". Please use one of the following one: %s', $contentEncryptionAlgorithm, \implode(', ', $jweBuilder->getContentEncryptionAlgorithmManager()->list()))); |
||
159 | } |
||
160 | $this->jweBuilder = $jweBuilder; |
||
161 | $this->keyEncryptionAlgorithm = $keyEncryptionAlgorithm; |
||
162 | $this->contentEncryptionAlgorithm = $contentEncryptionAlgorithm; |
||
163 | } |
||
164 | |||
165 | public function build(): string |
||
166 | { |
||
167 | if (null === $this->scope) { |
||
168 | throw new \LogicException('It is mandatory to set the scope.'); |
||
169 | } |
||
170 | $data = $this->userinfo->getUserinfo($this->client, $this->userAccount, $this->redirectUri, $this->requestedClaims, $this->scope, $this->claimsLocales); |
||
171 | //$data = $this->updateClaimsWithAmrAndAcrInfo($data, $this->userAccount); |
||
172 | //$data = $this->updateClaimsWithAuthenticationTime($data, $this->userAccount, $this->requestedClaims); |
||
0 ignored issues
–
show
|
|||
173 | $data = $this->updateClaimsWithNonce($data); |
||
174 | if (null !== $this->signatureAlgorithm) { |
||
175 | $data = $this->updateClaimsWithJwtClaims($data); |
||
176 | $data = $this->updateClaimsWithTokenHash($data); |
||
177 | $data = $this->updateClaimsAudience($data); |
||
178 | $result = $this->computeIdToken($data); |
||
179 | } else { |
||
180 | $result = \json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); |
||
181 | } |
||
182 | |||
183 | if (null !== $this->keyEncryptionAlgorithm && null !== $this->contentEncryptionAlgorithm) { |
||
184 | $result = $this->tryToEncrypt($this->client, $result); |
||
185 | } |
||
186 | |||
187 | return $result; |
||
188 | } |
||
189 | |||
190 | private function updateClaimsWithJwtClaims(array $claims): array |
||
191 | { |
||
192 | if (null === $this->expiresAt) { |
||
193 | $this->expiresAt = (new \DateTimeImmutable())->setTimestamp(\time() + $this->lifetime); |
||
194 | } |
||
195 | $claims += [ |
||
196 | 'iat' => \time(), |
||
197 | 'nbf' => \time(), |
||
198 | 'exp' => $this->expiresAt->getTimestamp(), |
||
199 | 'jti' => Base64Url::encode(\random_bytes(16)), |
||
200 | 'iss' => $this->issuer, |
||
201 | ]; |
||
202 | |||
203 | return $claims; |
||
204 | } |
||
205 | |||
206 | private function updateClaimsWithAuthenticationTime(array $claims, UserAccount $userAccount, array $requestedClaims): array |
||
0 ignored issues
–
show
|
|||
207 | { |
||
208 | if ((true === $this->withAuthenticationTime || \array_key_exists('auth_time', $requestedClaims)) && null !== $userAccount->getLastLoginAt()) { |
||
209 | $claims['auth_time'] = $userAccount->getLastLoginAt(); |
||
210 | } |
||
211 | |||
212 | return $claims; |
||
213 | } |
||
214 | |||
215 | private function updateClaimsWithNonce(array $claims): array |
||
216 | { |
||
217 | if (null !== $this->nonce) { |
||
218 | $claims['nonce'] = $this->nonce; |
||
219 | } |
||
220 | |||
221 | return $claims; |
||
222 | } |
||
223 | |||
224 | private function updateClaimsAudience(array $claims): array |
||
225 | { |
||
226 | $claims['aud'] = [ |
||
227 | $this->client->getPublicId()->getValue(), |
||
228 | $this->issuer, |
||
229 | ]; |
||
230 | $claims['azp'] = $this->client->getPublicId()->getValue(); |
||
231 | |||
232 | return $claims; |
||
233 | } |
||
234 | |||
235 | private function computeIdToken(array $claims): string |
||
236 | { |
||
237 | $signatureKey = $this->getSignatureKey($this->signatureAlgorithm); |
||
238 | $header = $this->getHeaders($signatureKey, $this->signatureAlgorithm); |
||
239 | $jsonConverter = new StandardConverter(); |
||
240 | $claims = $jsonConverter->encode($claims); |
||
241 | $jws = $this->jwsBuilder |
||
242 | ->create() |
||
243 | ->withPayload($claims) |
||
244 | ->addSignature($signatureKey, $header) |
||
245 | ->build(); |
||
246 | $serializer = new JwsCompactSerializer($jsonConverter); |
||
247 | |||
248 | return $serializer->serialize($jws, 0); |
||
249 | } |
||
250 | |||
251 | private function tryToEncrypt(Client $client, string $jwt): string |
||
252 | { |
||
253 | $clientKeySet = $this->getClientKeySet($client); |
||
254 | $keyEncryptionAlgorithm = $this->jweBuilder->getKeyEncryptionAlgorithmManager()->get($this->keyEncryptionAlgorithm); |
||
255 | $encryptionKey = $clientKeySet->selectKey('enc', $keyEncryptionAlgorithm); |
||
256 | if (null === $encryptionKey) { |
||
257 | throw new \InvalidArgumentException('No encryption key available for the client.'); |
||
258 | } |
||
259 | $header = [ |
||
260 | 'typ' => 'JWT', |
||
261 | 'jti' => Base64Url::encode(\random_bytes(16)), |
||
262 | 'alg' => $this->keyEncryptionAlgorithm, |
||
263 | 'enc' => $this->contentEncryptionAlgorithm, |
||
264 | ]; |
||
265 | $jwe = $this->jweBuilder |
||
266 | ->create() |
||
267 | ->withPayload($jwt) |
||
268 | ->withSharedProtectedHeader($header) |
||
269 | ->addRecipient($encryptionKey) |
||
270 | ->build(); |
||
271 | $jsonConverter = new StandardConverter(); |
||
272 | $serializer = new JweCompactSerializer($jsonConverter); |
||
273 | |||
274 | return $serializer->serialize($jwe, 0); |
||
275 | } |
||
276 | |||
277 | private function getSignatureKey(string $signatureAlgorithm): JWK |
||
278 | { |
||
279 | $keys = $this->signatureKeys; |
||
280 | if ($this->client->has('client_secret')) { |
||
281 | $jwk = JWK::create([ |
||
282 | 'kty' => 'oct', |
||
283 | 'use' => 'sig', |
||
284 | 'k' => Base64Url::encode($this->client->get('client_secret')), |
||
285 | ]); |
||
286 | $keys = $keys->with($jwk); |
||
287 | } |
||
288 | $signatureAlgorithm = $this->jwsBuilder->getSignatureAlgorithmManager()->get($signatureAlgorithm); |
||
289 | if ('none' === $signatureAlgorithm->name()) { |
||
290 | return JWK::create(['kty' => 'none', 'alg' => 'none', 'use' => 'sig']); |
||
291 | } |
||
292 | $signatureKey = $keys->selectKey('sig', $signatureAlgorithm); |
||
293 | if (null === $signatureKey) { |
||
294 | throw new \InvalidArgumentException('Unable to find a key to sign the ID Token. Please verify the selected key set contains suitable keys.'); |
||
295 | } |
||
296 | |||
297 | return $signatureKey; |
||
298 | } |
||
299 | |||
300 | private function getHeaders(JWK $signatureKey, string $signatureAlgorithm): array |
||
301 | { |
||
302 | $header = [ |
||
303 | 'typ' => 'JWT', |
||
304 | 'alg' => $signatureAlgorithm, |
||
305 | ]; |
||
306 | if ($signatureKey->has('kid')) { |
||
307 | $header['kid'] = $signatureKey->get('kid'); |
||
308 | } |
||
309 | |||
310 | return $header; |
||
311 | } |
||
312 | |||
313 | private function updateClaimsWithTokenHash(array $claims): array |
||
314 | { |
||
315 | if ('none' === $this->signatureAlgorithm) { |
||
316 | return $claims; |
||
317 | } |
||
318 | if (null !== $this->accessTokenId) { |
||
319 | $claims['at_hash'] = $this->getHash($this->accessTokenId); |
||
320 | } |
||
321 | if (null !== $this->authorizationCodeId) { |
||
322 | $claims['c_hash'] = $this->getHash($this->authorizationCodeId); |
||
323 | } |
||
324 | |||
325 | return $claims; |
||
326 | } |
||
327 | |||
328 | private function getHash(TokenId $tokenId): string |
||
329 | { |
||
330 | return Base64Url::encode(\mb_substr(\hash($this->getHashMethod(), $tokenId->getValue(), true), 0, $this->getHashSize(), '8bit')); |
||
331 | } |
||
332 | |||
333 | private function getHashMethod(): string |
||
334 | { |
||
335 | $map = [ |
||
336 | 'HS256' => 'sha256', |
||
337 | 'ES256' => 'sha256', |
||
338 | 'RS256' => 'sha256', |
||
339 | 'PS256' => 'sha256', |
||
340 | 'HS384' => 'sha384', |
||
341 | 'ES384' => 'sha384', |
||
342 | 'RS384' => 'sha384', |
||
343 | 'PS384' => 'sha384', |
||
344 | 'HS512' => 'sha512', |
||
345 | 'ES512' => 'sha512', |
||
346 | 'RS512' => 'sha512', |
||
347 | 'PS512' => 'sha512', |
||
348 | ]; |
||
349 | |||
350 | if (!\array_key_exists($this->signatureAlgorithm, $map)) { |
||
351 | throw new \InvalidArgumentException(\sprintf('Algorithm "%s" is not supported', $this->signatureAlgorithm)); |
||
352 | } |
||
353 | |||
354 | return $map[$this->signatureAlgorithm]; |
||
355 | } |
||
356 | |||
357 | private function getHashSize(): int |
||
358 | { |
||
359 | $map = [ |
||
360 | 'HS256' => 16, |
||
361 | 'ES256' => 16, |
||
362 | 'RS256' => 16, |
||
363 | 'PS256' => 16, |
||
364 | 'HS384' => 24, |
||
365 | 'ES384' => 24, |
||
366 | 'RS384' => 24, |
||
367 | 'PS384' => 24, |
||
368 | 'HS512' => 32, |
||
369 | 'ES512' => 32, |
||
370 | 'RS512' => 32, |
||
371 | 'PS512' => 32, |
||
372 | ]; |
||
373 | |||
374 | if (!\array_key_exists($this->signatureAlgorithm, $map)) { |
||
375 | throw new \InvalidArgumentException(\sprintf('Algorithm "%s" is not supported', $this->signatureAlgorithm)); |
||
376 | } |
||
377 | |||
378 | return $map[$this->signatureAlgorithm]; |
||
379 | } |
||
380 | |||
381 | private function getClientKeySet(Client $client): JWKSet |
||
382 | { |
||
383 | $keyset = JWKSet::createFromKeys([]); |
||
384 | if ($client->has('jwks')) { |
||
385 | $jwks = JWKSet::createFromJson($client->get('jwks')); |
||
386 | foreach ($jwks as $jwk) { |
||
387 | $keyset = $keyset->with($jwk); |
||
388 | } |
||
389 | } |
||
390 | if ($client->has('client_secret')) { |
||
391 | $jwk = JWK::create([ |
||
392 | 'kty' => 'oct', |
||
393 | 'use' => 'enc', |
||
394 | 'k' => Base64Url::encode($client->get('client_secret')), |
||
395 | ]); |
||
396 | $keyset = $keyset->with($jwk); |
||
397 | } |
||
398 | if ($client->has('jwks_uri') && null !== $this->jkuFactory) { |
||
399 | $jwksUri = $this->jkuFactory->loadFromUrl($client->get('jwks_uri')); |
||
400 | foreach ($jwksUri as $jwk) { |
||
401 | $keyset = $keyset->with($jwk); |
||
402 | } |
||
403 | } |
||
404 | |||
405 | if (empty($keyset)) { |
||
406 | throw new \InvalidArgumentException('The client has no key or key set.'); |
||
407 | } |
||
408 | |||
409 | return $keyset; |
||
410 | } |
||
411 | } |
||
412 |
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.
The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.
This check looks for comments that seem to be mostly valid code and reports them.