Passed
Push — master ( bcf14b...b38da1 )
by Dāvis
04:25
created

OpenIDConnectProvider::getRevokeToken()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 5
nc 1
nop 2
dl 0
loc 9
rs 9.6666
c 0
b 0
f 0
1
<?php
2
3
namespace Sludio\HelperBundle\Openidconnect\Provider;
4
5
use Lcobucci\JWT\Signer;
0 ignored issues
show
Bug introduced by
The type Lcobucci\JWT\Signer 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\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...
7
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...
8
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...
9
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...
10
use League\OAuth2\Client\Token\AccessToken as BaseAccessToken;
0 ignored issues
show
Bug introduced by
The type League\OAuth2\Client\Token\AccessToken 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...
11
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...
12
use Psr\Http\Message\ResponseInterface;
0 ignored issues
show
Bug introduced by
The type Psr\Http\Message\ResponseInterface 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...
13
use Sludio\HelperBundle\Openidconnect\Component\Providerable;
14
use Sludio\HelperBundle\Openidconnect\Security\Exception\InvalidTokenException;
15
use Sludio\HelperBundle\Openidconnect\Specification;
16
use Sludio\HelperBundle\Script\Exception\ErrorException;
17
use Sludio\HelperBundle\Script\Utils\Helper;
18
use Symfony\Bundle\FrameworkBundle\Routing\Router;
19
use Symfony\Component\HttpFoundation\Session\Session;
20
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
21
22
abstract class OpenIDConnectProvider extends AbstractProvider implements Providerable
23
{
24
    const METHOD_POST = 'POST';
25
    const METHOD_GET = 'GET';
26
27
    /**
28
     * @var string
29
     */
30
    protected $publicKey;
31
32
    /**
33
     * @var Signer
34
     */
35
    protected $signer;
36
37
    /**
38
     * @var Specification\ValidatorChain
39
     */
40
    protected $validatorChain;
41
42
    /**
43
     * @var string
44
     */
45
    protected $idTokenIssuer;
46
    /**
47
     * @var Uri[]
48
     */
49
    protected $uris = [];
50
51
    /**
52
     * @var bool
53
     */
54
    protected $useSession;
55
56
    /**
57
     * @var Session
58
     */
59
    protected $session;
60
61
    /**
62
     * @var int
63
     */
64
    protected $statusCode;
65
    /**
66
     * @var Router
67
     */
68
    private $router;
69
70
    /**
71
     * @var string
72
     */
73
    private $baseUri;
74
75
    /**
76
     * @param array   $options
77
     * @param array   $collaborators
78
     * @param Router  $router
79
     * @param Session $session
80
     */
81
    public function __construct(array $options = [], array $collaborators = [], Router $router, Session $session)
82
    {
83
        $this->signer = new Sha256();
84
85
        $this->validatorChain = new Specification\ValidatorChain();
86
        $this->validatorChain->setValidators([
87
            new Specification\NotEmpty('iat', true),
88
            new Specification\GreaterOrEqualsTo('exp', true),
89
            new Specification\EqualsTo('iss', true),
90
            new Specification\EqualsTo('aud', true),
91
            new Specification\NotEmpty('sub', true),
92
            new Specification\LesserOrEqualsTo('nbf'),
93
            new Specification\EqualsTo('jti'),
94
            new Specification\EqualsTo('azp'),
95
            new Specification\EqualsTo('nonce'),
96
        ]);
97
98
        $this->router = $router;
99
        $this->session = $session;
100
101
        parent::__construct($options, $collaborators);
102
        $this->buildParams($options);
103
    }
104
105
    private function buildParams(array $options = [])
106
    {
107
        if (!empty($options)) {
108
            $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...
109
            $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...
110
            unset($options['client_secret'], $options['client_key']);
111
            $this->idTokenIssuer = $options['id_token_issuer'];
112
            $this->publicKey = 'file://'.$options['public_key'];
113
            $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...
114
            $this->baseUri = $options['base_uri'];
115
            $this->useSession = $options['use_session'];
116
            $url = null;
117
            switch ($options['redirect']['type']) {
118
                case 'uri':
119
                    $url = $options['redirect']['uri'];
120
                    break;
121
                case 'route':
122
                    $params = !empty($options['redirect']['params']) ? $options['redirect']['params'] : [];
123
                    $url = $this->router->generate($options['redirect']['route'], $params, UrlGeneratorInterface::ABSOLUTE_URL);
124
                    break;
125
            }
126
            $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...
127
128
            /** @var $options array[] */
129
            foreach ($options['uris'] as $name => $uri) {
130
                $opt = [
131
                    'client_id' => $this->clientId,
132
                    'redirect_uri' => $this->redirectUri,
133
                    'state' => $this->state,
134
                    'base_uri' => $this->baseUri,
135
                ];
136
                $method = isset($uri['method']) ? $uri['method'] : self::METHOD_POST;
137
                $this->uris[$name] = new Uri($uri, $opt, $this->useSession, $method, $this->session);
138
            }
139
        }
140
    }
141
142
    /**
143
     * @inheritdoc
144
     */
145
    protected function getRandomState($length = 32)
146
    {
147
        return Helper::getUniqueId($length);
148
    }
149
150
    /**
151
     * Requests an access token using a specified grant and option set.
152
     *
153
     * @param  mixed $grant
154
     * @param  array $options
155
     *
156
     * @return AccessToken
157
     * @throws InvalidTokenException
158
     * @throws \BadMethodCallException
159
     * @throws ErrorException
160
     */
161
    public function getAccessToken($grant, array $options = [])
162
    {
163
        /** @var AccessToken $token */
164
        $accessToken = $this->getAccessTokenFunction($grant, $options);
165
166
        if (null === $accessToken) {
167
            throw new InvalidTokenException('Invalid access token.');
168
        }
169
170
        $token = $accessToken->getIdToken();
171
        // id_token is empty.
172
        if (null === $token) {
173
            throw new InvalidTokenException('Expected an id_token but did not receive one from the authorization server.');
174
        }
175
176
        // If the ID Token is received via direct communication between the Client and the Token Endpoint
177
        // (which it is in this flow), the TLS server validation MAY be used to validate the issuer in place of checking
178
        // the token signature. The Client MUST validate the signature of all other ID Tokens according to JWS [JWS]
179
        // using the algorithm specified in the JWT alg Header Parameter. The Client MUST use the keys provided by
180
        // the Issuer.
181
        //
182
        // The alg value SHOULD be the default of RS256 or the algorithm sent by the Client in the
183
        // id_token_signed_response_alg parameter during Registration.
184
        if (false === $token->verify($this->signer, $this->getPublicKey())) {
185
            throw new InvalidTokenException('Received an invalid id_token from authorization server.');
186
        }
187
188
        // validations
189
        // @see http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
190
        // validate the iss (issuer)
191
        // - The Issuer Identifier for the OpenID Provider (which is typically obtained during Discovery)
192
        // MUST exactly match the value of the iss (issuer) Claim.
193
        // validate the aud
194
        // - The Client MUST validate that the aud (audience) Claim contains its client_id value registered at the Issuer
195
        // identified by the iss (issuer) Claim as an audience. The aud (audience) Claim MAY contain an array with more
196
        // than one element. The ID Token MUST be rejected if the ID Token does not list the Client as a valid audience,
197
        // or if it contains additional audiences not trusted by the Client.
198
        // - If a nonce value was sent in the Authentication Request, a nonce Claim MUST be present and its value checked
199
        // to verify that it is the same value as the one that was sent in the Authentication Request. The Client SHOULD
200
        // check the nonce value for replay attacks. The precise method for detecting replay attacks is Client specific.
201
        // - If the auth_time Claim was requested, either through a specific request for this Claim or by using
202
        // the max_age parameter, the Client SHOULD check the auth_time Claim value and request re-authentication if it
203
        // determines too much time has elapsed since the last End-User authentication.
204
        // If the acr Claim was requested, the Client SHOULD check that the asserted Claim Value is appropriate.
205
        // The meaning and processing of acr Claim Values is out of scope for this specification.
206
        $currentTime = time();
207
        $data = [
208
            'iss' => $this->getIdTokenIssuer(),
209
            'exp' => $currentTime,
210
            'auth_time' => $currentTime,
211
            'iat' => $currentTime,
212
            'nbf' => $currentTime,
213
            'aud' => $this->clientId,
214
        ];
215
216
        // If the ID Token contains multiple audiences, the Client SHOULD verify that an azp Claim is present.
217
        // If an azp (authorized party) Claim is present, the Client SHOULD verify that its client_id is the Claim Value.
218
        if ($token->hasClaim('azp')) {
219
            $data['azp'] = $this->clientId;
220
        }
221
222
        if (false === $this->validatorChain->validate($data, $token)) {
223
            throw new InvalidTokenException('The id_token did not pass validation.');
224
        }
225
226
        if ($this->useSession) {
227
            $this->session->set('access_token', $accessToken->getToken());
228
            $this->session->set('refresh_token', $accessToken->getRefreshToken());
229
            $this->session->set('id_token', $accessToken->getIdTokenHint());
230
        }
231
232
        return $accessToken;
233
    }
234
235
    /**
236
     * @inheritdoc
237
     */
238
    public function getAccessTokenFunction($grant, array $options = [])
239
    {
240
        $grant = $this->verifyGrant($grant);
241
242
        $params = [
243
            'redirect_uri' => $this->redirectUri,
244
        ];
245
246
        $params = $grant->prepareRequestParameters($params, $options);
247
        $request = $this->getAccessTokenRequest($params);
248
        $response = $this->getResponse($request);
249
        if (!is_array($response)) {
250
            throw new ErrorException('error_invalid_request');
251
        }
252
        $prepared = $this->prepareAccessTokenResponse($response);
253
254
        return $this->createAccessToken($prepared, $grant);
255
    }
256
257
    public function getResponse(RequestInterface $request)
258
    {
259
        $response = $this->sendRequest($request);
260
        $this->statusCode = $response->getStatusCode();
261
        $parsed = $this->parseResponse($response);
262
        $this->checkResponse($response, $parsed);
263
264
        return $parsed;
265
    }
266
267
    protected function checkResponse(ResponseInterface $response, $data)
268
    {
269
    }
270
271
    /**
272
     * Creates an access token from a response.
273
     *
274
     * The grant that was used to fetch the response can be used to provide
275
     * additional context.
276
     *
277
     * @param  array             $response
278
     *
279
     * @param AbstractGrant|null $grant
280
     *
281
     * @return AccessToken
282
     */
283
    protected function createAccessToken(array $response, AbstractGrant $grant = null)
284
    {
285
        if ($this->check($response)) {
286
            return new AccessToken($response);
287
        }
288
289
        return null;
290
    }
291
292
    public function check($response = null)
293
    {
294
        return true;
295
    }
296
297
    public function getPublicKey()
298
    {
299
        return new Key($this->publicKey);
300
    }
301
302
    /**
303
     * Get the issuer of the OpenID Connect id_token
304
     *
305
     * @return string
306
     */
307
    protected function getIdTokenIssuer()
308
    {
309
        return $this->idTokenIssuer;
310
    }
311
312
    public function getBaseAuthorizationUrl()
313
    {
314
        return '';
315
    }
316
317
    public function getBaseAccessTokenUrl(array $params)
318
    {
319
        return '';
320
    }
321
322
    public function getDefaultScopes()
323
    {
324
        return [];
325
    }
326
327
    public function getResourceOwnerDetailsUrl(BaseAccessToken $token)
328
    {
329
    }
330
331
    /**
332
     * Overload parent as OpenID Connect specification states scopes shall be separated by spaces
333
     *
334
     * @return string
335
     */
336
    protected function getScopeSeparator()
337
    {
338
        return ' ';
339
    }
340
341
    protected function createResourceOwner(array $response, BaseAccessToken $token)
342
    {
343
        return [];
344
    }
345
346
    /**
347
     * @return Specification\ValidatorChain
348
     */
349
    public function getValidatorChain()
350
    {
351
        return $this->validatorChain;
352
    }
353
354
    public function getUri($name)
355
    {
356
        return $this->uris[$name];
357
    }
358
359
    public function getRefreshToken($token, array $options = [])
360
    {
361
        $params = [
362
            'token' => $token,
363
            'grant_type' => 'refresh_token',
364
        ];
365
        $params = array_merge($params, $options);
366
        $request = $this->getRefreshTokenRequest($params);
367
368
        return $this->getResponse($request);
369
    }
370
371
    protected function getRefreshTokenRequest(array $params)
372
    {
373
        $method = $this->getAccessTokenMethod();
374
        $url = $this->getRefreshTokenUrl();
375
        $options = $this->getAccessTokenOptions($params);
376
377
        return $this->getRequest($method, $url, $options);
378
    }
379
380
    /**
381
     * Builds request options used for requesting an access token.
382
     *
383
     * @param  array $params
384
     *
385
     * @return array
386
     */
387
    protected function getAccessTokenOptions(array $params)
388
    {
389
        $options = $this->getBaseTokenOptions($params);
390
        $options['headers']['authorization'] = 'Basic: '.base64_encode($this->clientId.':'.$this->clientSecret);
391
392
        return $options;
393
    }
394
395
    protected function getBaseTokenOptions(array $params)
396
    {
397
        $options = [
398
            'headers' => [
399
                'content-type' => 'application/x-www-form-urlencoded',
400
            ],
401
        ];
402
        if ($this->getAccessTokenMethod() === self::METHOD_POST) {
403
            $options['body'] = $this->getAccessTokenBody($params);
404
        }
405
406
        return $options;
407
    }
408
409
    public function getValidateToken($token, array $options = [])
410
    {
411
        $params = [
412
            'token' => $token,
413
        ];
414
        $params = array_merge($params, $options);
415
        $request = $this->getValidateTokenRequest($params);
416
417
        return $this->getResponse($request);
418
    }
419
420
    public function getRevokeToken($token, array $options = [])
421
    {
422
        $params = [
423
            'token' => $token,
424
        ];
425
        $params = array_merge($params, $options);
426
        $request = $this->getRevokeTokenRequest($params);
427
428
        return $this->getResponse($request);
429
    }
430
431
    protected function getValidateTokenRequest(array $params)
432
    {
433
        $method = $this->getAccessTokenMethod();
434
        $url = $this->getValidateTokenUrl();
435
        $options = $this->getBaseTokenOptions($params);
436
437
        return $this->getRequest($method, $url, $options);
438
    }
439
440
    protected function getRevokeTokenRequest(array $params)
441
    {
442
        $method = $this->getAccessTokenMethod();
443
        $url = $this->getRevokeTokenUrl();
444
        $options = $this->getAccessTokenOptions($params);
445
446
        return $this->getRequest($method, $url, $options);
447
    }
448
449
    /**
450
     * @return mixed
451
     */
452
    public function getStatusCode()
453
    {
454
        return $this->statusCode;
455
    }
456
457
    /**
458
     * @param mixed $statusCode
459
     *
460
     * @return $this
461
     */
462
    public function setStatusCode($statusCode)
463
    {
464
        $this->statusCode = $statusCode;
465
466
        return $this;
467
    }
468
469
    /**
470
     * Returns all options that are required.
471
     *
472
     * @return array
473
     */
474
    protected function getRequiredOptions()
475
    {
476
        return [];
477
    }
478
479
    abstract public function getValidateTokenUrl();
480
481
    abstract public function getRefreshTokenUrl();
482
483
    abstract public function getRevokeTokenUrl();
484
}
485