Passed
Branch master (eb9804)
by Dāvis
04:29
created

OpenIDConnectProvider::getRandomState()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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