Passed
Push — master ( b38da1...a59646 )
by Dāvis
04:33
created

OpenIDConnectProvider::buildUris()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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