Failed Conditions
Push — master ( 2eee97...42d85f )
by Florent
09:02
created

ClientAssertionJwt   A

Complexity

Total Complexity 24

Size/Duplication

Total Lines 203
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 13

Importance

Changes 0
Metric Value
wmc 24
lcom 2
cbo 13
dl 0
loc 203
rs 10
c 0
b 0
f 0

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 1
A enableEncryptedAssertions() 0 5 1
A getSupportedSignatureAlgorithms() 0 4 1
A getSupportedContentEncryptionAlgorithms() 0 4 2
A getSupportedKeyEncryptionAlgorithms() 0 4 2
A getSchemesParameters() 0 4 1
B findClientId() 0 32 4
A tryToDecryptClientAssertion() 0 19 4
A isClientAuthenticated() 0 12 2
A getSupportedAuthenticationMethods() 0 4 1
B checkClientConfiguration() 0 22 5
createClientSecret() 0 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\TokenEndpointAuthMethod;
15
16
use Assert\Assertion;
17
use Jose\Component\Checker\ClaimCheckerManager;
18
use Jose\Component\Core\JWKSet;
19
use Jose\Component\Encryption\JWELoader;
20
use Jose\Component\Signature\JWSLoader;
21
use OAuth2Framework\Component\Server\Model\Client\Client;
22
use OAuth2Framework\Component\Server\Model\Client\ClientId;
23
use OAuth2Framework\Component\Server\Model\DataBag\DataBag;
24
use OAuth2Framework\Component\Server\Response\OAuth2Exception;
25
use OAuth2Framework\Component\Server\Response\OAuth2ResponseFactoryManager;
26
use Psr\Http\Message\ServerRequestInterface;
27
28
abstract class ClientAssertionJwt implements TokenEndpointAuthMethodInterface
29
{
30
    /**
31
     * @var JWSLoader
32
     */
33
    private $jwsLoader;
34
35
    /**
36
     * @var JWELoader
37
     */
38
    private $jweLoader;
39
40
    /**
41
     * @var bool
42
     */
43
    private $encryptionRequired = false;
44
45
    /**
46
     * @var JWKSet|null
47
     */
48
    private $keyEncryptionKeySet = null;
49
50
    /**
51
     * @var int
52
     */
53
    private $secretLifetime;
54
55
    /**
56
     * @var ClaimCheckerManager
57
     */
58
    private $claimCheckerManager;
59
60
    /**
61
     * ClientAssertionJwt constructor.
62
     *
63
     * @param JWSLoader           $jwsLoader
64
     * @param ClaimCheckerManager $claimCheckerManager
65
     * @param int                 $secretLifetime
66
     */
67
    public function __construct(JWSLoader $jwsLoader, ClaimCheckerManager $claimCheckerManager, int $secretLifetime = 0)
68
    {
69
        Assertion::greaterOrEqualThan($secretLifetime, 0);
70
        $this->jwsLoader = $jwsLoader;
71
        $this->claimCheckerManager = $claimCheckerManager;
72
        $this->secretLifetime = $secretLifetime;
73
    }
74
75
    /**
76
     * @param bool                         $encryptionRequired
77
     * @param JWKSet $keyEncryptionKeySet
78
     */
79
    public function enableEncryptedAssertions(bool $encryptionRequired, JWKSet $keyEncryptionKeySet)
80
    {
81
        $this->encryptionRequired = $encryptionRequired;
82
        $this->keyEncryptionKeySet = $keyEncryptionKeySet;
83
    }
84
85
    /**
86
     * @return string[]
87
     */
88
    public function getSupportedSignatureAlgorithms(): array
89
    {
90
        return $this->jwsLoader->getSignatureAlgorithmManager()->list();
91
    }
92
93
    /**
94
     * @return string[]
95
     */
96
    public function getSupportedContentEncryptionAlgorithms(): array
97
    {
98
        return null === $this->jweLoader ? [] : $this->jweLoader->getContentEncryptionAlgorithmManager()->list();
99
    }
100
101
    /**
102
     * @return string[]
103
     */
104
    public function getSupportedKeyEncryptionAlgorithms(): array
105
    {
106
        return null === $this->jweLoader ? [] : $this->jweLoader->getKeyEncryptionAlgorithmManager()->list();
107
    }
108
109
    /**
110
     * {@inheritdoc}
111
     */
112
    public function getSchemesParameters(): array
113
    {
114
        return [];
115
    }
116
117
    /**
118
     * {@inheritdoc}
119
     */
120
    public function findClientId(ServerRequestInterface $request, &$clientCredentials = null): ? ClientId
121
    {
122
        $parameters = $request->getParsedBody() ?? [];
123
        if (!array_key_exists('client_assertion_type', $parameters)) {
124
            return null;
125
        }
126
        $clientAssertionType = $parameters['client_assertion_type'];
127
128
        //We verify the client assertion type in the request
129
        if ('urn:ietf:params:oauth:client-assertion-type:jwt-bearer' !== $clientAssertionType) {
130
            return null;
131
        }
132
133
        try {
134
            Assertion::keyExists($parameters, 'client_assertion', 'Parameter \'client_assertion\' is missing.');
135
            $client_assertion = $parameters['client_assertion'];
136
            $client_assertion = $this->tryToDecryptClientAssertion($client_assertion);
137
            $jwt = $this->jwsLoader->load($client_assertion);
138
            $this->claimCheckerManager->check($jwt);
139
            $claims = json_decode($jwt->getPayload(), true);
140
141
            $diff = array_diff(['iss', 'sub', 'aud', 'jti', 'exp'], array_keys($claims));
142
            Assertion::eq(0, count($diff), sprintf('The following claim(s) is/are mandatory: \'%s\'.', implode(', ', array_values($diff))));
143
            Assertion::eq($claims['sub'], $claims['iss'], 'The claims \'sub\' and \'iss\' must contain the client public ID.');
144
        } catch (\Exception $e) {
145
            throw new OAuth2Exception(400, ['error' => OAuth2ResponseFactoryManager::ERROR_INVALID_REQUEST, 'error_description' => $e->getMessage()]);
146
        }
147
148
        $clientCredentials = $jwt;
149
150
        return ClientId::create($claims['sub']);
151
    }
152
153
    /**
154
     * {@inheritdoc}
155
     */
156
    private function tryToDecryptClientAssertion(string $clientAssertion): string
157
    {
158
        if (null === $this->jweLoader) {
159
            return $clientAssertion;
160
        }
161
162
        try {
163
            $jwe = $this->jweLoader->load($clientAssertion);
164
            $jwe = $this->jweLoader->decryptUsingKeySet($jwe, $this->keyEncryptionKeySet);
0 ignored issues
show
Bug introduced by
It seems like $this->keyEncryptionKeySet can be null; however, decryptUsingKeySet() 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...
165
166
            return $jwe->getPayload();
167
        } catch (\Exception $e) {
168
            if (true === $this->encryptionRequired) {
169
                throw new OAuth2Exception(400, ['error' => OAuth2ResponseFactoryManager::ERROR_INVALID_REQUEST, 'error_description' => $e->getMessage()]);
170
            }
171
172
            return $clientAssertion;
173
        }
174
    }
175
176
    /**
177
     * {@inheritdoc}
178
     */
179
    public function isClientAuthenticated(Client $client, $clientCredentials, ServerRequestInterface $request): bool
180
    {
181
        try {
182
            $jwkSet = $client->getPublicKeySet();
183
            Assertion::isInstanceOf($jwkSet, JWKSet::class);
184
            $this->jwsLoader->verifyWithKeySet($clientCredentials, $jwkSet);
185
        } catch (\Exception $e) {
186
            return false;
187
        }
188
189
        return true;
190
    }
191
192
    /**
193
     * {@inheritdoc}
194
     */
195
    public function getSupportedAuthenticationMethods(): array
196
    {
197
        return ['client_secret_jwt', 'private_key_jwt'];
198
    }
199
200
    /**
201
     * {@inheritdoc}
202
     */
203
    public function checkClientConfiguration(DataBag $commandParameters, DataBag $validatedParameters): DataBag
204
    {
205
        if ('client_secret_jwt' === $commandParameters->get('token_endpoint_auth_method')) {
206
            $validatedParameters = $validatedParameters->with('client_secret', $this->createClientSecret());
207
            $validatedParameters = $validatedParameters->with('client_secret_expires_at', (0 === $this->secretLifetime ? 0 : time() + $this->secretLifetime));
208
        } elseif ('private_key_jwt' === $commandParameters->get('token_endpoint_auth_method')) {
209
            Assertion::true($commandParameters->has('jwks') xor $commandParameters->has('jwks_uri'), 'The parameter \'jwks\' or \'jwks_uri\' must be set.');
210
            if ($commandParameters->has('jwks')) {
211
                $jwks = JWKSet::createFromKeyData($commandParameters->get('jwks'));
212
                Assertion::isInstanceOf($jwks, JWKSet::class, 'The parameter \'jwks\' must be a valid JWKSet object.');
213
                $validatedParameters = $validatedParameters->with('jwks', $commandParameters->get('jwks'));
214
            }/* else {
0 ignored issues
show
Unused Code Comprehensibility introduced by
60% 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...
215
                $jwks = JWKFactory::createFromJKU($commandParameters->get('jwks_uri'));
216
                Assertion::isInstanceOf($jwks, JWKSet::class, 'The parameter \'jwks_uri\' must be a valid uri that provide a valid JWKSet.');
217
                $validatedParameters = $validatedParameters->with('jwks_uri', $commandParameters->get('jwks_uri'));
218
            }*/
219
        } else {
220
            throw new \InvalidArgumentException('Unsupported token endpoint authentication method.');
221
        }
222
223
        return $validatedParameters;
224
    }
225
226
    /**
227
     * @return string
228
     */
229
    abstract protected function createClientSecret(): string;
230
}
231