Passed
Push — master ( 63d167...fb60c6 )
by Rutger
03:11
created

Oauth2AuthorizeAction::run()   F

Complexity

Conditions 46
Paths > 20000

Size

Total Lines 305
Code Lines 184

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 172
CRAP Score 48.0844

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 184
c 2
b 0
f 0
dl 0
loc 305
ccs 172
cts 191
cp 0.9005
rs 0
cc 46
nc 26629
nop 1
crap 48.0844

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\client\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\controllers\web\server\Oauth2AuthorizeActionInterface;
16
use rhertogh\Yii2Oauth2Server\interfaces\models\external\user\Oauth2UserAuthenticatedAtInterface;
17
use rhertogh\Yii2Oauth2Server\interfaces\models\Oauth2ClientInterface;
18
use rhertogh\Yii2Oauth2Server\Oauth2Module;
19
use Yii;
20
use yii\base\InvalidConfigException;
21
use yii\web\BadRequestHttpException;
22
use yii\web\HttpException;
23
use yii\web\UnauthorizedHttpException;
24
25
/**
26
 * @property Oauth2ServerController $controller
27
 */
28
class Oauth2AuthorizeAction extends Oauth2BaseServerAction implements Oauth2AuthorizeActionInterface
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 1
                        'OpenId Connect is enabled but user component does not implement '
41 1
                        . Oauth2OidcUserComponentInterface::class
42 1
                    );
43
                }
44
45 38
                $oidcRequest = $this->getRequestParam(
46 38
                    $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 38
                    Oauth2OidcAuthenticationRequestInterface::REQUEST_PARAMETER_REQUEST
48 38
                );
49 38
                if ($oidcRequest !== null) {
50 1
                    throw Oauth2OidcServerException::requestParameterNotSupported();
51
                }
52
53 37
                $oidcRequestUri = $this->getRequestParam(
54 37
                    $request,
55 37
                    Oauth2OidcAuthenticationRequestInterface::REQUEST_PARAMETER_REQUEST_URI
56 37
                );
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
            $module->validateAuthRequestScopes($client, $requestedScopeIdentifiers, $authRequest->getRedirectUri());
72
73
            if (
74 32
                !$client->isAuthCodeWithoutPkceAllowed()
75
                // PKCE is not supported in the implicit flow.
76 32
                && $authRequest->getGrantTypeId() != Oauth2Module::GRANT_TYPE_IDENTIFIER_IMPLICIT
77
            ) {
78 31
                if (empty($request->get('code_challenge'))) {
79 2
                    throw new BadRequestHttpException(
80 2
                        'PKCE is required for this client when using grant type "'
81 2
                            . $authRequest->getGrantTypeId() . '".'
82 2
                    );
83 29
                } elseif ($request->get('code_challenge_method', 'plain') === 'plain') {
84 1
                    throw new BadRequestHttpException('PKCE code challenge mode "plain" is not allowed.');
85
                }
86
            }
87
88 29
            if ($clientAuthorizationRequestId) {
89 11
                $clientAuthorizationRequest = $module->getClientAuthReqSession($clientAuthorizationRequestId);
90
                if (
91 11
                    $clientAuthorizationRequest
92 11
                    && $clientAuthorizationRequest->getState()
93 11
                    && !Yii::$app->security->compareString(
94 11
                        $clientAuthorizationRequest->getState(),
95 11
                        $authRequest->getState()
96 11
                    )
97
                ) {
98 1
                    throw new UnauthorizedHttpException('Invalid state.');
99
                }
100
            }
101
102 28
            if (empty($clientAuthorizationRequest)) {
103 24
                $prompts = explode(
104 24
                    ' ',
105 24
                    (string)$this->getRequestParam(
106 24
                        $request,
107 24
                        Oauth2OidcAuthenticationRequestInterface::REQUEST_PARAMETER_PROMPT
108 24
                    )
109 24
                )
110 24
                    ?? [];
111
112
                if (
113 24
                    in_array(Oauth2OidcAuthenticationRequestInterface::REQUEST_PARAMETER_PROMPT_NONE, $prompts)
114 24
                    && (count($prompts) > 1)
115
                ) {
116 1
                    throw new BadRequestHttpException(
117 1
                        'When the "prompt" parameter contains "none" other values are not allowed.'
118 1
                    );
119
                }
120
121
                // Ignore `offline_access` scope if prompt doesn't contain 'consent' (or pre-approved via config).
122
                // See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess.
123
                if (
124 23
                    in_array(Oauth2OidcScopeInterface::OPENID_CONNECT_SCOPE_OFFLINE_ACCESS, $requestedScopeIdentifiers)
125 23
                    && !in_array(Oauth2OidcAuthenticationRequestInterface::REQUEST_PARAMETER_PROMPT_CONSENT, $prompts)
126 23
                    && !$client->getOpenIdConnectAllowOfflineAccessWithoutConsent()
127
                ) {
128 1
                    $requestedScopeIdentifiers = array_diff(
129 1
                        $requestedScopeIdentifiers,
130 1
                        [Oauth2OidcScopeInterface::OPENID_CONNECT_SCOPE_OFFLINE_ACCESS]
131 1
                    );
132
                }
133
134 23
                $maxAge = $this->getRequestParam(
135 23
                    $request,
136 23
                    Oauth2OidcAuthenticationRequestInterface::REQUEST_PARAMETER_MAX_AGE
137 23
                );
138 23
                if ($maxAge === '') {
139 1
                    $maxAge = null;
140 22
                } elseif ($maxAge !== null) {
141 4
                    $maxAge = (int)$maxAge;
142
                }
143
144
                /** @var Oauth2ClientAuthorizationRequestInterface $clientAuthorizationRequest */
145 23
                $clientAuthorizationRequest = Yii::createObject([
146 23
                    'class' => Oauth2ClientAuthorizationRequestInterface::class,
147 23
                    'module' => $module,
148 23
                    'userAuthenticatedBeforeRequest' => !$user->isGuest,
149 23
                    'clientIdentifier' => $authRequest->getClient()->getIdentifier(),
150 23
                    'state' => $authRequest->getState(),
151 23
                    'requestedScopeIdentifiers' => $requestedScopeIdentifiers,
152 23
                    'grantType' => $authRequest->getGrantTypeId(),
153 23
                    'authorizeUrl' => $request->absoluteUrl,
154 23
                    'redirectUri' => $authRequest->getRedirectUri(),
155 23
                    'prompts' => $prompts,
156 23
                    'maxAge' => $maxAge,
157 23
                ]);
158
            }
159
160 27
            if ($user->isGuest) {
161
                if (
162 4
                    in_array(
163 4
                        Oauth2OidcAuthenticationRequestInterface::REQUEST_PARAMETER_PROMPT_NONE,
164 4
                        $clientAuthorizationRequest->getPrompts()
165 4
                    )
166
                ) {
167
                    // User authentication disallowed by OpenID Connect.
168 1
                    throw Oauth2OidcServerException::loginRequired($authRequest->getRedirectUri());
169
                }
170
171 3
                $module->setClientAuthReqSession($clientAuthorizationRequest);
172 3
                $user->setReturnUrl($clientAuthorizationRequest->getAuthorizationRequestUrl());
173
//                $user->setReturnUrl(UrlHelper::addQueryParams($request->absoluteUrl, [
174
//                    'clientAuthorizationRequestId' => $clientAuthorizationRequest->getRequestId(),
175
//                ]));
176
177 3
                return Yii::$app->response->redirect($user->loginUrl);
178
            }
179
180
            // Check if reauthentication is required.
181 25
            $reauthenticationRequired = false;
182 25
            if (!$clientAuthorizationRequest->wasUserAthenticatedDuringRequest()) {
183
                if (
184
                    (// true in case user was authenticated before request and oidc prompt requires login.
185 25
                        in_array(
186 25
                            Oauth2OidcAuthenticationRequestInterface::REQUEST_PARAMETER_PROMPT_LOGIN,
187 25
                            $clientAuthorizationRequest->getPrompts()
188 25
                        )
189 25
                        && $clientAuthorizationRequest->wasUserAuthenticatedBeforeRequest()
190
                    )
191
                ) {
192 2
                    $reauthenticationRequired = true;
193
                }
194
195
                if (
196 25
                    !$reauthenticationRequired // Prevent unnecessary checking.
197 25
                    && $clientAuthorizationRequest->getMaxAge() !== null
198
                ) {
199 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...
200 4
                    if (!($appUserIdentity instanceof Oauth2UserAuthenticatedAtInterface)) {
201
                        throw new InvalidConfigException(
202
                            '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

202
                            'The authorization request max age is set, but ' . get_class(/** @scrutinizer ignore-type */ $appUserIdentity)
Loading history...
203
                            . ' does not implement ' . Oauth2UserAuthenticatedAtInterface::class
204
                        );
205
                    }
206 4
                    $latestAuthenticatedAt = $appUserIdentity->getLatestAuthenticatedAt();
207
                    if (
208 4
                        ($latestAuthenticatedAt === null)
209
                        || ( // if $latestAuthenticatedAt is not null, check if it's before the max time allowed.
210 4
                            (time() - $latestAuthenticatedAt->getTimestamp()) > $clientAuthorizationRequest->getMaxAge()
211
                        )
212
                    ) {
213 3
                        $reauthenticationRequired = true;
214
                    }
215
                }
216
            }
217
218 25
            if ($reauthenticationRequired) {
219
                // Prevent redirect loop.
220 4
                $redirectAttempt = (int)$request->get('redirectAttempt', 0);
221 4
                if ($redirectAttempt > 3) {
222
                    // This error most likely occurs if the User Controller does not correctly performs
223
                    // user reauthentication and redirect back to this action without calling
224
                    // `setUserAuthenticatedDuringRequest(true)` on the $clientAuthorizationRequest.
225
                    throw new HttpException(
226
                        501,
227
                        'Reauthentication not correctly implemented, aborting due to redirect loop.'
228
                    );
229
                }
230
231 4
                $module->setClientAuthReqSession($clientAuthorizationRequest);
232 4
                $user->setReturnUrl(
233 4
                    UrlHelper::addQueryParams($clientAuthorizationRequest->getAuthorizationRequestUrl(), [
234 4
                        'redirectAttempt' => $redirectAttempt + 1,
235 4
                    ])
236 4
                );
237 4
                return $user->reauthenticationRequired($clientAuthorizationRequest);
238
            }
239
240 22
            $userAccountSelection = $client->getUserAccountSelection() ?? $module->defaultUserAccountSelection;
241
242 22
            if (empty($clientAuthorizationRequest->getUserIdentity())) {
243
                if (
244 18
                    ($userAccountSelection === Oauth2Module::USER_ACCOUNT_SELECTION_ALWAYS)
245
                    || (
246 18
                        $userAccountSelection === Oauth2Module::USER_ACCOUNT_SELECTION_UPON_CLIENT_REQUEST
247 18
                        && in_array(
248 18
                            Oauth2OidcAuthenticationRequestInterface::REQUEST_PARAMETER_PROMPT_SELECT_ACCOUNT,
249 18
                            $clientAuthorizationRequest->getPrompts()
250 18
                        )
251
                    )
252
                ) {
253
                    if (
254 4
                        in_array(
255 4
                            Oauth2OidcAuthenticationRequestInterface::REQUEST_PARAMETER_PROMPT_NONE,
256 4
                            $clientAuthorizationRequest->getPrompts()
257 4
                        )
258
                    ) {
259 2
                        throw Oauth2OidcServerException::accountSelectionRequired(
260 2
                            'User account selection is required but the "prompt" parameter is set to "none".',
261 2
                            $authRequest->getRedirectUri()
262 2
                        );
263
                    }
264 2
                    $accountSelectionRequiredResponse = $user->accountSelectionRequired($clientAuthorizationRequest);
265 2
                    if ($accountSelectionRequiredResponse === false) {
266
                        throw Oauth2OidcServerException::accountSelectionRequired(
267
                            'User account selection is not supported by the server.',
268
                            $authRequest->getRedirectUri(),
269
                        );
270
                    }
271 2
                    $module->setClientAuthReqSession($clientAuthorizationRequest);
272 2
                    $user->setReturnUrl($clientAuthorizationRequest->getAuthorizationRequestUrl());
273 2
                    return $accountSelectionRequiredResponse;
274
                } else {
275 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...
276
                }
277
            }
278
279
            if (
280 19
                $clientAuthorizationRequest->getUserIdentity()->isOauth2ClientAllowed(
281 19
                    $client,
282 19
                    $clientAuthorizationRequest->getGrantType()
283 19
                ) !== true
284
            ) {
285
                throw Oauth2ServerException::accessDenied(
286
                    Yii::t('oauth2', 'User {user_id} is not allowed to use client {client_identifier}.', [
287
                        'user_id' => $user->getId(),
288
                        'client_identifier' => $client->getIdentifier(),
289
                    ])
290
                );
291
            }
292
293
            if (
294 19
                $clientAuthorizationRequest->isAuthorizationNeeded()
295 19
                || in_array(
296 19
                    Oauth2OidcAuthenticationRequestInterface::REQUEST_PARAMETER_PROMPT_CONSENT,
297 19
                    $clientAuthorizationRequest->getPrompts()
298 19
                )
299
            ) {
300 18
                if (!$clientAuthorizationRequest->isAuthorizationAllowed()) {
301
                    throw Oauth2ServerException::authorizationNotAllowed();
302
                }
303
304
                if (
305 18
                    in_array(
306 18
                        Oauth2OidcAuthenticationRequestInterface::REQUEST_PARAMETER_PROMPT_NONE,
307 18
                        $clientAuthorizationRequest->getPrompts()
308 18
                    )
309
                ) {
310
                    // User consent is disallowed by OpenID Connect.
311 1
                    throw Oauth2OidcServerException::consentRequired($authRequest->getRedirectUri());
312
                }
313
314 17
                if ($clientAuthorizationRequest->isCompleted()) {
315 6
                    $authorizationApproved = $clientAuthorizationRequest->isApproved();
316
                    // Cleanup session data.
317 6
                    $module->removeClientAuthReqSession($clientAuthorizationRequest->getRequestId());
318
                } else {
319 17
                    return $module->generateClientAuthReqRedirectResponse($clientAuthorizationRequest);
320
                }
321
            } else {
322
                // All scopes are already approved (or are default).
323 2
                $authorizationApproved = true;
324
            }
325
326 8
            $authRequest->setUser($clientAuthorizationRequest->getUserIdentity());
327 8
            $authRequest->setAuthorizationApproved($authorizationApproved);
328
329 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

329
            $psr7Response = Psr7Helper::yiiToPsr7Response(/** @scrutinizer ignore-type */ Yii::$app->response);
Loading history...
330 8
            $psr7Response = $server->completeAuthorizationRequest($authRequest, $psr7Response);
331
332 7
            return Psr7Helper::psr7ToYiiResponse($psr7Response);
333 17
        } catch (\Exception $e) {
334 17
            return $this->processException($e, __METHOD__);
335
        }
336
    }
337
}
338