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

ClientAssertionJwt::enableJkuSupport()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
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\ClientAuthentication;
15
16
use Base64Url\Base64Url;
17
use Jose\Component\Checker\ClaimCheckerManager;
18
use Jose\Component\Checker\HeaderCheckerManager;
19
use Jose\Component\Core\Converter\JsonConverter;
20
use Jose\Component\Core\JWK;
21
use Jose\Component\Core\JWKSet;
22
use Jose\Component\Encryption\JWELoader;
23
use Jose\Component\KeyManagement\JKUFactory;
24
use Jose\Component\Signature\JWS;
25
use Jose\Component\Signature\JWSVerifier;
26
use Jose\Component\Signature\Serializer\CompactSerializer;
27
use OAuth2Framework\Component\Core\Client\Client;
28
use OAuth2Framework\Component\Core\Client\ClientId;
29
use OAuth2Framework\Component\Core\DataBag\DataBag;
30
use OAuth2Framework\Component\Core\Message\OAuth2Error;
31
use OAuth2Framework\Component\Core\TrustedIssuer\TrustedIssuerRepository;
32
use OAuth2Framework\Component\Core\Util\RequestBodyParser;
33
use Psr\Http\Message\ServerRequestInterface;
34
35
class ClientAssertionJwt implements AuthenticationMethod
36
{
37
    private const CLIENT_ASSERTION_TYPE = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer';
38
39
    private $jwsVerifier;
40
41
    /**
42
     * @var null|TrustedIssuerRepository
43
     */
44
    private $trustedIssuerRepository = null;
45
46
    /**
47
     * @var null|JKUFactory
48
     */
49
    private $jkuFactory = null;
50
51
    /**
52
     * @var null|JWELoader
53
     */
54
    private $jweLoader = null;
55
56
    /**
57
     * @var null|JWKSet
58
     */
59
    private $keyEncryptionKeySet = null;
60
61
    private $encryptionRequired = false;
62
63
    private $secretLifetime;
64
65
    private $headerCheckerManager;
66
67
    private $claimCheckerManager;
68
69
    private $jsonConverter;
70
71
    public function __construct(JsonConverter $jsonConverter, JWSVerifier $jwsVerifier, HeaderCheckerManager $headerCheckerManager, ClaimCheckerManager $claimCheckerManager, int $secretLifetime = 0)
72
    {
73
        if ($secretLifetime < 0) {
74
            throw new \InvalidArgumentException('The secret lifetime must be at least 0 (= unlimited).');
75
        }
76
        $this->jsonConverter = $jsonConverter;
77
        $this->jwsVerifier = $jwsVerifier;
78
        $this->headerCheckerManager = $headerCheckerManager;
79
        $this->claimCheckerManager = $claimCheckerManager;
80
        $this->secretLifetime = $secretLifetime;
81
    }
82
83
    public function enableTrustedIssuerSupport(TrustedIssuerRepository $trustedIssuerRepository): void
84
    {
85
        $this->trustedIssuerRepository = $trustedIssuerRepository;
86
    }
87
88
    public function enableJkuSupport(JKUFactory $jkuFactory): void
89
    {
90
        $this->jkuFactory = $jkuFactory;
91
    }
92
93
    public function enableEncryptedAssertions(JWELoader $jweLoader, JWKSet $keyEncryptionKeySet, bool $encryptionRequired): void
94
    {
95
        $this->jweLoader = $jweLoader;
96
        $this->encryptionRequired = $encryptionRequired;
97
        $this->keyEncryptionKeySet = $keyEncryptionKeySet;
98
    }
99
100
    /**
101
     * @return string[]
102
     */
103
    public function getSupportedSignatureAlgorithms(): array
104
    {
105
        return $this->jwsVerifier->getSignatureAlgorithmManager()->list();
106
    }
107
108
    /**
109
     * @return string[]
110
     */
111
    public function getSupportedContentEncryptionAlgorithms(): array
112
    {
113
        return null === $this->jweLoader ? [] : $this->jweLoader->getJweDecrypter()->getContentEncryptionAlgorithmManager()->list();
114
    }
115
116
    /**
117
     * @return string[]
118
     */
119
    public function getSupportedKeyEncryptionAlgorithms(): array
120
    {
121
        return null === $this->jweLoader ? [] : $this->jweLoader->getJweDecrypter()->getKeyEncryptionAlgorithmManager()->list();
122
    }
123
124
    public function getSchemesParameters(): array
125
    {
126
        return [];
127
    }
128
129
    public function findClientIdAndCredentials(ServerRequestInterface $request, &$clientCredentials = null): ?ClientId
130
    {
131
        $parameters = RequestBodyParser::parseFormUrlEncoded($request);
132
        if (!\array_key_exists('client_assertion_type', $parameters)) {
133
            return null;
134
        }
135
        $clientAssertionType = $parameters['client_assertion_type'];
136
137
        if (self::CLIENT_ASSERTION_TYPE !== $clientAssertionType) {
138
            return null;
139
        }
140
        if (!\array_key_exists('client_assertion', $parameters)) {
141
            throw new OAuth2Error(400, OAuth2Error::ERROR_INVALID_REQUEST, 'Parameter "client_assertion" is missing.');
142
        }
143
144
        try {
145
            $client_assertion = $parameters['client_assertion'];
146
            $client_assertion = $this->tryToDecryptClientAssertion($client_assertion);
147
            $serializer = new CompactSerializer($this->jsonConverter);
148
            $jws = $serializer->unserialize($client_assertion);
149
            $this->headerCheckerManager->check($jws, 0);
150
            $claims = $this->jsonConverter->decode($jws->getPayload());
151
            $this->claimCheckerManager->check($claims);
152
        } catch (OAuth2Error $e) {
153
            throw $e;
154
        } catch (\Exception $e) {
155
            throw new OAuth2Error(400, OAuth2Error::ERROR_INVALID_REQUEST, 'Unable to load, decrypt or verify the client assertion.', [], $e);
156
        }
157
158
        // FIXME: Other claims can be considered as mandatory by the server
159
        $diff = \array_diff(['iss', 'sub', 'aud', 'exp'], \array_keys($claims));
160
        if (!empty($diff)) {
161
            throw new OAuth2Error(400, OAuth2Error::ERROR_INVALID_REQUEST, \Safe\sprintf('The following claim(s) is/are mandatory: "%s".', \implode(', ', \array_values($diff))));
162
        }
163
164
        $clientCredentials = $jws;
165
166
        return new ClientId($claims['sub']);
167
    }
168
169
    private function tryToDecryptClientAssertion(string $assertion): string
170
    {
171
        if (null === $this->jweLoader) {
172
            return $assertion;
173
        }
174
175
        try {
176
            $jwe = $this->jweLoader->loadAndDecryptWithKeySet($assertion, $this->keyEncryptionKeySet, $recipient);
0 ignored issues
show
Bug introduced by
It seems like $this->keyEncryptionKeySet can be null; however, loadAndDecryptWithKeySet() 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...
177
            if (1 !== $jwe->countRecipients()) {
178
                throw new \InvalidArgumentException('The client assertion must have only one recipient.');
179
            }
180
181
            return $jwe->getPayload();
182
        } catch (\Exception $e) {
183
            if (true === $this->encryptionRequired) {
184
                throw new OAuth2Error(400, OAuth2Error::ERROR_INVALID_REQUEST, 'The encryption of the assertion is mandatory but the decryption of the assertion failed.', [], $e);
185
            }
186
187
            return $assertion;
188
        }
189
    }
190
191
    public function isClientAuthenticated(Client $client, $clientCredentials, ServerRequestInterface $request): bool
192
    {
193
        try {
194
            if (!$clientCredentials instanceof JWS) {
195
                return false;
196
            }
197
198
            $claims = $this->jsonConverter->decode($clientCredentials->getPayload());
199
            $jwkset = $this->retrieveIssuerKeySet($client, $clientCredentials, $claims);
200
201
            return $this->jwsVerifier->verifyWithKeySet($clientCredentials, $jwkset, 0);
202
        } catch (\Exception $e) {
203
            return false;
204
        }
205
    }
206
207
    public function getSupportedMethods(): array
208
    {
209
        return ['client_secret_jwt', 'private_key_jwt'];
210
    }
211
212
    public function checkClientConfiguration(DataBag $commandParameters, DataBag $validatedParameters): DataBag
213
    {
214
        switch ($commandParameters->get('token_endpoint_auth_method')) {
215
            case 'client_secret_jwt':
216
                return $this->checkClientSecretJwtConfiguration($commandParameters, $validatedParameters);
217
            case 'private_key_jwt':
218
                return $this->checkPrivateKeyJwtConfiguration($commandParameters, $validatedParameters);
219
            default:
220
                return $validatedParameters;
221
        }
222
    }
223
224
    private function checkClientSecretJwtConfiguration(DataBag $commandParameters, DataBag $validatedParameters): DataBag
225
    {
226
        $validatedParameters->set('token_endpoint_auth_method', $commandParameters->get('token_endpoint_auth_method'));
227
        $validatedParameters->set('client_secret', $this->createClientSecret());
228
        $validatedParameters->set('client_secret_expires_at', (0 === $this->secretLifetime ? 0 : \time() + $this->secretLifetime));
229
230
        return $validatedParameters;
231
    }
232
233
    private function checkPrivateKeyJwtConfiguration(DataBag $commandParameters, DataBag $validatedParameters): DataBag
234
    {
235
        switch (true) {
236
            case $commandParameters->has('jwks') && $commandParameters->has('jwks_uri'):
237
            case !$commandParameters->has('jwks') && !$commandParameters->has('jwks_uri') && null === $this->trustedIssuerRepository:
238
                throw new \InvalidArgumentException('Either the parameter "jwks" or "jwks_uri" must be set.');
239
            case !$commandParameters->has('jwks') && !$commandParameters->has('jwks_uri') && null !== $this->trustedIssuerRepository: //Allowed when trusted issuer support is set
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
240
241
                break;
242
            case $commandParameters->has('jwks'):
243
                $validatedParameters->set('jwks', $commandParameters->get('jwks'));
244
245
                break;
246
            case $commandParameters->has('jwks_uri'):
247
                $validatedParameters->set('jwks_uri', $commandParameters->get('jwks_uri'));
248
249
                break;
250
        }
251
252
        return $validatedParameters;
253
    }
254
255
    private function createClientSecret(): string
256
    {
257
        return \bin2hex(\random_bytes(32));
258
    }
259
260
    private function retrieveIssuerKeySet(Client $client, JWS $jws, array $claims): JWKSet
261
    {
262
        if ($claims['sub'] === $claims['iss']) { // The client is the issuer
263
            return $this->getClientKeySet($client);
264
        }
265
266
        if (null === $this->trustedIssuerRepository || null === $trustedIssuer = $this->trustedIssuerRepository->find($claims['iss'])) {
267
            throw new \InvalidArgumentException('Unable to retrieve the key set of the issuer.');
268
        }
269
270
        if (!\in_array(self::CLIENT_ASSERTION_TYPE, $trustedIssuer->getAllowedAssertionTypes(), true)) {
271
            throw new \InvalidArgumentException(\Safe\sprintf('The assertion type "%s" is not allowed for that issuer.', self::CLIENT_ASSERTION_TYPE));
272
        }
273
274
        $signatureAlgorithm = $jws->getSignature(0)->getProtectedHeaderParameter('alg');
275
        if (!\in_array($signatureAlgorithm, $trustedIssuer->getAllowedSignatureAlgorithms(), true)) {
276
            throw new \InvalidArgumentException(\Safe\sprintf('The signature algorithm "%s" is not allowed for that issuer.', $signatureAlgorithm));
277
        }
278
279
        return $trustedIssuer->getJWKSet();
280
    }
281
282
    private function getClientKeySet(Client $client): JWKSet
283
    {
284
        switch (true) {
285
            case $client->has('jwks') && 'private_key_jwt' === $client->getTokenEndpointAuthenticationMethod():
286
                $jwks = \Safe\json_decode(\Safe\json_encode($client->get('jwks'), JSON_FORCE_OBJECT), true);
287
288
                return JWKSet::createFromKeyData($jwks);
289
            case $client->has('client_secret') && 'client_secret_jwt' === $client->getTokenEndpointAuthenticationMethod():
290
                $jwk = JWK::create([
291
                    'kty' => 'oct',
292
                    'use' => 'sig',
293
                    'k' => Base64Url::encode($client->get('client_secret')),
294
                ]);
295
296
                return JWKSet::createFromKeys([$jwk]);
297
            case $client->has('jwks_uri') && 'private_key_jwt' === $client->getTokenEndpointAuthenticationMethod() && null !== $this->jkuFactory:
298
                return $this->jkuFactory->loadFromUrl($client->get('jwks_uri'));
299
            default:
300
                throw new \InvalidArgumentException('The client has no key or key set.');
301
        }
302
    }
303
}
304