Passed
Push — master ( 442a9a...3948f8 )
by Dāvis
05:23
created

Openidconnect/Provider/OpenIDConnectProvider.php (2 issues)

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 Sludio\HelperBundle\Script\Utils\Helper;
14
use Symfony\Bundle\FrameworkBundle\Routing\Router;
15
use Symfony\Component\HttpFoundation\Session\Session;
16
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
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 Helper::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
        if ($this->useSession) {
151
            $this->session->set('access_token', $accessToken->getToken());
152
            $this->session->set('refresh_token', $accessToken->getRefreshToken());
153
            $this->session->set('id_token', $accessToken->getIdTokenHint());
154
        }
155
156
        return $accessToken;
157
    }
158
159
    /**
160
     * @inheritdoc
161
     *
162
     * @throws ErrorException
163
     */
164
    public function getAccessTokenFunction($grant, array $options = [])
165
    {
166
        $grant = $this->verifyGrant($grant);
167
168
        $params = [
169
            'redirect_uri' => $this->redirectUri,
170
        ];
171
172
        $params = $grant->prepareRequestParameters($params, $options);
173
        $request = $this->getAccessTokenRequest($params);
174
        $response = $this->getResponse($request);
175
        if (!\is_array($response)) {
0 ignored issues
show
The condition is_array($response) is always true.
Loading history...
176
            throw new ErrorException('Invalid request parameters');
177
        }
178
        $prepared = $this->prepareAccessTokenResponse($response);
179
180
        return $this->createAccessToken($prepared, $grant);
181
    }
182
183
    public function getResponse(RequestInterface $request)
184
    {
185
        $response = $this->sendRequest($request);
0 ignored issues
show
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

185
        /** @scrutinizer ignore-call */ 
186
        $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...
186
        $this->statusCode = $response->getStatusCode();
187
        $parsed = $this->parseResponse($response);
188
        $this->checkResponse($response, $parsed);
189
190
        return $parsed;
191
    }
192
193
    /**
194
     * Creates an access token from a response.
195
     *
196
     * The grant that was used to fetch the response can be used to provide
197
     * additional context.
198
     *
199
     * @param  array             $response
200
     * @param AbstractGrant|null $grant
201
     *
202
     * @return AccessToken
203
     */
204
    protected function createAccessToken(array $response, AbstractGrant $grant = null)
205
    {
206
        if ($this->check($response)) {
207
            return new AccessToken($response);
208
        }
209
210
        return null;
211
    }
212
213
    public function getPublicKey()
214
    {
215
        return new Key($this->publicKey);
216
    }
217
218
    public function getRefreshToken($token, array $options = [])
219
    {
220
        $params = [
221
            'token' => $token,
222
            'grant_type' => 'refresh_token',
223
        ];
224
        $params = array_merge($params, $options);
225
        $request = $this->getRefreshTokenRequest($params);
226
227
        return $this->getResponse($request);
228
    }
229
230
    protected function getRefreshTokenRequest(array $params)
231
    {
232
        $method = $this->getAccessTokenMethod();
233
        $url = $this->getRefreshTokenUrl();
234
        $options = $this->getAccessTokenOptions($params);
235
236
        return $this->getRequest($method, $url, $options);
237
    }
238
239
    /**
240
     * Builds request options used for requesting an access token.
241
     *
242
     * @param  array $params
243
     *
244
     * @return array
245
     */
246
    protected function getAccessTokenOptions(array $params)
247
    {
248
        $options = $this->getBaseTokenOptions($params);
249
        $options['headers']['authorization'] = 'Basic: '.base64_encode($this->clientId.':'.$this->clientSecret);
250
251
        return $options;
252
    }
253
254
    protected function getBaseTokenOptions(array $params)
255
    {
256
        $options = [
257
            'headers' => [
258
                'content-type' => 'application/x-www-form-urlencoded',
259
            ],
260
        ];
261
        if ($this->getAccessTokenMethod() === self::METHOD_POST) {
262
            $options['body'] = $this->getAccessTokenBody($params);
263
        }
264
265
        return $options;
266
    }
267
268
    public function getValidateToken($token, array $options = [])
269
    {
270
        $params = [
271
            'token' => $token,
272
        ];
273
        $params = array_merge($params, $options);
274
        $request = $this->getValidateTokenRequest($params);
275
276
        return $this->getResponse($request);
277
    }
278
279
    protected function getValidateTokenRequest(array $params)
280
    {
281
        $method = $this->getAccessTokenMethod();
282
        $url = $this->getValidateTokenUrl();
283
        $options = $this->getBaseTokenOptions($params);
284
285
        return $this->getRequest($method, $url, $options);
286
    }
287
288
    public function getRevokeToken($token, array $options = [])
289
    {
290
        $params = [
291
            'token' => $token,
292
        ];
293
        $params = array_merge($params, $options);
294
        $request = $this->getRevokeTokenRequest($params);
295
296
        return $this->getResponse($request);
297
    }
298
299
    protected function getRevokeTokenRequest(array $params)
300
    {
301
        $method = $this->getAccessTokenMethod();
302
        $url = $this->getRevokeTokenUrl();
303
        $options = $this->getAccessTokenOptions($params);
304
305
        return $this->getRequest($method, $url, $options);
306
    }
307
}
308