Failed Conditions
Push — ng ( a36945...8b0de7 )
by Florent
15:25
created

IdTokenBuilder::updateClaimsWithJwtClaims()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 15
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 10
nc 2
nop 1
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
class IdTokenBuilder
0 ignored issues
show
Coding Style introduced by
Since you have declared the constructor as private, maybe you should also declare the class as final.
Loading history...
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);
0 ignored issues
show
Documentation Bug introduced by
It seems like (new \DateTimeImmutable(...me() + $this->lifetime) can also be of type false. However, the property $expiresAt is declared as type object<DateTimeImmutable>|null. Maybe add an additional type check?

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 the id property of an instance of the Account 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.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
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