Passed
Push — master ( 9b369b...9fe481 )
by Dāvis
04:59
created

OpenIDConnectProvider::getValidateTokenRequest()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 1
dl 0
loc 7
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace Sludio\HelperBundle\Openidconnect\Provider;
4
5
use Lcobucci\JWT\Signer\Key;
0 ignored issues
show
Bug introduced by
The type Lcobucci\JWT\Signer\Key was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
6
use Lcobucci\JWT\Signer\Rsa\Sha256;
0 ignored issues
show
Bug introduced by
The type Lcobucci\JWT\Signer\Rsa\Sha256 was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
7
use League\OAuth2\Client\Grant\AbstractGrant;
0 ignored issues
show
Bug introduced by
The type League\OAuth2\Client\Grant\AbstractGrant was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
8
use League\OAuth2\Client\Provider\AbstractProvider;
0 ignored issues
show
Bug introduced by
The type League\OAuth2\Client\Provider\AbstractProvider was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
9
use Psr\Http\Message\RequestInterface;
0 ignored issues
show
Bug introduced by
The type Psr\Http\Message\RequestInterface was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
10
use Sludio\HelperBundle\Openidconnect\Component\Providerable;
11
use Sludio\HelperBundle\Openidconnect\Security\Exception\InvalidTokenException;
12
use Sludio\HelperBundle\Openidconnect\Specification;
13
use Sludio\HelperBundle\Script\Security\Exception\ErrorException;
14
use Sludio\HelperBundle\Script\Utils\Helper;
15
use Symfony\Bundle\FrameworkBundle\Routing\Router;
16
use Symfony\Component\HttpFoundation\Session\Session;
17
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
18
19
abstract class OpenIDConnectProvider extends AbstractProvider implements Providerable
20
{
21
    use VariableTrait;
22
23
    const METHOD_POST = 'POST';
24
    const METHOD_GET = 'GET';
25
26
    /**
27
     * @param array   $options
28
     * @param array   $collaborators
29
     * @param Router  $router
30
     * @param Session $session
31
     */
32
    public function __construct(array $options = [], array $collaborators = [], Router $router, Session $session)
33
    {
34
        $this->signer = new Sha256();
35
36
        $this->validatorChain = new Specification\ValidatorChain();
37
        $this->validatorChain->setValidators([
38
            new Specification\NotEmpty('iat', true),
39
            new Specification\GreaterOrEqualsTo('exp', true),
40
            new Specification\EqualsTo('iss', true),
41
            new Specification\EqualsTo('aud', true),
42
            new Specification\NotEmpty('sub', true),
43
            new Specification\LesserOrEqualsTo('nbf'),
44
            new Specification\EqualsTo('jti'),
45
            new Specification\EqualsTo('azp'),
46
            new Specification\EqualsTo('nonce'),
47
        ]);
48
49
        $this->router = $router;
50
        $this->session = $session;
51
52
        parent::__construct($options, $collaborators);
53
        $this->buildParams($options);
54
    }
55
56
    private function buildParams(array $options = [])
57
    {
58
        if (!empty($options)) {
59
            $this->clientId = $options['client_key'];
0 ignored issues
show
Bug Best Practice introduced by
The property clientId does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
60
            $this->clientSecret = $options['client_secret'];
0 ignored issues
show
Bug Best Practice introduced by
The property clientSecret does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
61
            unset($options['client_secret'], $options['client_key']);
62
            $this->idTokenIssuer = $options['id_token_issuer'];
63
            $this->publicKey = 'file://'.$options['public_key'];
64
            $this->state = $this->getRandomState();
0 ignored issues
show
Bug Best Practice introduced by
The property state does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
65
            $this->baseUri = $options['base_uri'];
66
            $this->useSession = $options['use_session'];
67
            $url = null;
68
            switch ($options['redirect']['type']) {
69
                case 'uri':
70
                    $url = $options['redirect']['uri'];
71
                    break;
72
                case 'route':
73
                    $params = !empty($options['redirect']['params']) ? $options['redirect']['params'] : [];
74
                    $url = $this->router->generate($options['redirect']['route'], $params, UrlGeneratorInterface::ABSOLUTE_URL);
75
                    break;
76
            }
77
            $this->redirectUri = $url;
0 ignored issues
show
Bug Best Practice introduced by
The property redirectUri does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
78
79
            $this->buildUris($options);
80
        }
81
    }
82
83
    /**
84
     * @inheritdoc
85
     */
86
    protected function getRandomState($length = 32)
87
    {
88
        return Helper::getUniqueId($length);
89
    }
90
91
    private function buildUris($options = [])
92
    {
93
        foreach ($options['uris'] as $name => $uri) {
94
            $opt = [
95
                'client_id' => $this->clientId,
96
                'redirect_uri' => $this->redirectUri,
97
                'state' => $this->state,
98
                'base_uri' => $this->baseUri,
99
            ];
100
            $method = isset($uri['method']) ? $uri['method'] : self::METHOD_POST;
101
            $this->uris[$name] = new Uri($uri, $opt, $this->useSession, $method, $this->session);
102
        }
103
    }
104
105
    /**
106
     * Requests an access token using a specified grant and option set.
107
     *
108
     * @param  mixed $grant
109
     * @param  array $options
110
     *
111
     * @return AccessToken
112
     * @throws InvalidTokenException
113
     * @throws \BadMethodCallException
114
     * @throws ErrorException
115
     */
116
    public function getAccessToken($grant, array $options = [])
117
    {
118
        /** @var AccessToken $token */
119
        $accessToken = $this->getAccessTokenFunction($grant, $options);
120
121
        if (null === $accessToken) {
122
            throw new InvalidTokenException('Invalid access token.');
123
        }
124
125
        $token = $accessToken->getIdToken();
126
        // id_token is empty.
127
        if (null === $token) {
128
            throw new InvalidTokenException('Expected an id_token but did not receive one from the authorization server.');
129
        }
130
131
        // If the ID Token is received via direct communication between the Client and the Token Endpoint
132
        // (which it is in this flow), the TLS server validation MAY be used to validate the issuer in place of checking
133
        // the token signature. The Client MUST validate the signature of all other ID Tokens according to JWS [JWS]
134
        // using the algorithm specified in the JWT alg Header Parameter. The Client MUST use the keys provided by
135
        // the Issuer.
136
        //
137
        // The alg value SHOULD be the default of RS256 or the algorithm sent by the Client in the
138
        // id_token_signed_response_alg parameter during Registration.
139
        if (false === $token->verify($this->signer, $this->getPublicKey())) {
140
            throw new InvalidTokenException('Received an invalid id_token from authorization server.');
141
        }
142
143
        // validations
144
        // @see http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
145
        // validate the iss (issuer)
146
        // - The Issuer Identifier for the OpenID Provider (which is typically obtained during Discovery)
147
        // MUST exactly match the value of the iss (issuer) Claim.
148
        // validate the aud
149
        // - The Client MUST validate that the aud (audience) Claim contains its client_id value registered at the Issuer
150
        // identified by the iss (issuer) Claim as an audience. The aud (audience) Claim MAY contain an array with more
151
        // than one element. The ID Token MUST be rejected if the ID Token does not list the Client as a valid audience,
152
        // or if it contains additional audiences not trusted by the Client.
153
        // - If a nonce value was sent in the Authentication Request, a nonce Claim MUST be present and its value checked
154
        // to verify that it is the same value as the one that was sent in the Authentication Request. The Client SHOULD
155
        // check the nonce value for replay attacks. The precise method for detecting replay attacks is Client specific.
156
        // - If the auth_time Claim was requested, either through a specific request for this Claim or by using
157
        // the max_age parameter, the Client SHOULD check the auth_time Claim value and request re-authentication if it
158
        // determines too much time has elapsed since the last End-User authentication.
159
        // If the acr Claim was requested, the Client SHOULD check that the asserted Claim Value is appropriate.
160
        // The meaning and processing of acr Claim Values is out of scope for this specification.
161
        $currentTime = time();
162
        $data = [
163
            'iss' => $this->getIdTokenIssuer(),
164
            'exp' => $currentTime,
165
            'auth_time' => $currentTime,
166
            'iat' => $currentTime,
167
            'nbf' => $currentTime,
168
            'aud' => $this->clientId,
169
        ];
170
171
        // If the ID Token contains multiple audiences, the Client SHOULD verify that an azp Claim is present.
172
        // If an azp (authorized party) Claim is present, the Client SHOULD verify that its client_id is the Claim Value.
173
        if ($token->hasClaim('azp')) {
174
            $data['azp'] = $this->clientId;
175
        }
176
177
        if (false === $this->validatorChain->validate($data, $token)) {
178
            throw new InvalidTokenException('The id_token did not pass validation.');
179
        }
180
181
        if ($this->useSession) {
182
            $this->session->set('access_token', $accessToken->getToken());
183
            $this->session->set('refresh_token', $accessToken->getRefreshToken());
184
            $this->session->set('id_token', $accessToken->getIdTokenHint());
185
        }
186
187
        return $accessToken;
188
    }
189
190
    /**
191
     * @inheritdoc
192
     *
193
     * @throws ErrorException
194
     */
195
    public function getAccessTokenFunction($grant, array $options = [])
196
    {
197
        $grant = $this->verifyGrant($grant);
198
199
        $params = [
200
            'redirect_uri' => $this->redirectUri,
201
        ];
202
203
        $params = $grant->prepareRequestParameters($params, $options);
204
        $request = $this->getAccessTokenRequest($params);
205
        $response = $this->getResponse($request);
206
        if (!\is_array($response)) {
207
            throw new ErrorException('Invalid request parameters');
208
        }
209
        $prepared = $this->prepareAccessTokenResponse($response);
210
211
        return $this->createAccessToken($prepared, $grant);
212
    }
213
214
    public function getResponse(RequestInterface $request)
215
    {
216
        $response = $this->sendRequest($request);
217
        $this->statusCode = $response->getStatusCode();
218
        $parsed = $this->parseResponse($response);
219
        $this->checkResponse($response, $parsed);
220
221
        return $parsed;
222
    }
223
224
    /**
225
     * Creates an access token from a response.
226
     *
227
     * The grant that was used to fetch the response can be used to provide
228
     * additional context.
229
     *
230
     * @param  array             $response
231
     * @param AbstractGrant|null $grant
232
     *
233
     * @return AccessToken
234
     */
235
    protected function createAccessToken(array $response, AbstractGrant $grant = null)
236
    {
237
        if ($this->check($response)) {
238
            return new AccessToken($response);
239
        }
240
241
        return null;
242
    }
243
244
    public function getPublicKey()
245
    {
246
        return new Key($this->publicKey);
247
    }
248
249
    public function getRefreshToken($token, array $options = [])
250
    {
251
        $params = [
252
            'token' => $token,
253
            'grant_type' => 'refresh_token',
254
        ];
255
        $params = array_merge($params, $options);
256
        $request = $this->getRefreshTokenRequest($params);
257
258
        return $this->getResponse($request);
259
    }
260
261
    protected function getRefreshTokenRequest(array $params)
262
    {
263
        $method = $this->getAccessTokenMethod();
264
        $url = $this->getRefreshTokenUrl();
265
        $options = $this->getAccessTokenOptions($params);
266
267
        return $this->getRequest($method, $url, $options);
268
    }
269
270
    /**
271
     * Builds request options used for requesting an access token.
272
     *
273
     * @param  array $params
274
     *
275
     * @return array
276
     */
277
    protected function getAccessTokenOptions(array $params)
278
    {
279
        $options = $this->getBaseTokenOptions($params);
280
        $options['headers']['authorization'] = 'Basic: '.base64_encode($this->clientId.':'.$this->clientSecret);
281
282
        return $options;
283
    }
284
285
    protected function getBaseTokenOptions(array $params)
286
    {
287
        $options = [
288
            'headers' => [
289
                'content-type' => 'application/x-www-form-urlencoded',
290
            ],
291
        ];
292
        if ($this->getAccessTokenMethod() === self::METHOD_POST) {
293
            $options['body'] = $this->getAccessTokenBody($params);
294
        }
295
296
        return $options;
297
    }
298
299
    public function getValidateToken($token, array $options = [])
300
    {
301
        $params = [
302
            'token' => $token,
303
        ];
304
        $params = array_merge($params, $options);
305
        $request = $this->getValidateTokenRequest($params);
306
307
        return $this->getResponse($request);
308
    }
309
310
    protected function getValidateTokenRequest(array $params)
311
    {
312
        $method = $this->getAccessTokenMethod();
313
        $url = $this->getValidateTokenUrl();
314
        $options = $this->getBaseTokenOptions($params);
315
316
        return $this->getRequest($method, $url, $options);
317
    }
318
319
    public function getRevokeToken($token, array $options = [])
320
    {
321
        $params = [
322
            'token' => $token,
323
        ];
324
        $params = array_merge($params, $options);
325
        $request = $this->getRevokeTokenRequest($params);
326
327
        return $this->getResponse($request);
328
    }
329
330
    protected function getRevokeTokenRequest(array $params)
331
    {
332
        $method = $this->getAccessTokenMethod();
333
        $url = $this->getRevokeTokenUrl();
334
        $options = $this->getAccessTokenOptions($params);
335
336
        return $this->getRequest($method, $url, $options);
337
    }
338
}
339