Passed
Push — master ( 9f8783...853dd9 )
by Dāvis
04:39
created

OpenIDConnectProvider::saveSession()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 1
dl 0
loc 6
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;
6
use Lcobucci\JWT\Signer\Rsa\Sha256;
7
use League\OAuth2\Client\Grant\AbstractGrant;
8
use Psr\Http\Message\RequestInterface;
9
use Sludio\HelperBundle\Openidconnect\Component\Providerable;
10
use Sludio\HelperBundle\Openidconnect\Security\Exception\InvalidTokenException;
11
use Sludio\HelperBundle\Openidconnect\Specification;
12
use Sludio\HelperBundle\Script\Security\Exception\ErrorException;
13
use Symfony\Bundle\FrameworkBundle\Routing\Router;
14
use Symfony\Component\HttpFoundation\Session\Session;
15
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
16
use Sludio\HelperBundle\Script\Utils\Generator;
17
18
abstract class OpenIDConnectProvider extends AbstractVariables implements Providerable
19
{
20
    const METHOD_POST = 'POST';
21
    const METHOD_GET = 'GET';
22
23
    /**
24
     * @param array   $options
25
     * @param array   $collaborators
26
     * @param Router  $router
27
     * @param Session $session
28
     */
29
    public function __construct(array $options = [], array $collaborators = [], Router $router, Session $session)
30
    {
31
        $this->signer = new Sha256();
32
33
        $this->validatorChain = new Specification\ValidatorChain();
34
        $this->validatorChain->setValidators([
35
            new Specification\NotEmpty('iat', true),
36
            new Specification\GreaterOrEqualsTo('exp', true),
37
            new Specification\EqualsTo('iss', true),
38
            new Specification\EqualsTo('aud', true),
39
            new Specification\NotEmpty('sub', true),
40
            new Specification\LesserOrEqualsTo('nbf'),
41
            new Specification\EqualsTo('jti'),
42
            new Specification\EqualsTo('azp'),
43
            new Specification\EqualsTo('nonce'),
44
        ]);
45
46
        $this->router = $router;
47
        $this->session = $session;
48
49
        parent::__construct($options, $collaborators);
50
        $this->buildParams($options);
51
    }
52
53
    private function buildParams(array $options = [])
54
    {
55
        if (!empty($options)) {
56
            $this->clientId = $options['client_key'];
57
            $this->clientSecret = $options['client_secret'];
58
            unset($options['client_secret'], $options['client_key']);
59
            $this->idTokenIssuer = $options['id_token_issuer'];
60
            $this->publicKey = 'file://'.$options['public_key'];
61
            $this->state = $this->getRandomState();
62
            $this->baseUri = $options['base_uri'];
63
            $this->useSession = $options['use_session'];
64
            $url = null;
65
            switch ($options['redirect']['type']) {
66
                case 'uri':
67
                    $url = $options['redirect']['uri'];
68
                    break;
69
                case 'route':
70
                    $params = !empty($options['redirect']['params']) ? $options['redirect']['params'] : [];
71
                    $url = $this->router->generate($options['redirect']['route'], $params, UrlGeneratorInterface::ABSOLUTE_URL);
72
                    break;
73
            }
74
            $this->redirectUri = $url;
75
76
            $this->buildUris($options);
77
        }
78
    }
79
80
    /**
81
     * @inheritdoc
82
     */
83
    protected function getRandomState($length = 32)
84
    {
85
        return Generator::getUniqueId($length);
86
    }
87
88
    private function buildUris($options = [])
89
    {
90
        foreach ($options['uris'] as $name => $uri) {
91
            $opt = [
92
                'client_id' => $this->clientId,
93
                'redirect_uri' => $this->redirectUri,
94
                'state' => $this->state,
95
                'base_uri' => $this->baseUri,
96
            ];
97
            $method = isset($uri['method']) ? $uri['method'] : self::METHOD_POST;
98
            $this->uris[$name] = new Uri($uri, $opt, $this->useSession, $method, $this->session);
99
        }
100
    }
101
102
    /**
103
     * Requests an access token using a specified grant and option set.
104
     *
105
     * @param  mixed $grant
106
     * @param  array $options
107
     *
108
     * @return AccessToken
109
     * @throws InvalidTokenException
110
     * @throws \BadMethodCallException
111
     * @throws ErrorException
112
     */
113
    public function getAccessToken($grant, array $options = [])
114
    {
115
        /** @var AccessToken $token */
116
        $accessToken = $this->getAccessTokenFunction($grant, $options);
117
118
        if (null === $accessToken) {
119
            throw new InvalidTokenException('Invalid access token.');
120
        }
121
122
        $token = $accessToken->getIdToken();
123
        // id_token is empty.
124
        if (null === $token) {
125
            throw new InvalidTokenException('Expected an id_token but did not receive one from the authorization server.');
126
        }
127
128
        if (false === $token->verify($this->signer, $this->getPublicKey())) {
129
            throw new InvalidTokenException('Received an invalid id_token from authorization server.');
130
        }
131
132
        $currentTime = time();
133
        $data = [
134
            'iss' => $this->getIdTokenIssuer(),
135
            'exp' => $currentTime,
136
            'auth_time' => $currentTime,
137
            'iat' => $currentTime,
138
            'nbf' => $currentTime,
139
            'aud' => $this->clientId,
140
        ];
141
142
        if ($token->hasClaim('azp')) {
143
            $data['azp'] = $this->clientId;
144
        }
145
146
        if (false === $this->validatorChain->validate($data, $token)) {
147
            throw new InvalidTokenException('The id_token did not pass validation.');
148
        }
149
150
        $this->saveSession($accessToken);
151
152
        return $accessToken;
153
    }
154
155
    private function saveSession($accessToken)
156
    {
157
        if ($this->useSession) {
158
            $this->session->set('access_token', $accessToken->getToken());
159
            $this->session->set('refresh_token', $accessToken->getRefreshToken());
160
            $this->session->set('id_token', $accessToken->getIdTokenHint());
161
        }
162
    }
163
164
    /**
165
     * @inheritdoc
166
     *
167
     * @throws ErrorException
168
     */
169
    public function getAccessTokenFunction($grant, array $options = [])
170
    {
171
        $grant = $this->verifyGrant($grant);
172
173
        $params = [
174
            'redirect_uri' => $this->redirectUri,
175
        ];
176
177
        $params = $grant->prepareRequestParameters($params, $options);
178
        $request = $this->getAccessTokenRequest($params);
179
        $response = $this->getResponse($request);
180
        if (!\is_array($response)) {
0 ignored issues
show
introduced by
The condition is_array($response) is always true.
Loading history...
181
            throw new ErrorException('Invalid request parameters');
182
        }
183
        $prepared = $this->prepareAccessTokenResponse($response);
184
185
        return $this->createAccessToken($prepared, $grant);
186
    }
187
188
    public function getResponse(RequestInterface $request)
189
    {
190
        $response = $this->sendRequest($request);
0 ignored issues
show
Bug introduced by
The method sendRequest() does not exist on Sludio\HelperBundle\Open...r\OpenIDConnectProvider. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

190
        /** @scrutinizer ignore-call */ 
191
        $response = $this->sendRequest($request);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
191
        $this->statusCode = $response->getStatusCode();
192
        $parsed = $this->parseResponse($response);
193
        $this->checkResponse($response, $parsed);
194
195
        return $parsed;
196
    }
197
198
    /**
199
     * Creates an access token from a response.
200
     *
201
     * The grant that was used to fetch the response can be used to provide
202
     * additional context.
203
     *
204
     * @param  array             $response
205
     * @param AbstractGrant|null $grant
206
     *
207
     * @return AccessToken
208
     */
209
    protected function createAccessToken(array $response, AbstractGrant $grant = null)
210
    {
211
        if ($this->check($response)) {
212
            return new AccessToken($response);
213
        }
214
215
        return null;
216
    }
217
218
    public function getPublicKey()
219
    {
220
        return new Key($this->publicKey);
221
    }
222
223
    public function getRefreshToken($token, array $options = [])
224
    {
225
        $params = [
226
            'token' => $token,
227
            'grant_type' => 'refresh_token',
228
        ];
229
        $params = array_merge($params, $options);
230
        $request = $this->getRefreshTokenRequest($params);
231
232
        return $this->getResponse($request);
233
    }
234
235
    protected function getRefreshTokenRequest(array $params)
236
    {
237
        $method = $this->getAccessTokenMethod();
238
        $url = $this->getRefreshTokenUrl();
239
        $options = $this->getAccessTokenOptions($params);
240
241
        return $this->getRequest($method, $url, $options);
242
    }
243
244
    /**
245
     * Builds request options used for requesting an access token.
246
     *
247
     * @param  array $params
248
     *
249
     * @return array
250
     */
251
    protected function getAccessTokenOptions(array $params)
252
    {
253
        $options = $this->getBaseTokenOptions($params);
254
        $options['headers']['authorization'] = 'Basic: '.base64_encode($this->clientId.':'.$this->clientSecret);
255
256
        return $options;
257
    }
258
259
    protected function getBaseTokenOptions(array $params)
260
    {
261
        $options = [
262
            'headers' => [
263
                'content-type' => 'application/x-www-form-urlencoded',
264
            ],
265
        ];
266
        if ($this->getAccessTokenMethod() === self::METHOD_POST) {
267
            $options['body'] = $this->getAccessTokenBody($params);
268
        }
269
270
        return $options;
271
    }
272
273
    public function getValidateToken($token, array $options = [])
274
    {
275
        $params = [
276
            'token' => $token,
277
        ];
278
        $params = array_merge($params, $options);
279
        $request = $this->getValidateTokenRequest($params);
280
281
        return $this->getResponse($request);
282
    }
283
284
    protected function getValidateTokenRequest(array $params)
285
    {
286
        $method = $this->getAccessTokenMethod();
287
        $url = $this->getValidateTokenUrl();
288
        $options = $this->getBaseTokenOptions($params);
289
290
        return $this->getRequest($method, $url, $options);
291
    }
292
293
    public function getRevokeToken($token, array $options = [])
294
    {
295
        $params = [
296
            'token' => $token,
297
        ];
298
        $params = array_merge($params, $options);
299
        $request = $this->getRevokeTokenRequest($params);
300
301
        return $this->getResponse($request);
302
    }
303
304
    protected function getRevokeTokenRequest(array $params)
305
    {
306
        $method = $this->getAccessTokenMethod();
307
        $url = $this->getRevokeTokenUrl();
308
        $options = $this->getAccessTokenOptions($params);
309
310
        return $this->getRequest($method, $url, $options);
311
    }
312
313
    protected function getAllowedClientOptions(array $options)
314
    {
315
        return [
316
            'timeout',
317
            'proxy',
318
            'verify',
319
        ];
320
    }
321
322
    protected function parseJson($content)
323
    {
324
        if (empty($content)) {
325
            return [];
326
        }
327
328
        $content = json_decode($content, true);
329
        if (json_last_error() !== JSON_ERROR_NONE) {
330
            throw new \UnexpectedValueException(sprintf("Failed to parse JSON response: %s", json_last_error_msg()));
331
        }
332
333
        return $content;
334
    }
335
}
336