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\Signature\JWSBuilder; |
22
|
|
|
use Jose\Component\Signature\Serializer\CompactSerializer as JwsCompactSerializer; |
23
|
|
|
use Jose\Component\Encryption\Serializer\CompactSerializer as JweCompactSerializer; |
24
|
|
|
use OAuth2Framework\Component\AuthorizationCodeGrant\AuthorizationCodeId; |
25
|
|
|
use OAuth2Framework\Component\Core\AccessToken\AccessToken; |
26
|
|
|
use OAuth2Framework\Component\Core\AccessToken\AccessTokenId; |
27
|
|
|
use OAuth2Framework\Component\Core\Client\Client; |
28
|
|
|
use OAuth2Framework\Component\Core\Token\TokenId; |
29
|
|
|
use OAuth2Framework\Component\Core\UserAccount\UserAccount; |
30
|
|
|
use OAuth2Framework\Component\OpenIdConnect\UserInfo\UserInfo; |
31
|
|
|
|
32
|
|
|
final class IdTokenBuilder |
33
|
|
|
{ |
34
|
|
|
/** |
35
|
|
|
* @var string |
36
|
|
|
*/ |
37
|
|
|
private $issuer; |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* @var Client |
41
|
|
|
*/ |
42
|
|
|
private $client; |
43
|
|
|
|
44
|
|
|
/** |
45
|
|
|
* @var UserAccount |
46
|
|
|
*/ |
47
|
|
|
private $userAccount; |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* @var string |
51
|
|
|
*/ |
52
|
|
|
private $redirectUri; |
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* @var UserInfo |
56
|
|
|
*/ |
57
|
|
|
private $userinfo; |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* @var JWKSet |
61
|
|
|
*/ |
62
|
|
|
private $signatureKeys; |
63
|
|
|
|
64
|
|
|
/** |
65
|
|
|
* @var int |
66
|
|
|
*/ |
67
|
|
|
private $lifetime; |
68
|
|
|
|
69
|
|
|
/** |
70
|
|
|
* @var string[] |
71
|
|
|
*/ |
72
|
|
|
private $scopes = []; |
73
|
|
|
|
74
|
|
|
/** |
75
|
|
|
* @var array |
76
|
|
|
*/ |
77
|
|
|
private $requestedClaims = []; |
78
|
|
|
|
79
|
|
|
/** |
80
|
|
|
* @var string|null |
81
|
|
|
*/ |
82
|
|
|
private $claimsLocales = null; |
83
|
|
|
|
84
|
|
|
/** |
85
|
|
|
* @var AccessTokenId|null |
86
|
|
|
*/ |
87
|
|
|
private $accessTokenId = null; |
88
|
|
|
|
89
|
|
|
/** |
90
|
|
|
* @var AuthorizationCodeId|null |
91
|
|
|
*/ |
92
|
|
|
private $authorizationCodeId = null; |
93
|
|
|
|
94
|
|
|
/** |
95
|
|
|
* @var string|null |
96
|
|
|
*/ |
97
|
|
|
private $nonce = null; |
98
|
|
|
|
99
|
|
|
/** |
100
|
|
|
* @var bool |
101
|
|
|
*/ |
102
|
|
|
private $withAuthenticationTime = false; |
103
|
|
|
|
104
|
|
|
/** |
105
|
|
|
* @var JWSBuilder|null |
106
|
|
|
*/ |
107
|
|
|
private $jwsBuilder = null; |
108
|
|
|
|
109
|
|
|
/** |
110
|
|
|
* @var string|null |
111
|
|
|
*/ |
112
|
|
|
private $signatureAlgorithm = null; |
113
|
|
|
|
114
|
|
|
/** |
115
|
|
|
* @var JWEBuilder|null |
116
|
|
|
*/ |
117
|
|
|
private $jweBuilder; |
118
|
|
|
|
119
|
|
|
/** |
120
|
|
|
* @var string|null |
121
|
|
|
*/ |
122
|
|
|
private $keyEncryptionAlgorithm = null; |
123
|
|
|
|
124
|
|
|
/** |
125
|
|
|
* @var string|null |
126
|
|
|
*/ |
127
|
|
|
private $contentEncryptionAlgorithm = null; |
128
|
|
|
|
129
|
|
|
/** |
130
|
|
|
* @var \DateTimeImmutable|null |
131
|
|
|
*/ |
132
|
|
|
private $expiresAt = null; |
133
|
|
|
|
134
|
|
|
/** |
135
|
|
|
* IdTokenBuilder constructor. |
136
|
|
|
* |
137
|
|
|
* @param string $issuer |
138
|
|
|
* @param UserInfo $userinfo |
139
|
|
|
* @param int $lifetime |
140
|
|
|
* @param Client $client |
141
|
|
|
* @param UserAccount $userAccount |
142
|
|
|
* @param string $redirectUri |
143
|
|
|
*/ |
144
|
|
|
private function __construct(string $issuer, UserInfo $userinfo, int $lifetime, Client $client, UserAccount $userAccount, string $redirectUri) |
145
|
|
|
{ |
146
|
|
|
$this->issuer = $issuer; |
147
|
|
|
$this->userinfo = $userinfo; |
148
|
|
|
$this->lifetime = $lifetime; |
149
|
|
|
$this->client = $client; |
150
|
|
|
$this->userAccount = $userAccount; |
151
|
|
|
$this->redirectUri = $redirectUri; |
152
|
|
|
} |
153
|
|
|
|
154
|
|
|
/** |
155
|
|
|
* @param string $issuer |
156
|
|
|
* @param UserInfo $userinfo |
157
|
|
|
* @param int $lifetime |
158
|
|
|
* @param Client $client |
159
|
|
|
* @param UserAccount $userAccount |
160
|
|
|
* @param string $redirectUri |
161
|
|
|
* |
162
|
|
|
* @return IdTokenBuilder |
163
|
|
|
*/ |
164
|
|
|
public static function create(string $issuer, UserInfo $userinfo, int $lifetime, Client $client, UserAccount $userAccount, string $redirectUri) |
165
|
|
|
{ |
166
|
|
|
return new self($issuer, $userinfo, $lifetime, $client, $userAccount, $redirectUri); |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
/** |
170
|
|
|
* @param AccessToken $accessToken |
171
|
|
|
* |
172
|
|
|
* @return IdTokenBuilder |
173
|
|
|
*/ |
174
|
|
|
public function withAccessToken(AccessToken $accessToken): self |
175
|
|
|
{ |
176
|
|
|
$clone = clone $this; |
177
|
|
|
$clone->accessTokenId = $accessToken->getTokenId(); |
178
|
|
|
$clone->expiresAt = $accessToken->getExpiresAt(); |
179
|
|
|
$clone->scopes = $accessToken->getScopes(); |
180
|
|
|
|
181
|
|
|
if ($accessToken->hasMetadata('code')) { |
182
|
|
|
$authorizationCode = $accessToken->getMetadata('code'); |
183
|
|
|
$clone->authorizationCodeId = $authorizationCode->getTokenId(); |
184
|
|
|
$queryParams = $authorizationCode->getQueryParams(); |
185
|
|
|
foreach (['nonce' => 'nonce', 'claims_locales' => 'claimsLocales'] as $k => $v) { |
186
|
|
|
if (array_key_exists($k, $queryParams)) { |
187
|
|
|
$clone->$v = $queryParams[$k]; |
188
|
|
|
} |
189
|
|
|
} |
190
|
|
|
$clone->withAuthenticationTime = array_key_exists('max_age', $authorizationCode->getQueryParams()); |
191
|
|
|
} |
192
|
|
|
|
193
|
|
|
return $clone; |
194
|
|
|
} |
195
|
|
|
|
196
|
|
|
/** |
197
|
|
|
* @param AccessTokenId $accessTokenId |
198
|
|
|
* |
199
|
|
|
* @return IdTokenBuilder |
200
|
|
|
*/ |
201
|
|
|
public function withAccessTokenId(AccessTokenId $accessTokenId): self |
202
|
|
|
{ |
203
|
|
|
$clone = clone $this; |
204
|
|
|
$clone->accessTokenId = $accessTokenId; |
205
|
|
|
|
206
|
|
|
return $clone; |
207
|
|
|
} |
208
|
|
|
|
209
|
|
|
/** |
210
|
|
|
* @param AuthorizationCodeId $authorizationCodeId |
211
|
|
|
* |
212
|
|
|
* @return IdTokenBuilder |
213
|
|
|
*/ |
214
|
|
|
public function withAuthorizationCodeId(AuthorizationCodeId $authorizationCodeId): self |
215
|
|
|
{ |
216
|
|
|
$clone = clone $this; |
217
|
|
|
$clone->authorizationCodeId = $authorizationCodeId; |
218
|
|
|
|
219
|
|
|
return $clone; |
220
|
|
|
} |
221
|
|
|
|
222
|
|
|
/** |
223
|
|
|
* @param string $claimsLocales |
224
|
|
|
* |
225
|
|
|
* @return IdTokenBuilder |
226
|
|
|
*/ |
227
|
|
|
public function withClaimsLocales(string $claimsLocales): self |
228
|
|
|
{ |
229
|
|
|
$clone = clone $this; |
230
|
|
|
$clone->claimsLocales = $claimsLocales; |
231
|
|
|
|
232
|
|
|
return $clone; |
233
|
|
|
} |
234
|
|
|
|
235
|
|
|
/** |
236
|
|
|
* @return IdTokenBuilder |
237
|
|
|
*/ |
238
|
|
|
public function withAuthenticationTime(): self |
239
|
|
|
{ |
240
|
|
|
$clone = clone $this; |
241
|
|
|
$clone->withAuthenticationTime = true; |
242
|
|
|
|
243
|
|
|
return $clone; |
244
|
|
|
} |
245
|
|
|
|
246
|
|
|
/** |
247
|
|
|
* @param string[] $scopes |
248
|
|
|
* |
249
|
|
|
* @return IdTokenBuilder |
250
|
|
|
*/ |
251
|
|
|
public function withScope(array $scopes): self |
252
|
|
|
{ |
253
|
|
|
$clone = clone $this; |
254
|
|
|
$clone->scopes = $scopes; |
255
|
|
|
|
256
|
|
|
return $clone; |
257
|
|
|
} |
258
|
|
|
|
259
|
|
|
/** |
260
|
|
|
* @param array $requestedClaims |
261
|
|
|
* |
262
|
|
|
* @return IdTokenBuilder |
263
|
|
|
*/ |
264
|
|
|
public function withRequestedClaims(array $requestedClaims): self |
265
|
|
|
{ |
266
|
|
|
$clone = clone $this; |
267
|
|
|
$clone->requestedClaims = $requestedClaims; |
268
|
|
|
|
269
|
|
|
return $clone; |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
/** |
273
|
|
|
* @param string $nonce |
274
|
|
|
* |
275
|
|
|
* @return IdTokenBuilder |
276
|
|
|
*/ |
277
|
|
|
public function withNonce(string $nonce): self |
278
|
|
|
{ |
279
|
|
|
$clone = clone $this; |
280
|
|
|
$clone->nonce = $nonce; |
281
|
|
|
|
282
|
|
|
return $clone; |
283
|
|
|
} |
284
|
|
|
|
285
|
|
|
/** |
286
|
|
|
* @param \DateTimeImmutable $expiresAt |
287
|
|
|
* |
288
|
|
|
* @return IdTokenBuilder |
289
|
|
|
*/ |
290
|
|
|
public function withExpirationAt(\DateTimeImmutable $expiresAt): self |
291
|
|
|
{ |
292
|
|
|
$clone = clone $this; |
293
|
|
|
$clone->expiresAt = $expiresAt; |
294
|
|
|
|
295
|
|
|
return $clone; |
296
|
|
|
} |
297
|
|
|
|
298
|
|
|
/** |
299
|
|
|
* @return IdTokenBuilder |
300
|
|
|
*/ |
301
|
|
|
public function withoutAuthenticationTime(): self |
302
|
|
|
{ |
303
|
|
|
$clone = clone $this; |
304
|
|
|
$clone->withAuthenticationTime = false; |
305
|
|
|
|
306
|
|
|
return $clone; |
307
|
|
|
} |
308
|
|
|
|
309
|
|
|
/** |
310
|
|
|
* @param JWSBuilder $jwsBuilder |
311
|
|
|
* @param JWKSet $signatureKeys |
312
|
|
|
* @param string $signatureAlgorithm |
313
|
|
|
* |
314
|
|
|
* @return IdTokenBuilder |
315
|
|
|
*/ |
316
|
|
|
public function withSignature(JWSBuilder $jwsBuilder, JWKSet $signatureKeys, string $signatureAlgorithm): self |
317
|
|
|
{ |
318
|
|
|
if (!in_array($signatureAlgorithm, $jwsBuilder->getSignatureAlgorithmManager()->list())) { |
319
|
|
|
throw new \InvalidArgumentException(sprintf('Unsupported signature algorithm "%s". Please use one of the following one: %s', $signatureAlgorithm, implode(', ', $jwsBuilder->getSignatureAlgorithmManager()->list()))); |
320
|
|
|
} |
321
|
|
|
if (0 === $signatureKeys->count()) { |
322
|
|
|
throw new \InvalidArgumentException('The signature key set must contain at least one key.'); |
323
|
|
|
} |
324
|
|
|
$clone = clone $this; |
325
|
|
|
$clone->jwsBuilder = $jwsBuilder; |
326
|
|
|
$clone->signatureKeys = $signatureKeys; |
327
|
|
|
$clone->signatureAlgorithm = $signatureAlgorithm; |
328
|
|
|
|
329
|
|
|
return $clone; |
330
|
|
|
} |
331
|
|
|
|
332
|
|
|
/** |
333
|
|
|
* @param JWEBuilder $jweBuilder |
334
|
|
|
* @param string $keyEncryptionAlgorithm |
335
|
|
|
* @param string $contentEncryptionAlgorithm |
336
|
|
|
* |
337
|
|
|
* @return IdTokenBuilder |
338
|
|
|
*/ |
339
|
|
|
public function withEncryption(JWEBuilder $jweBuilder, string $keyEncryptionAlgorithm, string $contentEncryptionAlgorithm): self |
340
|
|
|
{ |
341
|
|
|
if (!in_array($keyEncryptionAlgorithm, $jweBuilder->getKeyEncryptionAlgorithmManager()->list())) { |
342
|
|
|
throw new \InvalidArgumentException(sprintf('Unsupported key encryption algorithm "%s". Please use one of the following one: %s', $keyEncryptionAlgorithm, implode(', ', $jweBuilder->getKeyEncryptionAlgorithmManager()->list()))); |
343
|
|
|
} |
344
|
|
|
if (!in_array($contentEncryptionAlgorithm, $jweBuilder->getContentEncryptionAlgorithmManager()->list())) { |
345
|
|
|
throw new \InvalidArgumentException(sprintf('Unsupported content encryption algorithm "%s". Please use one of the following one: %s', $contentEncryptionAlgorithm, implode(', ', $jweBuilder->getContentEncryptionAlgorithmManager()->list()))); |
346
|
|
|
} |
347
|
|
|
$clone = clone $this; |
348
|
|
|
$clone->jweBuilder = $jweBuilder; |
349
|
|
|
$clone->keyEncryptionAlgorithm = $keyEncryptionAlgorithm; |
350
|
|
|
$clone->contentEncryptionAlgorithm = $contentEncryptionAlgorithm; |
351
|
|
|
|
352
|
|
|
return $clone; |
353
|
|
|
} |
354
|
|
|
|
355
|
|
|
/** |
356
|
|
|
* @return string |
357
|
|
|
*/ |
358
|
|
|
public function build(): string |
359
|
|
|
{ |
360
|
|
|
$data = $this->userinfo->getUserinfo($this->client, $this->userAccount, $this->redirectUri, $this->requestedClaims, $this->scopes, $this->claimsLocales); |
361
|
|
|
$data = $this->updateClaimsWithAmrAndAcrInfo($data, $this->userAccount); |
362
|
|
|
$data = $this->updateClaimsWithAuthenticationTime($data, $this->userAccount); |
363
|
|
|
$data = $this->updateClaimsWithNonce($data); |
364
|
|
|
if (null !== $this->signatureAlgorithm) { |
365
|
|
|
$data = $this->updateClaimsWithJwtClaims($data); |
366
|
|
|
$data = $this->updateClaimsWithTokenHash($data); |
367
|
|
|
$data = $this->updateClaimsAudience($data); |
368
|
|
|
$result = $this->computeIdToken($data); |
369
|
|
|
} else { |
370
|
|
|
$result = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); |
371
|
|
|
} |
372
|
|
|
|
373
|
|
|
if (null !== $this->keyEncryptionAlgorithm && null !== $this->contentEncryptionAlgorithm) { |
374
|
|
|
$result = $this->tryToEncrypt($this->client, $result); |
375
|
|
|
} |
376
|
|
|
|
377
|
|
|
return $result; |
378
|
|
|
} |
379
|
|
|
|
380
|
|
|
/** |
381
|
|
|
* @param array $claims |
382
|
|
|
* |
383
|
|
|
* @return array |
384
|
|
|
*/ |
385
|
|
|
private function updateClaimsWithJwtClaims(array $claims): array |
386
|
|
|
{ |
387
|
|
|
if (null === $this->expiresAt) { |
388
|
|
|
$this->expiresAt = (new \DateTimeImmutable())->setTimestamp(time() + $this->lifetime); |
|
|
|
|
389
|
|
|
} |
390
|
|
|
$claims += [ |
391
|
|
|
'iat' => time(), |
392
|
|
|
'nbf' => time(), |
393
|
|
|
'exp' => $this->expiresAt->getTimestamp(), |
394
|
|
|
'jti' => Base64Url::encode(random_bytes(25)), |
395
|
|
|
'iss' => $this->issuer, |
396
|
|
|
]; |
397
|
|
|
|
398
|
|
|
return $claims; |
399
|
|
|
} |
400
|
|
|
|
401
|
|
|
/** |
402
|
|
|
* @param array $claims |
403
|
|
|
* @param UserAccount $userAccount |
404
|
|
|
* |
405
|
|
|
* @return array |
406
|
|
|
*/ |
407
|
|
|
private function updateClaimsWithAuthenticationTime(array $claims, UserAccount $userAccount): array |
408
|
|
|
{ |
409
|
|
|
if (true === $this->withAuthenticationTime && null !== $userAccount->getLastLoginAt()) { //FIXME: check if the client has a require_auth_time parameter |
410
|
|
|
$claims['auth_time'] = $userAccount->getLastLoginAt()->getTimestamp(); |
411
|
|
|
} |
412
|
|
|
|
413
|
|
|
return $claims; |
414
|
|
|
} |
415
|
|
|
|
416
|
|
|
/** |
417
|
|
|
* @param array $claims |
418
|
|
|
* |
419
|
|
|
* @return array |
420
|
|
|
*/ |
421
|
|
|
private function updateClaimsWithNonce(array $claims): array |
422
|
|
|
{ |
423
|
|
|
if (null !== $this->nonce) { |
424
|
|
|
$claims['nonce'] = $this->nonce; |
425
|
|
|
} |
426
|
|
|
|
427
|
|
|
return $claims; |
428
|
|
|
} |
429
|
|
|
|
430
|
|
|
/** |
431
|
|
|
* @param array $claims |
432
|
|
|
* |
433
|
|
|
* @return array |
434
|
|
|
*/ |
435
|
|
|
private function updateClaimsAudience(array $claims): array |
436
|
|
|
{ |
437
|
|
|
$claims['aud'] = [ |
438
|
|
|
$this->client->getPublicId()->getValue(), |
439
|
|
|
$this->issuer, |
440
|
|
|
]; |
441
|
|
|
$claims['azp'] = $this->client->getPublicId()->getValue(); |
442
|
|
|
|
443
|
|
|
return $claims; |
444
|
|
|
} |
445
|
|
|
|
446
|
|
|
/** |
447
|
|
|
* @param array $claims |
448
|
|
|
* @param UserAccount $userAccount |
449
|
|
|
* |
450
|
|
|
* @return array |
451
|
|
|
*/ |
452
|
|
|
private function updateClaimsWithAmrAndAcrInfo(array $claims, UserAccount $userAccount): array |
453
|
|
|
{ |
454
|
|
|
foreach (['amr' => 'amr', 'acr' => 'acr'] as $claim => $key) { |
455
|
|
|
if ($userAccount->has($claim)) { |
456
|
|
|
$claims[$key] = $userAccount->get($claim); |
457
|
|
|
} |
458
|
|
|
} |
459
|
|
|
|
460
|
|
|
return $claims; |
461
|
|
|
} |
462
|
|
|
|
463
|
|
|
/** |
464
|
|
|
* @param array $claims |
465
|
|
|
* |
466
|
|
|
* @return string |
467
|
|
|
*/ |
468
|
|
|
private function computeIdToken(array $claims): string |
469
|
|
|
{ |
470
|
|
|
$signatureKey = $this->getSignatureKey($this->signatureAlgorithm); |
471
|
|
|
$header = $this->getHeaders($signatureKey, $this->signatureAlgorithm); |
472
|
|
|
$jsonConverter = new StandardConverter(); |
473
|
|
|
$claims = $jsonConverter->encode($claims); |
474
|
|
|
$jws = $this->jwsBuilder |
475
|
|
|
->create() |
476
|
|
|
->withPayload($claims) |
477
|
|
|
->addSignature($signatureKey, $header) |
478
|
|
|
->build(); |
479
|
|
|
$serializer = new JwsCompactSerializer($jsonConverter); |
480
|
|
|
|
481
|
|
|
return $serializer->serialize($jws, 0); |
482
|
|
|
} |
483
|
|
|
|
484
|
|
|
/** |
485
|
|
|
* @param Client $client |
486
|
|
|
* @param string $jwt |
487
|
|
|
* |
488
|
|
|
* @return string |
489
|
|
|
*/ |
490
|
|
|
private function tryToEncrypt(Client $client, string $jwt): string |
491
|
|
|
{ |
492
|
|
|
$clientKeySet = $client->getPublicKeySet(); |
493
|
|
|
$keyEncryptionAlgorithm = $this->jweBuilder->getKeyEncryptionAlgorithmManager()->get($this->keyEncryptionAlgorithm); |
494
|
|
|
$encryptionKey = $clientKeySet->selectKey('enc', $keyEncryptionAlgorithm); |
495
|
|
|
if (null === $encryptionKey) { |
496
|
|
|
throw new \InvalidArgumentException('No encryption key available for the client.'); |
497
|
|
|
} |
498
|
|
|
$header = [ |
499
|
|
|
'typ' => 'JWT', |
500
|
|
|
'jti' => Base64Url::encode(random_bytes(25)), |
501
|
|
|
'alg' => $this->keyEncryptionAlgorithm, |
502
|
|
|
'enc' => $this->contentEncryptionAlgorithm, |
503
|
|
|
]; |
504
|
|
|
$jwe = $this->jweBuilder |
505
|
|
|
->create() |
506
|
|
|
->withPayload($jwt) |
507
|
|
|
->withSharedProtectedHeader($header) |
508
|
|
|
->addRecipient($encryptionKey) |
509
|
|
|
->build(); |
510
|
|
|
$jsonConverter = new StandardConverter(); |
511
|
|
|
$serializer = new JweCompactSerializer($jsonConverter); |
512
|
|
|
|
513
|
|
|
return $serializer->serialize($jwe, 0); |
514
|
|
|
} |
515
|
|
|
|
516
|
|
|
/** |
517
|
|
|
* @param string $signatureAlgorithm |
518
|
|
|
* |
519
|
|
|
* @return JWK |
520
|
|
|
*/ |
521
|
|
|
private function getSignatureKey(string $signatureAlgorithm): JWK |
522
|
|
|
{ |
523
|
|
|
$signatureAlgorithm = $this->jwsBuilder->getSignatureAlgorithmManager()->get($signatureAlgorithm); |
524
|
|
|
if ('none' === $signatureAlgorithm->name()) { |
525
|
|
|
return JWK::create(['kty' => 'none', 'alg' => 'none', 'use' => 'sig']); |
526
|
|
|
} |
527
|
|
|
$signatureKey = $this->signatureKeys->selectKey('sig', $signatureAlgorithm); |
528
|
|
|
if (null === $signatureKey) { |
529
|
|
|
throw new \InvalidArgumentException('Unable to find a key to sign the ID Token. Please verify the selected key set contains suitable keys.'); |
530
|
|
|
} |
531
|
|
|
|
532
|
|
|
return $signatureKey; |
533
|
|
|
} |
534
|
|
|
|
535
|
|
|
/** |
536
|
|
|
* @param JWK $signatureKey |
537
|
|
|
* @param string $signatureAlgorithm |
538
|
|
|
* |
539
|
|
|
* @return array |
540
|
|
|
*/ |
541
|
|
|
private function getHeaders(JWK $signatureKey, string $signatureAlgorithm): array |
542
|
|
|
{ |
543
|
|
|
$header = [ |
544
|
|
|
'typ' => 'JWT', |
545
|
|
|
'alg' => $signatureAlgorithm, |
546
|
|
|
]; |
547
|
|
|
if ($signatureKey->has('kid')) { |
548
|
|
|
$header['kid'] = $signatureKey->get('kid'); |
549
|
|
|
} |
550
|
|
|
|
551
|
|
|
return $header; |
552
|
|
|
} |
553
|
|
|
|
554
|
|
|
/** |
555
|
|
|
* @param array $claims |
556
|
|
|
* |
557
|
|
|
* @return array |
558
|
|
|
*/ |
559
|
|
|
private function updateClaimsWithTokenHash(array $claims): array |
560
|
|
|
{ |
561
|
|
|
if ('none' === $this->signatureAlgorithm) { |
562
|
|
|
return $claims; |
563
|
|
|
} |
564
|
|
|
if (null !== $this->accessTokenId) { |
565
|
|
|
$claims['at_hash'] = $this->getHash($this->accessTokenId); |
566
|
|
|
} |
567
|
|
|
if (null !== $this->authorizationCodeId) { |
568
|
|
|
$claims['c_hash'] = $this->getHash($this->authorizationCodeId); |
569
|
|
|
} |
570
|
|
|
|
571
|
|
|
return $claims; |
572
|
|
|
} |
573
|
|
|
|
574
|
|
|
/** |
575
|
|
|
* @param TokenId $tokenId |
576
|
|
|
* |
577
|
|
|
* @return string |
578
|
|
|
*/ |
579
|
|
|
private function getHash(TokenId $tokenId): string |
580
|
|
|
{ |
581
|
|
|
return Base64Url::encode(mb_substr(hash($this->getHashMethod(), $tokenId->getValue(), true), 0, $this->getHashSize(), '8bit')); |
582
|
|
|
} |
583
|
|
|
|
584
|
|
|
/** |
585
|
|
|
* @throws \InvalidArgumentException |
586
|
|
|
* |
587
|
|
|
* @return string |
588
|
|
|
*/ |
589
|
|
|
private function getHashMethod(): string |
590
|
|
|
{ |
591
|
|
|
$map = [ |
592
|
|
|
'HS256' => 'sha256', |
593
|
|
|
'ES256' => 'sha256', |
594
|
|
|
'RS256' => 'sha256', |
595
|
|
|
'PS256' => 'sha256', |
596
|
|
|
'HS384' => 'sha384', |
597
|
|
|
'ES384' => 'sha384', |
598
|
|
|
'RS384' => 'sha384', |
599
|
|
|
'PS384' => 'sha384', |
600
|
|
|
'HS512' => 'sha512', |
601
|
|
|
'ES512' => 'sha512', |
602
|
|
|
'RS512' => 'sha512', |
603
|
|
|
'PS512' => 'sha512', |
604
|
|
|
]; |
605
|
|
|
|
606
|
|
|
if (!array_key_exists($this->signatureAlgorithm, $map)) { |
607
|
|
|
throw new \InvalidArgumentException(sprintf('Algorithm "%s" is not supported', $this->signatureAlgorithm)); |
608
|
|
|
} |
609
|
|
|
|
610
|
|
|
return $map[$this->signatureAlgorithm]; |
611
|
|
|
} |
612
|
|
|
|
613
|
|
|
/** |
614
|
|
|
* @throws \InvalidArgumentException |
615
|
|
|
* |
616
|
|
|
* @return int |
617
|
|
|
*/ |
618
|
|
|
private function getHashSize(): int |
619
|
|
|
{ |
620
|
|
|
$map = [ |
621
|
|
|
'HS256' => 16, |
622
|
|
|
'ES256' => 16, |
623
|
|
|
'RS256' => 16, |
624
|
|
|
'PS256' => 16, |
625
|
|
|
'HS384' => 24, |
626
|
|
|
'ES384' => 24, |
627
|
|
|
'RS384' => 24, |
628
|
|
|
'PS384' => 24, |
629
|
|
|
'HS512' => 32, |
630
|
|
|
'ES512' => 32, |
631
|
|
|
'RS512' => 32, |
632
|
|
|
'PS512' => 32, |
633
|
|
|
]; |
634
|
|
|
|
635
|
|
|
if (!array_key_exists($this->signatureAlgorithm, $map)) { |
636
|
|
|
throw new \InvalidArgumentException(sprintf('Algorithm "%s" is not supported', $this->signatureAlgorithm)); |
637
|
|
|
} |
638
|
|
|
|
639
|
|
|
return $map[$this->signatureAlgorithm]; |
640
|
|
|
} |
641
|
|
|
} |
642
|
|
|
|
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.