Failed Conditions
Push — master ( 22ddb5...a86abe )
by Florent
03:15
created

IdTokenBuilder::updateClaimsAudience()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 9
rs 9.6666
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\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\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 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 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 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 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();
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 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): IdTokenBuilder
317
    {
318
        Assertion::inArray($signatureAlgorithm, $jwsBuilder->getSignatureAlgorithmManager()->list(), sprintf('Unsupported signature algorithm \'%s\'. Please use one of the following one: %s', $signatureAlgorithm, implode(', ', $jwsBuilder->getSignatureAlgorithmManager()->list())));
319
        Assertion::true(0 !== $signatureKeys->count(), 'The signature key set must contain at least one key.');
320
        $clone = clone $this;
321
        $clone->jwsBuilder = $jwsBuilder;
322
        $clone->signatureKeys = $signatureKeys;
323
        $clone->signatureAlgorithm = $signatureAlgorithm;
324
325
        return $clone;
326
    }
327
328
    /**
329
     * @param JWEBuilder $jweBuilder
330
     * @param string     $keyEncryptionAlgorithm
331
     * @param string     $contentEncryptionAlgorithm
332
     *
333
     * @return IdTokenBuilder
334
     */
335
    public function withEncryption(JWEBuilder $jweBuilder, string $keyEncryptionAlgorithm, string $contentEncryptionAlgorithm): IdTokenBuilder
336
    {
337
        Assertion::inArray($keyEncryptionAlgorithm, $jweBuilder->getKeyEncryptionAlgorithmManager()->list(), sprintf('Unsupported key encryption algorithm \'%s\'. Please use one of the following one: %s', $keyEncryptionAlgorithm, implode(', ', $jweBuilder->getKeyEncryptionAlgorithmManager()->list())));
338
        Assertion::inArray($contentEncryptionAlgorithm, $jweBuilder->getContentEncryptionAlgorithmManager()->list(), sprintf('Unsupported content encryption algorithm \'%s\'. Please use one of the following one: %s', $contentEncryptionAlgorithm, implode(', ', $jweBuilder->getContentEncryptionAlgorithmManager()->list())));
339
        $clone = clone $this;
340
        $clone->jweBuilder = $jweBuilder;
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
        $data = $this->updateClaimsAudience($data);
357
        if (null !== $this->signatureAlgorithm) {
358
            $data = $this->updateClaimsWithJwtClaims($data);
359
            $data = $this->updateClaimsWithTokenHash($data);
360
            $result = $this->computeIdToken($data);
361
        } else {
362
            $result = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
363
        }
364
365
        if (null !== $this->keyEncryptionAlgorithm && null !== $this->contentEncryptionAlgorithm) {
366
            $result = $this->tryToEncrypt($this->client, $result);
367
        }
368
369
        return $result;
370
    }
371
372
    /**
373
     * @param array $claims
374
     *
375
     * @return array
376
     */
377
    private function updateClaimsWithJwtClaims(array $claims): array
378
    {
379
        if (null === $this->expiresAt) {
380
            $time = sprintf('now +%s sec', $this->lifetime);
381
            $this->expiresAt = new \DateTimeImmutable($time);
382
        }
383
        $claims += [
384
            'iat' => time(),
385
            'nbf' => time(),
386
            'exp' => $this->expiresAt->getTimestamp(),
387
            'jti' => Base64Url::encode(random_bytes(25)),
388
            'iss' => $this->issuer,
389
        ];
390
391
        return $claims;
392
    }
393
394
    /**
395
     * @param array                $claims
396
     * @param UserAccountInterface $userAccount
397
     *
398
     * @return array
399
     */
400
    private function updateClaimsWithAuthenticationTime(array $claims, UserAccountInterface $userAccount): array
401
    {
402
        if (true === $this->withAuthenticationTime && null !== $userAccount->getLastLoginAt()) { //FIXME: check if the client has a require_auth_time parameter
403
            $claims['auth_time'] = $userAccount->getLastLoginAt()->getTimestamp();
404
        }
405
406
        return $claims;
407
    }
408
409
    /**
410
     * @param array $claims
411
     *
412
     * @return array
413
     */
414
    private function updateClaimsWithNonce(array $claims): array
415
    {
416
        if (null !== $this->nonce) {
417
            $claims['nonce'] = $this->nonce;
418
        }
419
420
        return $claims;
421
    }
422
423
    /**
424
     * @param array $claims
425
     *
426
     * @return array
427
     */
428
    private function updateClaimsAudience(array $claims): array
429
    {
430
        $claims['aud'] = [
431
            $this->client->getPublicId()->getValue(),
432
            $this->issuer,
433
        ];
434
435
        return $claims;
436
    }
437
438
    /**
439
     * @param array                $claims
440
     * @param UserAccountInterface $userAccount
441
     *
442
     * @return array
443
     */
444
    private function updateClaimsWithAmrAndAcrInfo(array $claims, UserAccountInterface $userAccount): array
445
    {
446
        foreach (['amr' => 'amr', 'acr' => 'acr'] as $claim => $key) {
447
            if ($userAccount->has($claim)) {
448
                $claims[$key] = $userAccount->get($claim);
449
            }
450
        }
451
452
        return $claims;
453
    }
454
455
    /**
456
     * @param array $claims
457
     *
458
     * @return string
459
     */
460
    private function computeIdToken(array $claims): string
461
    {
462
        $signatureKey = $this->getSignatureKey($this->signatureAlgorithm);
463
        $headers = $this->getHeaders($signatureKey, $this->signatureAlgorithm);
464
        $jws = $this->jwsBuilder
465
            ->create()
466
            ->withPayload($claims)
467
            ->addSignature($signatureKey, $headers)
468
            ->build();
469
        $serializer = new JwsCompactSerializer();
470
471
        return $serializer->serialize($jws, 0);
472
    }
473
474
    /**
475
     * @param Client $client
476
     * @param string $jwt
477
     *
478
     * @return string
479
     */
480
    private function tryToEncrypt(Client $client, string $jwt): string
481
    {
482
        $clientKeySet = $client->getPublicKeySet();
483
        $keyEncryptionAlgorithm = $this->jweBuilder->getKeyEncryptionAlgorithmManager()->get($this->keyEncryptionAlgorithm);
484
        $encryptionKey = $clientKeySet->selectKey('enc', $keyEncryptionAlgorithm);
485
        Assertion::notNull($encryptionKey, 'No encryption key available for the client.');
486
        $headers = [
487
            'typ' => 'JWT',
488
            'jti' => Base64Url::encode(random_bytes(25)),
489
            'alg' => $this->keyEncryptionAlgorithm,
490
            'enc' => $this->contentEncryptionAlgorithm,
491
        ];
492
        $jwe = $this->jweBuilder
493
            ->create()
494
            ->withPayload($jwt)
495
            ->withSharedProtectedHeaders($headers)
496
            ->addRecipient($encryptionKey)
497
            ->build();
498
        $serializer = new JweCompactSerializer();
499
500
        return $serializer->serialize($jwe, 0);
501
    }
502
503
    /**
504
     * @param string $signatureAlgorithm
505
     *
506
     * @return JWK
507
     */
508
    private function getSignatureKey(string $signatureAlgorithm): JWK
509
    {
510
        $signatureAlgorithm = $this->jwsBuilder->getSignatureAlgorithmManager()->get($signatureAlgorithm);
511
        if ('none' === $signatureAlgorithm->name()) {
512
            return JWK::create(['kty' => 'none', 'alg' => 'none', 'use' => 'sig']);
513
        }
514
        $signatureKey = $this->signatureKeys->selectKey('sig', $signatureAlgorithm);
515
        Assertion::notNull($signatureKey, 'Unable to find a key to sign the ID Token. Please verify the selected key set contains suitable keys.');
516
517
        return $signatureKey;
518
    }
519
520
    /**
521
     * @param JWK    $signatureKey
522
     * @param string $signatureAlgorithm
523
     *
524
     * @return array
525
     */
526
    private function getHeaders(JWK $signatureKey, string $signatureAlgorithm): array
527
    {
528
        $headers = [
529
            'typ' => 'JWT',
530
            'alg' => $signatureAlgorithm,
531
        ];
532
        if ($signatureKey->has('kid')) {
533
            $headers['kid'] = $signatureKey->get('kid');
534
        }
535
536
        return $headers;
537
    }
538
539
    /**
540
     * @param array $claims
541
     *
542
     * @return array
543
     */
544
    private function updateClaimsWithTokenHash(array $claims): array
545
    {
546
        if ('none' === $this->signatureAlgorithm) {
547
            return $claims;
548
        }
549
        if (null !== $this->accessTokenId) {
550
            $claims['at_hash'] = $this->getHash($this->accessTokenId);
551
        }
552
        if (null !== $this->authCodeId) {
553
            $claims['c_hash'] = $this->getHash($this->authCodeId);
554
        }
555
556
        return $claims;
557
    }
558
559
    /**
560
     * @param TokenId $tokenId
561
     *
562
     * @return string
563
     */
564
    private function getHash(TokenId $tokenId): string
565
    {
566
        return Base64Url::encode(mb_substr(hash($this->getHashMethod(), $tokenId->getValue(), true), 0, $this->getHashSize(), '8bit'));
567
    }
568
569
    /**
570
     * @throws \InvalidArgumentException
571
     *
572
     * @return string
573
     */
574
    private function getHashMethod(): string
575
    {
576
        $map = [
577
            'HS256' => 'sha256',
578
            'ES256' => 'sha256',
579
            'RS256' => 'sha256',
580
            'PS256' => 'sha256',
581
            'HS384' => 'sha384',
582
            'ES384' => 'sha384',
583
            'RS384' => 'sha384',
584
            'PS384' => 'sha384',
585
            'HS512' => 'sha512',
586
            'ES512' => 'sha512',
587
            'RS512' => 'sha512',
588
            'PS512' => 'sha512',
589
        ];
590
591
        Assertion::keyExists($map, $this->signatureAlgorithm, sprintf('Algorithm \'%s\' is not supported', $this->signatureAlgorithm));
592
593
        return $map[$this->signatureAlgorithm];
594
    }
595
596
    /**
597
     * @throws \InvalidArgumentException
598
     *
599
     * @return int
600
     */
601
    private function getHashSize(): int
602
    {
603
        $map = [
604
            'HS256' => 16,
605
            'ES256' => 16,
606
            'RS256' => 16,
607
            'PS256' => 16,
608
            'HS384' => 24,
609
            'ES384' => 24,
610
            'RS384' => 24,
611
            'PS384' => 24,
612
            'HS512' => 32,
613
            'ES512' => 32,
614
            'RS512' => 32,
615
            'PS512' => 32,
616
        ];
617
618
        Assertion::keyExists($map, $this->signatureAlgorithm, sprintf('Algorithm \'%s\' is not supported', $this->signatureAlgorithm));
619
620
        return $map[$this->signatureAlgorithm];
621
    }
622
}
623