Completed
Push — master ( aebd05...0c0a9d )
by Alexandre
02:31
created

AuthorizationEndpoint::checkResponseMode()   D

Complexity

Conditions 10
Paths 42

Size

Total Lines 39
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 110

Importance

Changes 0
Metric Value
cc 10
eloc 22
nc 42
nop 2
dl 0
loc 39
ccs 0
cts 0
cp 0
crap 110
rs 4.8196
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * Created by PhpStorm.
4
 * User: GCC-MED
5
 * Date: 19/01/2018
6
 * Time: 16:02
7
 */
8
9
namespace OAuth2\OpenID\Endpoints;
10
11
12
use GuzzleHttp\Psr7\Response;
13
use GuzzleHttp\Psr7\Uri;
14
use OAuth2\Config;
15
use OAuth2\EndpointMessages\Authorization\AuthorizationResponse;
16
use OAuth2\EndpointMessages\Authorization\ErrorResponse;
17
use OAuth2\Exceptions\OAuthException;
18
use OAuth2\ResponseTypes\ResponseTypeInterface;
19
use OAuth2\Roles\ClientInterface;
20
use OAuth2\Roles\Clients\RegisteredClient;
21
use OAuth2\Roles\ResourceOwnerInterface;
22
use OAuth2\Storages\ClientStorageInterface;
23
use Psr\Http\Message\ResponseInterface;
24
use Psr\Http\Message\ServerRequestInterface;
25
26
class AuthorizationEndpoint extends \OAuth2\Endpoints\AuthorizationEndpoint
27
{
28
    /**
29
     * @param ServerRequestInterface $request
30
     * @param ResourceOwnerInterface $resourceOwner
31
     * @return ResponseInterface
32
     */
33
    public function handle(ServerRequestInterface $request, ResourceOwnerInterface $resourceOwner): ResponseInterface
34
    {
35
        /**
36
         * @var RegisteredClient $client
37
         */
38
        $result = $this->verify($request);
39
40
        if ($result instanceof Response) {
0 ignored issues
show
introduced by
The condition $result instanceof GuzzleHttp\Psr7\Response can never be true since $result is never a sub-type of GuzzleHttp\Psr7\Response.
Loading history...
41
            return $result;
42
        }
43
44
        /**
45
         * @var RegisteredClient $client
46
         * @var Uri $redirectUri
47
         * @var array $responseTypes
48
         * @var string|null $responseMode
49
         * @var array|null $scope
50
         * @var bool $isInsecure
51
         * @var array $data
52
         */
53
        extract($result);
54
55
        $this->verify($request);
56
57
58
        switch ($responseMode) {
59
            case 'query':
60
                break;
61
            case 'form_post':
62
                break;
63
            case 'fragment':
64
            default:
65
                break;
66
        }
67
68
        // https://developer.okta.com/docs/api/resources/oidc#parameter-details
69
        if (isset($data['response_mode']) && $data['response_mode'] == 'post_message') {
70
            return $this->popupResponse(['access_token' => 'a'], $redirectUri);
71
        }
72
        else {
73
            return new AuthorizationResponse($redirectUri);
74
        }
75
    }
76
77
    /**
78
     * @param ServerRequestInterface $request
79
     * @return Response
80
     * @throws \Exception
81
     */
82
    public function beforeConsent(ServerRequestInterface $request): ?Response
83
    {
84
        /**
85
         * @var RegisteredClient $client
86
         */
87
        $result = $this->verify($request);
88
        if ($result instanceof Response) {
0 ignored issues
show
introduced by
The condition $result instanceof GuzzleHttp\Psr7\Response can never be true since $result is never a sub-type of GuzzleHttp\Psr7\Response.
Loading history...
89
            return $result;
90
        }
91
92
        /**
93
         * @var RegisteredClient $client
94
         * @var Uri $redirectUri
95
         * @var array $responseTypes
96
         * @var string $responseMode
97
         * @var array|null $scope
98
         * @var bool $isInsecure
99
         * @var array $data
100
         */
101
        extract($result);
102
103
        // Si le consentement est requis, retourné null sauf si prompt=none
104
        // l'affichage du consentement peut utiliser le paramètre 'display'
105
106
107
108
109
110
        return new AuthorizationResponse($redirectUri);
111
    }
112
113
    /**
114
     * @param ServerRequestInterface $request
115
     * @return array|Response
116
     * @throws \Exception
117
     */
118
    protected function verify(ServerRequestInterface $request)
119
    {
120
        try {
121
            // Validate all oauth2 parameters
122
            // Verify that scope parameter is present and contains openid scope. If not, use basic oauth2 authorization endpoint
123
            // Verify all open id parameters
124
            // check sub (subject) claim that is the current user authenticated
125
            // check user authenticated or re-authenticate him if prompt=login
126
            // no interaction if prompt=none => no consent required, check client has consent
127
128
            if ($request->getMethod() == 'GET') {
129
                $data = $request->getQueryParams();
130
            } else if ($request->getMethod() == 'POST') {
131
                $data = $request->getParsedBody();
132
            } else {
133
                throw new OAuthException('invalid_request', 'Support only HTTP GET and POST methods');
134
            }
135
136
            $scopes = isset($data['scope']) ? explode(' ', trim($data['scope'])) : null;
137
            $client = $this->checkClient($data);
138
            $redirectUri = $this->checkRedirectUri($client, $data, $scopes);
139
        } catch (OAuthException $e) {
140
            return new \OAuth2\EndpointMessages\Token\ErrorResponse($e->getError(), $e->getErrorDescription(), $e->getErrorUri());
141
        }
142
143
        try {
144
            // Oauth2
145
            $responseTypes = $this->checkResponseTypes($request, $data);
146
            $this->checkAuthorizedClient($client, $responseTypes);
147
            $isSecure = $this->checkRedirectionEndpointConfidentiality($client, $responseTypes, $redirectUri);
148
149
            $scope = $this->checkScope($client, $data);
150
151
//            if (!isset($data['scope']) || !$data['scope']) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
68% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
152
//                throw new OAuthException('invalid_request', 'Missing a required parameter : scope');
153
//            }
154
155
156
            $isStateRequired = $this->server->getConfigurationRepository()->getConfig(Config::ENFORCE_STATE);
157
            if ($isStateRequired && (!isset($data['state']) || !$data['state'])) {
158
                throw new OAuthException('invalid_request', 'Missing a required parameter : state');
159
            }
160
161
            // OpenID
162
            if(!in_array('openid', $scope)) {
163
                return compact('client', 'redirectUri', 'responseTypes', 'data', 'scope', 'isSecure');
164
            }
165
166
            $responseMode = $this->checkResponseMode($responseTypes, $data);
167
168
            return compact('client', 'redirectUri', 'responseTypes', 'data', 'scope', 'isSecure',
169
                'responseMode');
170
171
        } catch (OAuthException $e) {
172
            return new ErrorResponse($redirectUri,
173
                $e->getError(), $e->getErrorDescription(), $e->getErrorUri(),
174
                $data['state'] ?? null);
175
        }
176
177
//        $requiredParameters = ['scope', 'response_type', 'client_id']
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
178
    }
179
180
    protected function checkAuthorizedClient(ClientInterface $client, array $responseTypes) {
181
        $isImplicit = false;
182
        foreach ($responseTypes as $responseType) {
183
            if ($responseType->isImplicit()) {
184
                $isImplicit = true;
185
            }
186
        }
187
188
        if ($isImplicit && !$client->isImplicitAllowed()) {
189
            throw new OAuthException('unauthorized_client',
190
                'Client is not allowed to use implicit grant',
191
                'https://tools.ietf.org/html/rfc6749#section-3.1.1');
192
        }
193
    }
194
195
    protected function checkScope(ClientInterface $client, array $data) : ?array {
196
        $scopePolicyManager = $this->server->getScopePolicyManager();
197
198
        if (isset($data['scope'])) {
199
            $scope = explode(' ', $data['scope']);
200
        } else {
201
            $scope = $scopePolicyManager->getDefaultScopes($client);
202
        }
203
204
        if (!$scopePolicyManager->checkScope($client, $scope)) {
205
            $supportedScopes = implode(', ', $scopePolicyManager->getSupportedScopes($client));
206
            throw new OAuthException('invalid_scope',
207
                'Some of requested scopes are not supported. Scope supported : ' . $supportedScopes,
208
                'https://tools.ietf.org/html/rfc6749#section-4.1');
209
        }
210
211
        return $scope;
212
    }
213
214
    protected function checkRedirectionEndpointConfidentiality(ClientInterface $client, array $responseTypes, Uri $redirectUri) : bool {
215
        $enforceTls = $this->server->getConfigurationRepository()->getConfig(Config::ENFORCE_TLS);
216
        if($redirectUri->getScheme() === 'https') {
217
            return true;
218
        }
219
220
        foreach ($responseTypes as $responseType) {
221
            if($responseType->requireTLS()) {
222
                if($enforceTls === true || (is_null($enforceTls) && $client->isTLSSupported())) {
223
                    throw new OAuthException('access_denied',
224
                        'Require the use of TLS for the redirect URI',
225
                        'https://tools.ietf.org/html/rfc6749#section-3.1.2.1');
226
                }
227
                return false;
228
            }
229
        }
230
        return true;
231
    }
232
233
    protected function checkClient(array $data): ClientInterface
234
    {
235
        if (!isset($data['client_id']) || !$data['client_id']) {
236
            throw new OAuthException('invalid_request', 'Missing a required parameter : client_id',
237
                'https://tools.ietf.org/html/rfc6749#section-4.1');
238
        }
239
240
        /**
241
         * @var ClientStorageInterface $clientStorage
242
         */
243
        $clientStorage = $this->server->getStorageRepository()->getStorage('client');
244
245
        $client = $clientStorage->get($data['client_id']);
246
        if (!$client) {
247
            throw new OAuthException('invalid_request', 'Invalid parameter : client_id',
248
                'https://tools.ietf.org/html/rfc6749#section-4.1');
249
        }
250
251
        if (!$client instanceof RegisteredClient) {
0 ignored issues
show
introduced by
The condition ! $client instanceof OAu...lients\RegisteredClient can never be true.
Loading history...
252
            throw new OAuthException('invalid_request', 'Client type is not supported',
253
                'https://tools.ietf.org/html/rfc6749#section-4.1');
254
        }
255
256
        return $client;
257
    }
258
259
    protected function checkRedirectUri(ClientInterface $client, array $data, ?array $scopes): Uri
260
    {
261
        if (!isset($data['redirect_uri']) || !$data['redirect_uri']) {
262
            if (is_array($scopes) && in_array('openid', $scopes)) {
263
                throw new OAuthException('invalid_request', 'Missing a required parameter : redirect_uri');
264
            }
265
266
            if ($this->server->getConfigurationRepository()->getConfig(Config::ENFORCE_REDIRECT_URI)) {
267
                throw new OAuthException('invalid_request', 'A redirect URI must be supplied');
268
            }
269
            if (empty($client->getRedirectUris())) {
270
                throw new OAuthException('invalid_request', 'No redirect URI was supplied or stored');
271
            }
272
            if (count($client->getRedirectUris()) > 1) {
273
                throw new OAuthException('invalid_request', 'A redirect URI must be supplied when multiple redirect URIs are registered');
274
            }
275
276
            $redirectUri = new Uri($client->getRedirectUris()[0]);
277
        } else {
278
279
            $redirectUri = new Uri($data['redirect_uri']);
280
            if ($redirectUri->getFragment()) {
281
                throw new OAuthException('invalid_request', 'The redirect URI must not contain a fragment');
282
            }
283
284
            if (empty($client->getRedirectUris())) {
285
                if ($client->requireRedirectUri()) {
286
                    throw new OAuthException('invalid_request', 'The client must register a redirect URI');
287
                } else {
288
                    return $redirectUri;
289
                }
290
            }
291
292
            $redirectUriWithoutQuery = Uri::composeComponents(
293
                $redirectUri->getScheme(), $redirectUri->getAuthority(), $redirectUri->getPath(), '', '');
294
295
            $match = false;
296
            foreach ($client->getRedirectUris() as $registeredUri) {
297
                $registeredUri = new Uri($registeredUri);
298
                if ($registeredUri->getQuery()) {
299
                    if ($registeredUri->__toString() === $redirectUri->__toString()) {
300
                        $match = true;
301
                        break;
302
                    }
303
                } else {
304
                    if ($this->server->getConfigurationRepository()->getConfig(Config::STRICT_REDIRECT_URI_COMPARISON)) {
305
                        if ($registeredUri->__toString() === $redirectUri->__toString()) {
306
                            $match = true;
307
                            break;
308
                        }
309
                    } else if ($registeredUri->__toString() === $redirectUriWithoutQuery) {
310
                        $match = true;
311
                        break;
312
                    }
313
                }
314
            }
315
316
            if (!$match) {
317
                throw new OAuthException('invalid_request', 'The redirect URI provided is missing or does not match');
318
            }
319
        }
320
321
        return $redirectUri;
322
    }
323
324
    /**
325
     * @param ServerRequestInterface $request
326
     * @param array $data
327
     * @return array
328
     * @throws OAuthException
329
     */
330
    protected function checkResponseTypes(ServerRequestInterface $request, array $data): array
331
    {
332
        if (!isset($data['response_type']) || !$data['response_type']) {
333
            throw new OAuthException('invalid_request', 'Missing a required parameter : response_type');
334
        }
335
336
        $responseTypeNames = explode(' ', $data['response_type']);
337
        $responseTypes = [];
338
339
        foreach ($responseTypeNames as $responseTypeName) {
340
            $responseType = $this->server->getResponseTypeRepository()->getResponseType($responseTypeName);
341
            if (!$responseType) {
342
                throw new OAuthException('invalid_request',
343
                    'Unknown response_type : ' . $responseTypeName);
344
            }
345
346
            if (!$responseType->isMultiValuedResponseTypeSupported() == ResponseTypeInterface::RESPONSE_MODE_FRAGMENT) {
347
                throw new OAuthException('invalid_request',
348
                    'Multi-valued response_type not supported with response_type : ' . $responseTypeName);
349
            }
350
351
            $responseTypes[] = $responseType;
352
        }
353
354
        if (empty($responseTypes)) {
355
            throw new OAuthException('invalid_request', 'Invalid response_type parameter');
356
        }
357
358
        foreach ($responseTypes as $responseType) {
359
            $responseType->verifyRequest($request);
360
        }
361
362
        return $responseTypes;
363
    }
364
365
    protected function checkResponseMode(array $responseTypes, array $data): string
366
    {
367
        $isResponseModeQueryAllowed = true;
368
        foreach ($responseTypes as $responseType) {
369
            if ($responseType->getDefaultResponseMode() == ResponseTypeInterface::RESPONSE_MODE_FRAGMENT) {
370
                $isResponseModeQueryAllowed = false;
371
            }
372
        }
373
374
        if (count($responseTypes) == 1) {
375
            $defaultResponseMode = $responseTypes[0]->getDefaultResponseMode();
376
        } else {
377
            $defaultResponseMode = ResponseTypeInterface::RESPONSE_MODE_FRAGMENT;
378
        }
379
380
        $responseMode = $data['response_mode'] ?? $defaultResponseMode;
381
        if ($responseMode === ResponseTypeInterface::RESPONSE_MODE_QUERY && !$isResponseModeQueryAllowed) {
382
            throw new OAuthException('invalid_request', 'Invalid response_mode parameter : query mode is not allowed');
383
        }
384
385
        $supportedResponsesModes = [
386
            ResponseTypeInterface::RESPONSE_MODE_QUERY,
387
            ResponseTypeInterface::RESPONSE_MODE_FRAGMENT
388
        ];
389
390
        if ($this->server->getConfigurationRepository()->getConfig(Config::SUPPORT_RESPONSE_TYPE_FORM_POST)) {
391
            $supportedResponsesModes[] = ResponseTypeInterface::RESPONSE_MODE_FORM_POST;
392
        }
393
394
        if (!in_array($responseMode, $supportedResponsesModes)) {
395
            throw new OAuthException('invalid_request',
396
                'Unsupported response_mode. Supported response_mode are : ' . implode(', ', $supportedResponsesModes));
397
        }
398
399
        if ($responseMode === ResponseTypeInterface::RESPONSE_MODE_QUERY && !$isResponseModeQueryAllowed) {
400
            throw new OAuthException('invalid_request', 'Invalid response_mode parameter : query mode is not allowed');
401
        }
402
403
        return $responseMode;
404
    }
405
406
    protected function popupResponse(array $data, Uri $redirectUri)
407
    {
408
        $message = json_encode($data);
409
        $targetOrigin = Uri::composeComponents(
410
            $redirectUri->getScheme(),
411
            $redirectUri->getAuthority(),
412
            $redirectUri->getPath(), '', '');
413
414
        $popup = <<<HTML
415
<!doctype html>
416
<html>
417
<head><title>Fermer cette fenêtre</title></head>
418
<body><pre>
419
Cette fenêtre devrait ce fermer automatiquement. 
420
Si ce n'est pas le cas cliquez sur ce bouton
421
</pre>
422
<button type="button" onclick="window.close();">Fermer cette fenêtre</button>
423
<script type="text/javascript">
424
  window.opener.postMessage({$message}, '{$targetOrigin}');
425
</script>
426
</html>
427
HTML;
428
        return new Response(200, [], $popup);
429
    }
430
}