Failed Conditions
Push — master ( 7c3864...930f9b )
by Florent
14:15
created

src/Component/OpenIdConnect/IdTokenBuilder.php (2 issues)

Upgrade to new PHP Analysis Engine

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
Unused Code Comprehensibility introduced by
62% of this comment could be valid code. Did you maybe forget this after debugging?

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.

Loading history...
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
This method is not used, and could be removed.
Loading history...
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