Passed
Push — master ( 6f2986...60854d )
by Rutger
13:35
created

Oauth2AuthorizeAction::run()   F

Complexity

Conditions 47
Paths > 20000

Size

Total Lines 311
Code Lines 188

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 128
CRAP Score 47.8418

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 188
c 1
b 0
f 0
dl 0
loc 311
ccs 128
cts 138
cp 0.9275
rs 0
cc 47
nc 26631
nop 1
crap 47.8418

How to fix   Long Method    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
namespace rhertogh\Yii2Oauth2Server\controllers\web\server;
4
5
use rhertogh\Yii2Oauth2Server\controllers\web\Oauth2ServerController;
6
use rhertogh\Yii2Oauth2Server\controllers\web\server\base\Oauth2BaseServerAction;
7
use rhertogh\Yii2Oauth2Server\exceptions\Oauth2OidcServerException;
8
use rhertogh\Yii2Oauth2Server\exceptions\Oauth2ServerException;
9
use rhertogh\Yii2Oauth2Server\helpers\Psr7Helper;
10
use rhertogh\Yii2Oauth2Server\helpers\UrlHelper;
11
use rhertogh\Yii2Oauth2Server\interfaces\components\authorization\Oauth2ClientAuthorizationRequestInterface;
12
use rhertogh\Yii2Oauth2Server\interfaces\components\openidconnect\request\Oauth2OidcAuthenticationRequestInterface;
13
use rhertogh\Yii2Oauth2Server\interfaces\components\openidconnect\scope\Oauth2OidcScopeInterface;
14
use rhertogh\Yii2Oauth2Server\interfaces\components\openidconnect\user\Oauth2OidcUserComponentInterface;
15
use rhertogh\Yii2Oauth2Server\interfaces\models\external\user\Oauth2UserAuthenticatedAtInterface;
16
use rhertogh\Yii2Oauth2Server\interfaces\models\Oauth2ClientInterface;
17
use rhertogh\Yii2Oauth2Server\Oauth2Module;
18
use Yii;
19
use yii\base\InvalidConfigException;
20
use yii\web\BadRequestHttpException;
21
use yii\web\HttpException;
22
use yii\web\Request;
23
use yii\web\UnauthorizedHttpException;
24
25
/**
26
 * @property Oauth2ServerController $controller
27
 */
28
class Oauth2AuthorizeAction extends Oauth2BaseServerAction
29
{
30 39
    public function run($clientAuthorizationRequestId = null)
31
    {
32
        try {
33 39
            $request = Yii::$app->request;
0 ignored issues
show
Documentation Bug introduced by
It seems like Yii::app->request can also be of type yii\web\Request. However, the property $request is declared as type yii\console\Request. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
34 39
            $user = Yii::$app->user;
35 39
            $module = $this->controller->module;
36
37 39
            if ($module->enableOpenIdConnect) {
38 39
                if (!($user instanceof Oauth2OidcUserComponentInterface)) {
39 1
                    throw new InvalidConfigException(
40
                        'OpenId Connect is enabled but user component does not implement '
41
                        . Oauth2OidcUserComponentInterface::class
42
                    );
43
                }
44
45 38
                $oidcRequest = $this->getRequestParam(
46
                    $request,
0 ignored issues
show
Bug introduced by
It seems like $request can also be of type yii\console\Request; however, parameter $request of rhertogh\Yii2Oauth2Serve...tion::getRequestParam() does only seem to accept yii\web\Request, maybe add an additional type check? ( Ignorable by Annotation )

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

46
                    /** @scrutinizer ignore-type */ $request,
Loading history...
47
                    Oauth2OidcAuthenticationRequestInterface::REQUEST_PARAMETER_REQUEST
48
                );
49 38
                if ($oidcRequest !== null) {
50 1
                    throw Oauth2OidcServerException::requestParameterNotSupported();
51
                }
52
53 37
                $oidcRequestUri = $this->getRequestParam(
54
                    $request,
55
                    Oauth2OidcAuthenticationRequestInterface::REQUEST_PARAMETER_REQUEST_URI
56
                );
57 37
                if ($oidcRequestUri !== null) {
58 1
                    throw Oauth2OidcServerException::requestUriParameterNotSupported();
59
                }
60
            }
61
62 36
            $server = $module->getAuthorizationServer();
63 34
            $psr7Request = Psr7Helper::yiiToPsr7Request($request);
0 ignored issues
show
Bug introduced by
It seems like $request can also be of type yii\console\Request; however, parameter $request of rhertogh\Yii2Oauth2Serve...per::yiiToPsr7Request() does only seem to accept yii\web\Request, maybe add an additional type check? ( Ignorable by Annotation )

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

63
            $psr7Request = Psr7Helper::yiiToPsr7Request(/** @scrutinizer ignore-type */ $request);
Loading history...
64 34
            $authRequest = $server->validateAuthorizationRequest($psr7Request);
65
66 33
            $requestedScopeIdentifiers = array_map(fn($scope) => $scope->getIdentifier(), $authRequest->getScopes());
67
68
            /** @var Oauth2ClientInterface $client */
69 33
            $client = $authRequest->getClient();
70
71 33
            if (!$client->validateAuthRequestScopes($requestedScopeIdentifiers, $unauthorizedScopes)) {
72 1
                throw Oauth2ServerException::scopeNotAllowedForClient(
73 1
                    array_shift($unauthorizedScopes),
74 1
                    $authRequest->getRedirectUri()
75
                );
76
            }
77
78
            if (
79 32
                !$client->isAuthCodeWithoutPkceAllowed()
80
                // PKCE is not supported in the implicit flow.
81 32
                && $authRequest->getGrantTypeId() != Oauth2Module::GRANT_TYPE_IDENTIFIER_IMPLICIT
82
            ) {
83 31
                if (empty($request->get('code_challenge'))) {
84 2
                    throw new BadRequestHttpException(
85
                        'PKCE is required for this client when using grant type "'
86 2
                            . $authRequest->getGrantTypeId() . '".'
87
                    );
88 29
                } elseif ($request->get('code_challenge_method', 'plain') === 'plain') {
89 1
                    throw new BadRequestHttpException('PKCE code challenge mode "plain" is not allowed.');
90
                }
91
            }
92
93 29
            if ($clientAuthorizationRequestId) {
94 11
                $clientAuthorizationRequest = $module->getClientAuthReqSession($clientAuthorizationRequestId);
95
                if (
96 11
                    $clientAuthorizationRequest
97 11
                    && $clientAuthorizationRequest->getState()
98 10
                    && !Yii::$app->security->compareString(
99 10
                        $clientAuthorizationRequest->getState(),
100 11
                        $authRequest->getState()
101
                    )
102
                ) {
103 1
                    throw new UnauthorizedHttpException('Invalid state.');
104
                }
105
            }
106
107 28
            if (empty($clientAuthorizationRequest)) {
108 24
                $prompts = explode(
109
                    ' ',
110 24
                    (string)$this->getRequestParam(
111
                        $request,
112
                        Oauth2OidcAuthenticationRequestInterface::REQUEST_PARAMETER_PROMPT
113
                    )
114
                )
115 24
                    ?? [];
116
117
                if (
118 24
                    in_array(Oauth2OidcAuthenticationRequestInterface::REQUEST_PARAMETER_PROMPT_NONE, $prompts)
119 24
                    && (count($prompts) > 1)
120
                ) {
121 1
                    throw new BadRequestHttpException(
122
                        'When the "prompt" parameter contains "none" other values are not allowed.'
123
                    );
124
                }
125
126
                // Ignore `offline_access` scope if prompt doesn't contain 'consent' (or pre-approved via config).
127
                // See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess.
128
                if (
129 23
                    in_array(Oauth2OidcScopeInterface::OPENID_CONNECT_SCOPE_OFFLINE_ACCESS, $requestedScopeIdentifiers)
130 23
                    && !in_array(Oauth2OidcAuthenticationRequestInterface::REQUEST_PARAMETER_PROMPT_CONSENT, $prompts)
131 23
                    && !$client->getOpenIdConnectAllowOfflineAccessWithoutConsent()
132
                ) {
133 1
                    $requestedScopeIdentifiers = array_diff(
134
                        $requestedScopeIdentifiers,
135 1
                        [Oauth2OidcScopeInterface::OPENID_CONNECT_SCOPE_OFFLINE_ACCESS]
136
                    );
137
                }
138
139 23
                $maxAge = $this->getRequestParam(
140
                    $request,
141
                    Oauth2OidcAuthenticationRequestInterface::REQUEST_PARAMETER_MAX_AGE
142
                );
143 23
                if ($maxAge === '') {
144 1
                    $maxAge = null;
145 22
                } elseif ($maxAge !== null) {
146 4
                    $maxAge = (int)$maxAge;
147
                }
148
149
                /** @var Oauth2ClientAuthorizationRequestInterface $clientAuthorizationRequest */
150 23
                $clientAuthorizationRequest = Yii::createObject([
151
                    'class' => Oauth2ClientAuthorizationRequestInterface::class,
152
                    'module' => $module,
153 23
                    'userAuthenticatedBeforeRequest' => !$user->isGuest,
154 23
                    'clientIdentifier' => $authRequest->getClient()->getIdentifier(),
155 23
                    'state' => $authRequest->getState(),
156
                    'requestedScopeIdentifiers' => $requestedScopeIdentifiers,
157 23
                    'grantType' => $authRequest->getGrantTypeId(),
158 23
                    'authorizeUrl' => $request->absoluteUrl,
159 23
                    'redirectUri' => $authRequest->getRedirectUri(),
160
                    'prompts' => $prompts,
161
                    'maxAge' => $maxAge,
162
                ]);
163
            }
164
165 27
            if ($user->isGuest) {
166
                if (
167 4
                    in_array(
168
                        Oauth2OidcAuthenticationRequestInterface::REQUEST_PARAMETER_PROMPT_NONE,
169 4
                        $clientAuthorizationRequest->getPrompts()
170
                    )
171
                ) {
172
                    // User authentication disallowed by OpenID Connect.
173 1
                    throw Oauth2OidcServerException::loginRequired($authRequest->getRedirectUri());
174
                }
175
176 3
                $module->setClientAuthReqSession($clientAuthorizationRequest);
177 3
                $user->setReturnUrl($clientAuthorizationRequest->getAuthorizationRequestUrl());
178
//                $user->setReturnUrl(UrlHelper::addQueryParams($request->absoluteUrl, [
179
//                    'clientAuthorizationRequestId' => $clientAuthorizationRequest->getRequestId(),
180
//                ]));
181
182 3
                return Yii::$app->response->redirect($user->loginUrl);
183
            }
184
185
            // Check if reauthentication is required.
186 25
            $reauthenticationRequired = false;
187 25
            if (!$clientAuthorizationRequest->wasUserAthenticatedDuringRequest()) {
188
                if (
189
                    (// true in case user was authenticated before request and oidc prompt requires login.
190 25
                        in_array(
191
                            Oauth2OidcAuthenticationRequestInterface::REQUEST_PARAMETER_PROMPT_LOGIN,
192 25
                            $clientAuthorizationRequest->getPrompts()
193
                        )
194 25
                        && $clientAuthorizationRequest->wasUserAuthenticatedBeforeRequest()
195
                    )
196
                ) {
197 2
                    $reauthenticationRequired = true;
198
                }
199
200
                if (
201 25
                    !$reauthenticationRequired // Prevent unnecessary checking.
202 25
                    && $clientAuthorizationRequest->getMaxAge() !== null
203
                ) {
204 4
                    $appUserIdentity = $module->getUserIdentity();
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $appUserIdentity is correct as $module->getUserIdentity() targeting rhertogh\Yii2Oauth2Serve...dule::getUserIdentity() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
205 4
                    if (!($appUserIdentity instanceof Oauth2UserAuthenticatedAtInterface)) {
206
                        throw new InvalidConfigException(
207
                            'The authorization request max age is set, but ' . get_class($appUserIdentity)
0 ignored issues
show
Bug introduced by
$appUserIdentity of type null is incompatible with the type object expected by parameter $object of get_class(). ( Ignorable by Annotation )

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

207
                            'The authorization request max age is set, but ' . get_class(/** @scrutinizer ignore-type */ $appUserIdentity)
Loading history...
208
                            . ' does not implement ' . Oauth2UserAuthenticatedAtInterface::class
209
                        );
210
                    }
211 4
                    $latestAuthenticatedAt = $appUserIdentity->getLatestAuthenticatedAt();
212
                    if (
213 4
                        ($latestAuthenticatedAt === null)
214
                        || ( // if $latestAuthenticatedAt is not null, check if it's before the max time allowed.
215 4
                            (time() - $latestAuthenticatedAt->getTimestamp()) > $clientAuthorizationRequest->getMaxAge()
216
                        )
217
                    ) {
218 3
                        $reauthenticationRequired = true;
219
                    }
220
                }
221
            }
222
223 25
            if ($reauthenticationRequired) {
224
                // Prevent redirect loop.
225 4
                $redirectAttempt = (int)$request->get('redirectAttempt', 0);
226 4
                if ($redirectAttempt > 3) {
227
                    // This error most likely occurs if the User Controller does not correctly performs
228
                    // user reauthentication and redirect back to this action without calling
229
                    // `setUserAuthenticatedDuringRequest(true)` on the $clientAuthorizationRequest.
230
                    throw new HttpException(
231
                        501,
232
                        'Reauthentication not correctly implemented, aborting due to redirect loop.'
233
                    );
234
                }
235
236 4
                $module->setClientAuthReqSession($clientAuthorizationRequest);
237 4
                $user->setReturnUrl(
238 4
                    UrlHelper::addQueryParams($clientAuthorizationRequest->getAuthorizationRequestUrl(), [
239 4
                        'redirectAttempt' => $redirectAttempt + 1,
240
                    ])
241
                );
242 4
                return $user->reauthenticationRequired($clientAuthorizationRequest);
243
            }
244
245 22
            $userAccountSelection = $client->getUserAccountSelection() ?? $module->defaultUserAccountSelection;
246
247 22
            if (empty($clientAuthorizationRequest->getUserIdentity())) {
248
                if (
249 18
                    ($userAccountSelection === Oauth2Module::USER_ACCOUNT_SELECTION_ALWAYS)
250
                    || (
251 16
                        $userAccountSelection === Oauth2Module::USER_ACCOUNT_SELECTION_UPON_CLIENT_REQUEST
252 16
                        && in_array(
253
                            Oauth2OidcAuthenticationRequestInterface::REQUEST_PARAMETER_PROMPT_SELECT_ACCOUNT,
254 18
                            $clientAuthorizationRequest->getPrompts()
255
                        )
256
                    )
257
                ) {
258
                    if (
259 4
                        in_array(
260
                            Oauth2OidcAuthenticationRequestInterface::REQUEST_PARAMETER_PROMPT_NONE,
261 4
                            $clientAuthorizationRequest->getPrompts()
262
                        )
263
                    ) {
264 2
                        throw Oauth2OidcServerException::accountSelectionRequired(
265
                            'User account selection is required but the "prompt" parameter is set to "none".',
266 2
                            $authRequest->getRedirectUri()
267
                        );
268
                    }
269 2
                    $accountSelectionRequiredResponse = $user->accountSelectionRequired($clientAuthorizationRequest);
270 2
                    if ($accountSelectionRequiredResponse === false) {
271
                        throw Oauth2OidcServerException::accountSelectionRequired(
272
                            'User account selection is not supported by the server.',
273
                            $authRequest->getRedirectUri(),
274
                        );
275
                    }
276 2
                    $module->setClientAuthReqSession($clientAuthorizationRequest);
277 2
                    $user->setReturnUrl($clientAuthorizationRequest->getAuthorizationRequestUrl());
278 2
                    return $accountSelectionRequiredResponse;
279
                } else {
280 14
                    $clientAuthorizationRequest->setUserIdentity($module->getUserIdentity());
0 ignored issues
show
Bug introduced by
Are you sure the usage of $module->getUserIdentity() targeting rhertogh\Yii2Oauth2Serve...dule::getUserIdentity() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
281
                }
282
            }
283
284
            if (
285 19
                $clientAuthorizationRequest->getUserIdentity()->isOauth2ClientAllowed(
286
                    $client,
287 19
                    $clientAuthorizationRequest->getGrantType()
288
                ) !== true
289
            ) {
290
                throw Oauth2ServerException::accessDenied(
291
                    Yii::t('oauth2', 'User {user_id} is not allowed to use client {client_identifier}.', [
292
                        'user_id' => $user->getId(),
293
                        'client_identifier' => $client->getIdentifier(),
294
                    ])
295
                );
296
            }
297
298
            if (
299 19
                $clientAuthorizationRequest->isAuthorizationNeeded()
300 2
                || in_array(
301
                    Oauth2OidcAuthenticationRequestInterface::REQUEST_PARAMETER_PROMPT_CONSENT,
302 19
                    $clientAuthorizationRequest->getPrompts()
303
                )
304
            ) {
305 18
                if (!$clientAuthorizationRequest->isAuthorizationAllowed()) {
306
                    throw Oauth2ServerException::authorizationNotAllowed();
307
                }
308
309
                if (
310 18
                    in_array(
311
                        Oauth2OidcAuthenticationRequestInterface::REQUEST_PARAMETER_PROMPT_NONE,
312 18
                        $clientAuthorizationRequest->getPrompts()
313
                    )
314
                ) {
315
                    // User consent is disallowed by OpenID Connect.
316 1
                    throw Oauth2OidcServerException::consentRequired($authRequest->getRedirectUri());
317
                }
318
319 17
                if ($clientAuthorizationRequest->isCompleted()) {
320 6
                    $authorizationApproved = $clientAuthorizationRequest->isApproved();
321
                    // Cleanup session data.
322 6
                    $module->removeClientAuthReqSession($clientAuthorizationRequest->getRequestId());
323
                } else {
324 17
                    return $module->generateClientAuthReqRedirectResponse($clientAuthorizationRequest);
325
                }
326
            } else {
327
                // All scopes are already approved (or are default).
328 2
                $authorizationApproved = true;
329
            }
330
331 8
            $authRequest->setUser($clientAuthorizationRequest->getUserIdentity());
332 8
            $authRequest->setAuthorizationApproved($authorizationApproved);
333
334 8
            $psr7Response = Psr7Helper::yiiToPsr7Response(Yii::$app->response);
0 ignored issues
show
Bug introduced by
It seems like Yii::app->response can also be of type yii\console\Response; however, parameter $response of rhertogh\Yii2Oauth2Serve...er::yiiToPsr7Response() does only seem to accept yii\web\Response, maybe add an additional type check? ( Ignorable by Annotation )

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

334
            $psr7Response = Psr7Helper::yiiToPsr7Response(/** @scrutinizer ignore-type */ Yii::$app->response);
Loading history...
335 8
            $psr7Response = $server->completeAuthorizationRequest($authRequest, $psr7Response);
336
337 7
            return Psr7Helper::psr7ToYiiResponse($psr7Response);
338 17
        } catch (\Exception $e) {
339 17
            Yii::error((string)$e, __METHOD__);
340 17
            return $this->processException($e);
341
        }
342
    }
343
344
    /**
345
     * @param Request $request
346
     * @param string $name
347
     * @return mixed
348
     */
349 38
    protected function getRequestParam($request, $name, $defaultValue = null)
350
    {
351 38
        return $request->post($name) ?? $request->get($name, $defaultValue);
352
    }
353
}
354