Failed Conditions
Pull Request — master (#31)
by Florent
07:03 queued 03:28
created

IdTokenBuilder::withSignatureAlgorithm()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 5
nc 1
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * The MIT License (MIT)
7
 *
8
 * Copyright (c) 2014-2017 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\Server\Model\IdToken;
15
16
use Assert\Assertion;
17
use Base64Url\Base64Url;
18
use Jose\EncrypterInterface;
19
use Jose\Factory\JWEFactory;
20
use Jose\Factory\JWSFactory;
21
use Jose\SignerInterface;
22
use Jose\Object\JWKInterface;
23
use Jose\Object\JWKSetInterface;
24
use OAuth2Framework\Component\Server\Endpoint\UserInfo\UserInfo;
25
use OAuth2Framework\Component\Server\Model\AccessToken\AccessToken;
26
use OAuth2Framework\Component\Server\Model\AccessToken\AccessTokenId;
27
use OAuth2Framework\Component\Server\Model\AuthCode\AuthCodeId;
28
use OAuth2Framework\Component\Server\Model\Client\Client;
29
use OAuth2Framework\Component\Server\Model\Token\TokenId;
30
use OAuth2Framework\Component\Server\Model\UserAccount\UserAccountInterface;
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 UserAccountInterface
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 JWKSetInterface
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 AuthCodeId|null
91
     */
92
    private $authCodeId = 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 SignerInterface|null
106
     */
107
    private $signer = null;
108
109
    /**
110
     * @var string|null
111
     */
112
    private $signatureAlgorithm = null;
113
114
    /**
115
     * @var EncrypterInterface|null
116
     */
117
    private $encrypter;
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 UserAccountInterface $userAccount
142
     * @param string               $redirectUri
143
     */
144
    private function __construct(string $issuer, UserInfo $userinfo, int $lifetime, Client $client, UserAccountInterface $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 UserAccountInterface $userAccount
160
     * @param string               $redirectUri
161
     *
162
     * @return IdTokenBuilder
163
     */
164
    public static function create(string $issuer, UserInfo $userinfo, int $lifetime, Client $client, UserAccountInterface $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): IdTokenBuilder
175
    {
176
        $clone = clone $this;
177
        $clone->accessTokenId = $accessToken->getTokenId();
0 ignored issues
show
Documentation Bug introduced by
It seems like $accessToken->getTokenId() of type object<OAuth2Framework\C...er\Model\Token\TokenId> is incompatible with the declared type object<OAuth2Framework\C...ken\AccessTokenId>|null of property $accessTokenId.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
178
        $clone->expiresAt = $accessToken->getExpiresAt();
179
        $clone->scopes = $accessToken->getScopes();
180
181
        if ($accessToken->hasMetadata('code')) {
182
            $authCode = $accessToken->getMetadata('code');
183
            $clone->authCodeId = $authCode->getTokenId();
184
            $queryParams = $authCode->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', $authCode->getQueryParams());
191
        }
192
193
        return $clone;
194
    }
195
196
    /**
197
     * @param AccessTokenId $accessTokenId
198
     *
199
     * @return IdTokenBuilder
200
     */
201
    public function withAccessTokenId(AccessTokenId $accessTokenId): IdTokenBuilder
202
    {
203
        $clone = clone $this;
204
        $clone->accessTokenId = $accessTokenId;
205
206
        return $clone;
207
    }
208
209
    /**
210
     * @param AuthCodeId $authCodeId
211
     *
212
     * @return IdTokenBuilder
213
     */
214
    public function withAuthCodeId(AuthCodeId $authCodeId): IdTokenBuilder
215
    {
216
        $clone = clone $this;
217
        $clone->authCodeId = $authCodeId;
218
219
        return $clone;
220
    }
221
222
    /**
223
     * @param string $claimsLocales
224
     *
225
     * @return IdTokenBuilder
226
     */
227
    public function withClaimsLocales(string $claimsLocales): IdTokenBuilder
228
    {
229
        $clone = clone $this;
230
        $clone->claimsLocales = $claimsLocales;
231
232
        return $clone;
233
    }
234
235
    /**
236
     * @return IdTokenBuilder
237
     */
238
    public function withAuthenticationTime(): IdTokenBuilder
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): IdTokenBuilder
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): IdTokenBuilder
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): IdTokenBuilder
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): IdTokenBuilder
291
    {
292
        $clone = clone $this;
293
        $clone->expiresAt = $expiresAt;
294
295
        return $clone;
296
    }
297
298
    /**
299
     * @return IdTokenBuilder
300
     */
301
    public function withoutAuthenticationTime(): IdTokenBuilder
302
    {
303
        $clone = clone $this;
304
        $clone->withAuthenticationTime = false;
305
306
        return $clone;
307
    }
308
309
    /**
310
     * @param SignerInterface $signer
311
     * @param JWKSetInterface $signatureKeys
312
     * @param string          $signatureAlgorithm
313
     *
314
     * @return IdTokenBuilder
315
     */
316
    public function withSignature(SignerInterface $signer, JWKSetInterface $signatureKeys, string $signatureAlgorithm): IdTokenBuilder
317
    {
318
        Assertion::inArray($signatureAlgorithm, $signer->getSupportedSignatureAlgorithms(), sprintf('Unsupported signature algorithm \'%s\'. Please use one of the following one: %s', $signatureAlgorithm, implode(', ', $signer->getSupportedSignatureAlgorithms())));
319
        Assertion::true(0 !== $signatureKeys->countKeys(), 'The signature key set must contain at least one key.');
320
        $clone = clone $this;
321
        $clone->signer = $signer;
322
        $clone->signatureKeys = $signatureKeys;
323
        $clone->signatureAlgorithm = $signatureAlgorithm;
324
325
        return $clone;
326
    }
327
328
    /**
329
     * @param EncrypterInterface $encrypter
330
     * @param string             $keyEncryptionAlgorithm
331
     * @param string             $contentEncryptionAlgorithm
332
     *
333
     * @return IdTokenBuilder
334
     */
335
    public function withEncryption(EncrypterInterface $encrypter, string $keyEncryptionAlgorithm, string $contentEncryptionAlgorithm): IdTokenBuilder
336
    {
337
        Assertion::inArray($keyEncryptionAlgorithm, $encrypter->getSupportedKeyEncryptionAlgorithms(), sprintf('Unsupported key encryption algorithm \'%s\'. Please use one of the following one: %s', $keyEncryptionAlgorithm, implode(', ', $encrypter->getSupportedKeyEncryptionAlgorithms())));
338
        Assertion::inArray($contentEncryptionAlgorithm, $encrypter->getSupportedContentEncryptionAlgorithms(), sprintf('Unsupported key encryption algorithm \'%s\'. Please use one of the following one: %s', $contentEncryptionAlgorithm, implode(', ', $encrypter->getSupportedContentEncryptionAlgorithms())));
339
        $clone = clone $this;
340
        $clone->encrypter = $encrypter;
341
        $clone->keyEncryptionAlgorithm = $keyEncryptionAlgorithm;
342
        $clone->contentEncryptionAlgorithm = $contentEncryptionAlgorithm;
343
344
        return $clone;
345
    }
346
347
    /**
348
     * @return string
349
     */
350
    public function build(): string
351
    {
352
        $data = $this->userinfo->getUserinfo($this->client, $this->userAccount, $this->redirectUri, $this->requestedClaims, $this->scopes, $this->claimsLocales);
353
        $data = $this->updateClaimsWithAmrAndAcrInfo($data, $this->userAccount);
354
        $data = $this->updateClaimsWithAuthenticationTime($data, $this->userAccount);
355
        $data = $this->updateClaimsWithNonce($data);
356
        if (null !== $this->signatureAlgorithm) {
357
            $data = $this->updateClaimsWithJwtClaims($data);
358
            $data = $this->updateClaimsWithTokenHash($data);
359
            $result = $this->computeIdToken($data);
360
        } else {
361
            $result = json_encode($data);
362
        }
363
364
        if (null !== $this->keyEncryptionAlgorithm && null !== $this->contentEncryptionAlgorithm) {
365
            $result = $this->tryToEncrypt($this->client, $result);
366
        }
367
368
        return $result;
369
    }
370
371
    /**
372
     * @param array $claims
373
     *
374
     * @return array
375
     */
376
    private function updateClaimsWithJwtClaims(array $claims): array
377
    {
378
        if (null === $this->expiresAt) {
379
            $time = sprintf('now +%s sec', $this->lifetime);
380
            $this->expiresAt = new \DateTimeImmutable($time);
381
        }
382
        $claims += [
383
            'iat' => time(),
384
            'nbf' => time(),
385
            'exp' => $this->expiresAt->getTimestamp(),
386
            'jti' => Base64Url::encode(random_bytes(25)),
387
            'iss' => $this->issuer,
388
        ];
389
390
        return $claims;
391
    }
392
393
    /**
394
     * @param array                $claims
395
     * @param UserAccountInterface $userAccount
396
     *
397
     * @return array
398
     */
399
    private function updateClaimsWithAuthenticationTime(array $claims, UserAccountInterface $userAccount): array
400
    {
401
        if (true === $this->withAuthenticationTime && null !== $userAccount->getLastLoginAt()) {
402
            $claims['auth_time'] = $userAccount->getLastLoginAt()->getTimestamp();
403
        }
404
405
        return $claims;
406
    }
407
408
    /**
409
     * @param array $claims
410
     *
411
     * @return array
412
     */
413
    private function updateClaimsWithNonce(array $claims): array
414
    {
415
        if (null !== $this->nonce) {
416
            $claims['nonce'] = $this->nonce;
417
        }
418
419
        return $claims;
420
    }
421
422
    /**
423
     * @param array                $claims
424
     * @param UserAccountInterface $userAccount
425
     *
426
     * @return array
427
     */
428
    private function updateClaimsWithAmrAndAcrInfo(array $claims, UserAccountInterface $userAccount): array
429
    {
430
        foreach (['amr' => 'amr', 'acr' => 'acr'] as $claim => $key) {
431
            if ($userAccount->has($claim)) {
432
                $claims[$key] = $userAccount->get($claim);
433
            }
434
        }
435
436
        return $claims;
437
    }
438
439
    /**
440
     * @param array $claims
441
     *
442
     * @return string
443
     */
444
    private function computeIdToken(array $claims): string
445
    {
446
        $signatureKey = $this->getSignatureKey($this->signatureAlgorithm);
447
        $headers = $this->getHeaders($signatureKey, $this->signatureAlgorithm);
448
        $jws = JWSFactory::createJWS($claims);
449
        $jws = $jws->addSignatureInformation($signatureKey, $headers);
450
        $this->signer->sign($jws);
451
452
        return $jws->toJson(0);
453
    }
454
455
    /**
456
     * @param Client $client
457
     * @param string $jwt
458
     *
459
     * @return string
460
     */
461
    private function tryToEncrypt(Client $client, string $jwt): string
462
    {
463
        $clientKeySet = $client->getPublicKeySet();
464
        $encryptionKey = $clientKeySet->selectKey('enc', $this->keyEncryptionAlgorithm);
465
        Assertion::notNull($encryptionKey, 'No encryption key available for the client.');
466
        $headers = [
467
            'typ' => 'JWT',
468
            'jti' => Base64Url::encode(random_bytes(25)),
469
            'alg' => $this->keyEncryptionAlgorithm,
470
            'enc' => $this->contentEncryptionAlgorithm,
471
        ];
472
        $jwe = JWEFactory::createJWE($jwt, $headers);
473
        $jwe = $jwe->addRecipientInformation($encryptionKey);
0 ignored issues
show
Bug introduced by
It seems like $encryptionKey defined by $clientKeySet->selectKey...keyEncryptionAlgorithm) on line 464 can be null; however, Jose\Object\JWEInterface...dRecipientInformation() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
474
        $this->encrypter->encrypt($jwe);
475
476
        return $jwe->toJson(0);
477
    }
478
479
    /**
480
     * @param string $signatureAlgorithm
481
     *
482
     * @return JWKInterface
483
     */
484
    private function getSignatureKey(string $signatureAlgorithm): JWKInterface
485
    {
486
        $signatureKey = $this->signatureKeys->selectKey('sig', $signatureAlgorithm);
487
        Assertion::notNull($signatureKey, 'Unable to find a key to sign the ID Token. Please verify the selected key set contains suitable keys.');
488
489
        return $signatureKey;
490
    }
491
492
    /**
493
     * @param JWKInterface $signatureKey
494
     * @param string       $signatureAlgorithm
495
     *
496
     * @return array
497
     */
498
    private function getHeaders(JWKInterface $signatureKey, string $signatureAlgorithm): array
499
    {
500
        $headers = [
501
            'typ' => 'JWT',
502
            'alg' => $signatureAlgorithm,
503
        ];
504
        if ($signatureKey->has('kid')) {
505
            $headers['kid'] = $signatureKey->get('kid');
506
        }
507
508
        return $headers;
509
    }
510
511
    /**
512
     * @param array $claims
513
     *
514
     * @return array
515
     */
516
    private function updateClaimsWithTokenHash(array $claims): array
517
    {
518
        if ('none' === $this->signatureAlgorithm) {
519
            return $claims;
520
        }
521
        if (null !== $this->accessTokenId) {
522
            $claims['at_hash'] = $this->getHash($this->accessTokenId);
523
        }
524
        if (null !== $this->authCodeId) {
525
            $claims['c_hash'] = $this->getHash($this->authCodeId);
526
        }
527
528
        return $claims;
529
    }
530
531
    /**
532
     * @param TokenId $tokenId
533
     *
534
     * @return string
535
     */
536
    private function getHash(TokenId $tokenId): string
537
    {
538
        return Base64Url::encode(mb_substr(hash($this->getHashMethod(), $tokenId->getValue(), true), 0, $this->getHashSize(), '8bit'));
539
    }
540
541
    /**
542
     * @throws \InvalidArgumentException
543
     *
544
     * @return string
545
     */
546
    private function getHashMethod(): string
547
    {
548
        $map = [
549
            'HS256' => 'sha256',
550
            'ES256' => 'sha256',
551
            'RS256' => 'sha256',
552
            'PS256' => 'sha256',
553
            'HS384' => 'sha384',
554
            'ES384' => 'sha384',
555
            'RS384' => 'sha384',
556
            'PS384' => 'sha384',
557
            'HS512' => 'sha512',
558
            'ES512' => 'sha512',
559
            'RS512' => 'sha512',
560
            'PS512' => 'sha512',
561
        ];
562
563
        Assertion::keyExists($map, $this->signatureAlgorithm, sprintf('Algorithm \'%s\' is not supported', $this->signatureAlgorithm));
564
565
        return $map[$this->signatureAlgorithm];
566
    }
567
568
    /**
569
     * @throws \InvalidArgumentException
570
     *
571
     * @return int
572
     */
573
    private function getHashSize(): int
574
    {
575
        $map = [
576
            'HS256' => 16,
577
            'ES256' => 16,
578
            'RS256' => 16,
579
            'PS256' => 16,
580
            'HS384' => 24,
581
            'ES384' => 24,
582
            'RS384' => 24,
583
            'PS384' => 24,
584
            'HS512' => 32,
585
            'ES512' => 32,
586
            'RS512' => 32,
587
            'PS512' => 32,
588
        ];
589
590
        Assertion::keyExists($map, $this->signatureAlgorithm, sprintf('Algorithm \'%s\' is not supported', $this->signatureAlgorithm));
591
592
        return $map[$this->signatureAlgorithm];
593
    }
594
}
595