Passed
Push — master ( 6e82fd...74b096 )
by Rutger
03:04
created

Oauth2Module   F

Complexity

Total Complexity 121

Size/Duplication

Total Lines 1260
Duplicated Lines 0 %

Test Coverage

Coverage 87.53%

Importance

Changes 7
Bugs 0 Features 0
Metric Value
wmc 121
eloc 442
c 7
b 0
f 0
dl 0
loc 1260
ccs 344
cts 393
cp 0.8753
rs 2

29 Methods

Rating   Name   Duplication   Size   Complexity  
A rotateStorageEncryptionKeys() 0 15 3
A getCryptographer() 0 11 2
A removeClientAuthReqSession() 0 7 2
A validateAuthRequestScopes() 0 17 6
A validateTlsConnection() 0 14 4
A ensureProperties() 0 5 3
B bootstrap() 0 54 11
A getRequestOauthClaim() 0 12 3
A getElaboratedHttpClientErrorsLogLevel() 0 7 3
D getAuthorizationServer() 0 101 18
A validateAuthenticatedRequest() 0 8 1
A generateClientAuthReqRedirectResponse() 0 13 2
A getPrivateKey() 0 7 2
B generatePersonalAccessToken() 0 43 8
A getUserIdentity() 0 10 3
A registerTranslations() 0 9 3
A getResourceServer() 0 19 3
A createClient() 0 47 4
A getClientAuthReqSession() 0 26 5
A logoutUser() 0 3 1
B init() 0 40 10
A getPublicKey() 0 7 2
A setUserAuthenticatedDuringClientAuthRequest() 0 8 2
A setClientAuthRequestUserIdentity() 0 6 2
B getOidcScopeCollection() 0 28 7
A generateClientAuthReqCompledRedirectResponse() 0 5 1
A getStorageEncryptionKeyUsage() 0 16 3
A findIdentityByAccessToken() 0 21 5
A setClientAuthReqSession() 0 8 2

How to fix   Complexity   

Complex Class

Complex classes like Oauth2Module often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Oauth2Module, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * @link http://www.yiiframework.com/
5
 * @copyright Copyright (c) 2008 Yii Software LLC
6
 * @license http://www.yiiframework.com/license/
7
 */
8
9
namespace rhertogh\Yii2Oauth2Server;
10
11
use Defuse\Crypto\Exception\BadFormatException;
12
use Defuse\Crypto\Exception\EnvironmentIsBrokenException;
13
use GuzzleHttp\Psr7\Response as Psr7Response;
14
use GuzzleHttp\Psr7\ServerRequest as Psr7ServerRequest;
15
use League\OAuth2\Server\CryptKey;
16
use League\OAuth2\Server\Grant\GrantTypeInterface;
17
use rhertogh\Yii2Oauth2Server\base\Oauth2BaseModule;
18
use rhertogh\Yii2Oauth2Server\components\server\tokens\Oauth2AccessTokenData;
19
use rhertogh\Yii2Oauth2Server\controllers\console\Oauth2ClientController;
20
use rhertogh\Yii2Oauth2Server\controllers\console\Oauth2DebugController;
21
use rhertogh\Yii2Oauth2Server\controllers\console\Oauth2EncryptionController;
22
use rhertogh\Yii2Oauth2Server\controllers\console\Oauth2MigrationsController;
23
use rhertogh\Yii2Oauth2Server\controllers\console\Oauth2PersonalAccessTokenController;
24
use rhertogh\Yii2Oauth2Server\exceptions\Oauth2ServerException;
25
use rhertogh\Yii2Oauth2Server\helpers\DiHelper;
26
use rhertogh\Yii2Oauth2Server\helpers\Psr7Helper;
27
use rhertogh\Yii2Oauth2Server\interfaces\components\authorization\Oauth2ClientAuthorizationRequestInterface;
28
use rhertogh\Yii2Oauth2Server\interfaces\components\common\DefaultAccessTokenTtlInterface;
29
use rhertogh\Yii2Oauth2Server\interfaces\components\encryption\Oauth2CryptographerInterface;
30
use rhertogh\Yii2Oauth2Server\interfaces\components\factories\encryption\Oauth2EncryptionKeyFactoryInterface;
31
use rhertogh\Yii2Oauth2Server\interfaces\components\factories\grants\base\Oauth2GrantTypeFactoryInterface;
32
use rhertogh\Yii2Oauth2Server\interfaces\components\openidconnect\scope\Oauth2OidcScopeCollectionInterface;
33
use rhertogh\Yii2Oauth2Server\interfaces\components\openidconnect\server\Oauth2OidcBearerTokenResponseInterface;
34
use rhertogh\Yii2Oauth2Server\interfaces\components\server\Oauth2AuthorizationServerInterface;
35
use rhertogh\Yii2Oauth2Server\interfaces\components\server\Oauth2ResourceServerInterface;
36
use rhertogh\Yii2Oauth2Server\interfaces\controllers\web\Oauth2CertificatesControllerInterface;
37
use rhertogh\Yii2Oauth2Server\interfaces\controllers\web\Oauth2ConsentControllerInterface;
38
use rhertogh\Yii2Oauth2Server\interfaces\controllers\web\Oauth2OidcControllerInterface;
39
use rhertogh\Yii2Oauth2Server\interfaces\controllers\web\Oauth2ServerControllerInterface;
40
use rhertogh\Yii2Oauth2Server\interfaces\controllers\web\Oauth2WellKnownControllerInterface;
41
use rhertogh\Yii2Oauth2Server\interfaces\filters\auth\Oauth2HttpBearerAuthInterface;
42
use rhertogh\Yii2Oauth2Server\interfaces\models\base\Oauth2EncryptedStorageInterface;
43
use rhertogh\Yii2Oauth2Server\interfaces\models\external\user\Oauth2OidcUserInterface;
44
use rhertogh\Yii2Oauth2Server\interfaces\models\external\user\Oauth2UserInterface;
45
use rhertogh\Yii2Oauth2Server\interfaces\models\Oauth2ClientInterface;
46
use rhertogh\Yii2Oauth2Server\interfaces\models\Oauth2ScopeInterface;
47
use rhertogh\Yii2Oauth2Server\traits\DefaultAccessTokenTtlTrait;
48
use Yii;
49
use yii\base\BootstrapInterface;
50
use yii\base\InvalidArgumentException;
51
use yii\base\InvalidCallException;
52
use yii\base\InvalidConfigException;
53
use yii\console\Application as ConsoleApplication;
54
use yii\helpers\ArrayHelper;
55
use yii\helpers\Json;
56
use yii\helpers\StringHelper;
57
use yii\helpers\VarDumper;
58
use yii\i18n\PhpMessageSource;
59
use yii\log\Logger;
60
use yii\validators\IpValidator;
61
use yii\web\Application as WebApplication;
62
use yii\web\GroupUrlRule;
63
use yii\web\IdentityInterface;
64
use yii\web\Response;
65
use yii\web\UrlRule;
66
67
/**
68
 * This is the main module class for the Yii2 Oauth2 Server module.
69
 * To use it, include it as a module in the application configuration like the following:
70
 *
71
 * ~~~
72
 * return [
73
 *     'bootstrap' => ['oauth2'],
74
 *     'modules' => [
75
 *         'oauth2' => [
76
 *             'class' => 'rhertogh\Yii2Oauth2Server\Oauth2Module',
77
 *             // ... Please check docs/guide/start-installation.md further details
78
 *          ],
79
 *     ],
80
 * ]
81
 * ~~~
82
 *
83
 * @property \DateInterval|string|null $defaultAccessTokenTTL
84
 * @since 1.0.0
85
 */
86
class Oauth2Module extends Oauth2BaseModule implements BootstrapInterface, DefaultAccessTokenTtlInterface
87
{
88
    use DefaultAccessTokenTtlTrait;
89
90
    /**
91
     * Application type "web": http response.
92
     * @since 1.0.0
93
     */
94
    public const APPLICATION_TYPE_WEB = 'web';
95
    /**
96
     * Application type "console": cli response.
97
     * @since 1.0.0
98
     */
99
    public const APPLICATION_TYPE_CONSOLE = 'console';
100
    /**
101
     * Supported Application types.
102
     * @since 1.0.0
103
     */
104
    public const APPLICATION_TYPES = [
105
        self::APPLICATION_TYPE_WEB,
106
        self::APPLICATION_TYPE_CONSOLE,
107
    ];
108
109
    /**
110
     * "Authorization Server" Role, please see guide for details.
111
     * @since 1.0.0
112
     */
113
    public const SERVER_ROLE_AUTHORIZATION_SERVER = 1;
114
    /**
115
     * "Resource Server" Role, please see guide for details.
116
     * @since 1.0.0
117
     */
118
    public const SERVER_ROLE_RESOURCE_SERVER = 2;
119
120
    /**
121
     * Required settings when the server role includes Authorization Server
122
     * @since 1.0.0
123
     */
124
    protected const REQUIRED_SETTINGS_AUTHORIZATION_SERVER = [
125
        'codesEncryptionKey',
126
        'storageEncryptionKeys',
127
        'defaultStorageEncryptionKey',
128
        'privateKey',
129
        'publicKey',
130
    ];
131
132
    /**
133
     * Encrypted Models
134
     *
135
     * @since 1.0.0
136
     */
137
    protected const ENCRYPTED_MODELS = [
138
        Oauth2ClientInterface::class,
139
    ];
140
141
    /**
142
     * Required settings when the server role includes Resource Server
143
     * @since 1.0.0
144
     */
145
    protected const REQUIRED_SETTINGS_RESOURCE_SERVER = [
146
        'publicKey',
147
    ];
148
149
    /**
150
     * Prefix used in session storage of Client Authorization Requests
151
     * @since 1.0.0
152
     */
153
    protected const CLIENT_AUTHORIZATION_REQUEST_SESSION_PREFIX = 'OATH2_CLIENT_AUTHORIZATION_REQUEST_';
154
155
    /**
156
     * Controller mapping for the module. Will be parsed on `init()`.
157
     * @since 1.0.0
158
     */
159
    protected const CONTROLLER_MAP = [
160
        self::APPLICATION_TYPE_WEB => [
161
            Oauth2ServerControllerInterface::CONTROLLER_NAME => [
162
                'controller' => Oauth2ServerControllerInterface::class,
163
                'serverRole' => self::SERVER_ROLE_AUTHORIZATION_SERVER,
164
            ],
165
            Oauth2ConsentControllerInterface::CONTROLLER_NAME => [
166
                'controller' => Oauth2ConsentControllerInterface::class,
167
                'serverRole' => self::SERVER_ROLE_AUTHORIZATION_SERVER,
168
            ],
169
            Oauth2WellKnownControllerInterface::CONTROLLER_NAME => [
170
                'controller' => Oauth2WellKnownControllerInterface::class,
171
                'serverRole' => self::SERVER_ROLE_AUTHORIZATION_SERVER,
172
            ],
173
            Oauth2CertificatesControllerInterface::CONTROLLER_NAME => [
174
                'controller' => Oauth2CertificatesControllerInterface::class,
175
                'serverRole' => self::SERVER_ROLE_AUTHORIZATION_SERVER,
176
            ],
177
            Oauth2OidcControllerInterface::CONTROLLER_NAME => [
178
                'controller' => Oauth2OidcControllerInterface::class,
179
                'serverRole' => self::SERVER_ROLE_AUTHORIZATION_SERVER,
180
            ],
181
        ],
182
        self::APPLICATION_TYPE_CONSOLE => [
183
            'migrations' => [
184
                'controller' => Oauth2MigrationsController::class,
185
                'serverRole' => self::SERVER_ROLE_AUTHORIZATION_SERVER | self::SERVER_ROLE_RESOURCE_SERVER,
186
            ],
187
            'client' => [
188
                'controller' => Oauth2ClientController::class,
189
                'serverRole' => self::SERVER_ROLE_AUTHORIZATION_SERVER,
190
            ],
191
            'encryption' => [
192
                'controller' => Oauth2EncryptionController::class,
193
                'serverRole' => self::SERVER_ROLE_AUTHORIZATION_SERVER,
194
            ],
195
            'debug' => [
196
                'controller' => Oauth2DebugController::class,
197
                'serverRole' => self::SERVER_ROLE_AUTHORIZATION_SERVER | self::SERVER_ROLE_RESOURCE_SERVER,
198
            ],
199
            'pat' => [
200
                'controller' => Oauth2PersonalAccessTokenController::class,
201
                'serverRole' => self::SERVER_ROLE_AUTHORIZATION_SERVER,
202
            ]
203
        ]
204
    ];
205
206
    /**
207
     * @inheritdoc
208
     */
209
    public $controllerNamespace = __NAMESPACE__ . '\-'; // Set explicitly via $controllerMap in `init()`.
210
211
    /**
212
     * @var string|null The application type. If `null` the type will be automatically detected.
213
     * @see APPLICATION_TYPES
214
     */
215
    public $appType = null;
216
217
    /**
218
     * @var int The Oauth 2.0 Server Roles the module will perform.
219
     * @since 1.0.0
220
     */
221
    public $serverRole = self::SERVER_ROLE_AUTHORIZATION_SERVER | self::SERVER_ROLE_RESOURCE_SERVER;
222
223
    /**
224
     * @var string|null The private key for the server. Can be a string containing the key itself or point to a file.
225
     * When pointing to a file it's recommended to use an absolute path prefixed with 'file://' or start with
226
     * '@' to use a Yii path alias.
227
     * @see $privateKeyPassphrase For setting a passphrase for the private key.
228
     * @since 1.0.0
229
     */
230
    public $privateKey = null;
231
232
    /**
233
     * @var string|null The passphrase for the private key.
234
     * @since 1.0.0
235
     */
236
    public $privateKeyPassphrase = null;
237
    /**
238
     * @var string|null The public key for the server. Can be a string containing the key itself or point to a file.
239
     * When pointing to a file it's recommended to use an absolute path prefixed with 'file://' or start with
240
     * '@' to use a Yii path alias.
241
     * @since 1.0.0
242
     */
243
    public $publicKey = null;
244
245
    /**
246
     * @var string|null The encryption key for authorization and refresh codes.
247
     * @since 1.0.0
248
     */
249
    public $codesEncryptionKey = null;
250
251
    /**
252
     * @var string[]|string|null The encryption keys for storage like client secrets.
253
     * Where the array key is the name of the key, and the value the key itself. E.g.
254
     * `['2022-01-01' => 'def00000cb36fd6ed6641e0ad70805b28d....']`
255
     * If a string (instead of an array of strings) is specified it will be JSON decoded
256
     * it should contain an object where each property name is the name of the key, its value the key itself. E.g.
257
     * `{"2022-01-01": "def00000cb36fd6ed6641e0ad70805b28d...."}`
258
     *
259
     * @since 1.0.0
260
     */
261
    public $storageEncryptionKeys = null;
262
263
    /**
264
     * @var string|null The index of the default key in storageEncryptionKeys. E.g. 'myKey'.
265
     * @since 1.0.0
266
     */
267
    public $defaultStorageEncryptionKey = null;
268
269
    /**
270
     * @var string|string[]|null IP addresses, CIDR ranges, or range aliases that are allowed to connect over a
271
     * non-TLS connection. If `null` or an empty array LTS is always required.
272
     *
273
     * Warning: Although you can use '*' or 'any' to allow a non-TLS connection from any ip address,
274
     * doing so would most likely introduce a security risk and should be done for debugging purposes only!
275
     *
276
     * @see \yii\validators\IpValidator::$networks for a list of available alliasses.
277
     */
278
    public $nonTlsAllowedRanges = 'localhost';
279
280
    /**
281
     * @var class-string<Oauth2UserInterface>|null The Identity Class of your application,
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<Oauth2UserInterface>|null at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<Oauth2UserInterface>|null.
Loading history...
282
     * most likely the same as the 'identityClass' of your application's User Component.
283
     * @since 1.0.0
284
     */
285
    public $identityClass = null;
286
287
    /**
288
     * @var null|string Prefix used for url rules. When `null` the module's uniqueId will be used.
289
     * @since 1.0.0
290
     */
291
    public $urlRulesPrefix = null;
292
293
    /**
294
     * @var string URL path for the access token endpoint (will be prefixed with $urlRulesPrefix).
295
     * @since 1.0.0
296
     */
297
    public $authorizePath = 'authorize';
298
299
    /**
300
     * @var string URL path for the access token endpoint (will be prefixed with $urlRulesPrefix).
301
     * @since 1.0.0
302
     */
303
    public $accessTokenPath = 'access-token';
304
305
    /**
306
     * @var string URL path for the certificates jwks endpoint (will be prefixed with $urlRulesPrefix).
307
     * @since 1.0.0
308
     */
309
    public $jwksPath = 'certs';
310
311
    /**
312
     * The URL to the page where the user can perform the client/scope authorization
313
     * (if `null` the build in page will be used).
314
     * @return string
315
     * @since 1.0.0
316
     */
317
    public $clientAuthorizationUrl = null;
318
319
    /**
320
     * @var string The URL path to the build in page where the user can authorize the client for the requested scopes
321
     * (will be prefixed with $urlRulesPrefix).
322
     * Note: This setting will only be used if $clientAuthorizationUrl is `null`.
323
     * @since 1.0.0
324
     */
325
    public $clientAuthorizationPath = 'authorize-client';
326
327
    /**
328
     * @var string The view to use in the "client authorization action" for the page where the user can
329
     * authorize the client for the requested scopes.
330
     * Note: This setting will only be used if $clientAuthorizationUrl is `null`.
331
     * @since 1.0.0
332
     */
333
    public $clientAuthorizationView = 'authorize-client';
334
335
    /**
336
     * @var bool Will the server throw an exception when a Client requests an unknown or unauthorized scope
337
     * (would be silently ignored otherwise).
338
     * Note: this setting can be overwritten per client.
339
     */
340
    public $exceptionOnInvalidScope = false;
341
342
    /**
343
     * Configuration for `Oauth2Client::getRedirectUrisEnvVarConfig()` fallback (the
344
     * Oauth2Client::$envVarConfig['redirectUris'] has precedence).
345
     * When configured, environment variables specified in the `Oauth2Client` redirect URI(s) will be substituted with
346
     * their values. Please see `EnvironmentHelper::parseEnvVars()` for more details.
347
     *
348
     * Warning: This setting applies to all clients, for security it's recommended to specify this configuration at the
349
     * individual client level via its `envVarConfig` setting.
350
     *
351
     * @var array{
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{ at position 2 could not be parsed: the token is null at position 2.
Loading history...
352
     *          allowList: array,
353
     *          denyList: array|null,
354
     *          parseNested: bool,
355
     *          exceptionWhenNotSet: bool,
356
     *          exceptionWhenNotAllowed: bool,
357
     *      }|null
358
     * @see Oauth2ClientInterface::setEnvVarConfig()
359
     * @see Oauth2ClientInterface::getRedirectUrisEnvVarConfig()
360
     * @see \rhertogh\Yii2Oauth2Server\helpers\EnvironmentHelper::parseEnvVars()
361
     */
362
    public $clientRedirectUrisEnvVarConfig = null;
363
364
    /**
365
     * @var string|null The URL path to the OpenID Connect Provider Configuration Information Action.
366
     * If set to `null` the endpoint will be disabled.
367
     * Note: This path is defined in the
368
     *       [OpenID Connect Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.4)
369
     *       specification and should normally not be changed.
370
     * @since 1.0.0
371
     */
372
    public $openIdConnectProviderConfigurationInformationPath = '.well-known/openid-configuration';
373
374
    /**
375
     * @var string The URL path to the OpenID Connect Userinfo Action (will be prefixed with $urlRulesPrefix).
376
     * Note: This setting will only be used if $enableOpenIdConnect and $openIdConnectUserinfoEndpoint are `true`.
377
     * @since 1.0.0
378
     * @see $openIdConnectUserinfoEndpoint
379
     */
380
    public $openIdConnectUserinfoPath = 'oidc/userinfo';
381
382
    /**
383
     * @var string The URL path to the OpenID Connect End Session Action (will be prefixed with $urlRulesPrefix).
384
     * Note: This setting will only be used if $enableOpenIdConnect and $openIdConnectRpInitiatedLogoutEndpoint are `true`.
385
     * @since 1.0.0
386
     * @see $openIdConnectRpInitiatedLogoutEndpoint
387
     * @see https://openid.net/specs/openid-connect-rpinitiated-1_0.html
388
     */
389
    public $openIdConnectRpInitiatedLogoutPath = 'oidc/end-session';
390
391
    /**
392
     * @var Oauth2GrantTypeFactoryInterface[]|GrantTypeInterface[]|string[]|Oauth2GrantTypeFactoryInterface|GrantTypeInterface|string|callable
393
     * The Oauth 2.0 Grant Types that the module will serve.
394
     * @since 1.0.0
395
     */
396
    public $grantTypes = [];
397
398
    /**
399
     * @var bool Should the resource server check for revocation of the access token.
400
     * @since 1.0.0
401
     */
402
    public $resourceServerAccessTokenRevocationValidation = true;
403
404
    /**
405
     * @var bool Enable support for OpenIdvConnect.
406
     * @since 1.0.0
407
     */
408
    public $enableOpenIdConnect = false;
409
410
    /**
411
     * @var bool Enable the .well-known/openid-configuration discovery endpoint.
412
     * @since 1.0.0
413
     */
414
    public $enableOpenIdConnectDiscovery = true;
415
416
    /**
417
     * @var bool include `grant_types_supported` in the OpenIdConnect Discovery.
418
     * Note: Since grant types can be specified per client not all clients might support all enabled grant types.
419
     * @since 1.0.0
420
     */
421
    public $openIdConnectDiscoveryIncludeSupportedGrantTypes = true;
422
423
    /**
424
     * @var string URL to include in the OpenID Connect Discovery Service of a page containing
425
     * human-readable information that developers might want or need to know when using the OpenID Provider.
426
     * @see 'service_documentation' in https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3
427
     * @since 1.0.0
428
     */
429
    public $openIdConnectDiscoveryServiceDocumentationUrl = null;
430
431
    /**
432
     * @var string|bool A string to a custom userinfo endpoint or `true` to enable the build in endpoint.
433
     * @since 1.0.0
434
     * @see $openIdConnectUserinfoPath
435
     */
436
    public $openIdConnectUserinfoEndpoint = true;
437
438
    /**
439
     * @var string|bool A string to a custom logout endpoint or `true` to enable the build in endpoint.
440
     * @since 1.0.0
441
     * @see $openIdConnectRpInitiatedLogoutPath
442
     * @see https://openid.net/specs/openid-connect-rpinitiated-1_0.html
443
     */
444
    public $openIdConnectRpInitiatedLogoutEndpoint = false;
445
446
    /**
447
     * @var bool Allow access to the "end session" endpoint without user authentication (in the form of the
448
     * `id_token_hint` parameter). If enabled the "end session" endpoint will always prompt the user to verify the
449
     * logout if no `id_token_hint` is provided and no redirect after logout will be performed.
450
     * Note: If disabled the client's `oidc_rp_initiated_logout` will be used
451
     * to determine whether to prompt the end-user for logout validation.
452
     * @since 1.0.0
453
     * @see $openIdConnectRpInitiatedLogoutPath
454
     * @see https://openid.net/specs/openid-connect-rpinitiated-1_0.html
455
     */
456
    public $openIdConnectAllowAnonymousRpInitiatedLogout = false;
457
458
    /**
459
     * Warning! Enabling this setting might introduce privacy concerns since the client could poll for the
460
     * online status of a user.
461
     *
462
     * @var bool If this setting is disabled in case of OpenID Connect Context the Access Token won't include a
463
     * Refresh Token when the 'offline_access' scope is not included in the authorization request.
464
     * In some cases it might be needed to always include a Refresh Token, in that case enable this setting and
465
     * implement the `Oauth2OidcUserSessionStatusInterface` on the User Identity model.
466
     * @since 1.0.0
467
     */
468
    public $openIdConnectIssueRefreshTokenWithoutOfflineAccessScope = false;
469
470
    /**
471
     * @var int The default option for "User Account Selection' when not specified for a client.
472
     * @since 1.0.0
473
     */
474
    public $defaultUserAccountSelection = self::USER_ACCOUNT_SELECTION_DISABLED;
475
476
    /**
477
     * @var bool|null Display exception messages that might leak server details. This could be useful for debugging.
478
     * In case of `null` (default) the YII_DEBUG constant will be used.
479
     * Warning: Should NOT be enabled in production!
480
     * @since 1.0.0
481
     */
482
    public $displayConfidentialExceptionMessages = null;
483
484
    /**
485
     * @var string|null The namespace with which migrations will be created (and by which they will be located).
486
     * Note: The specified namespace must be defined as a Yii alias (e.g. '@app').
487
     * @since 1.0.0
488
     */
489
    public $migrationsNamespace = null;
490
    /**
491
     * @var string|null Optional prefix used in the name of generated migrations
492
     * @since 1.0.0
493
     */
494
    public $migrationsPrefix = null;
495
    /**
496
     * @var string|array|int|null Sets the file ownership of generated migrations
497
     * @see \yii\helpers\BaseFileHelper::changeOwnership()
498
     * @since 1.0.0
499
     */
500
    public $migrationsFileOwnership = null;
501
    /**
502
     * @var int|null Sets the file mode of generated migrations
503
     * @see \yii\helpers\BaseFileHelper::changeOwnership()
504
     * @since 1.0.0
505
     */
506
    public $migrationsFileMode = null;
507
508
    /**
509
     * The log level for HTTP Client Errors (HTTP status code 400 - 499). Can be one of the following:
510
     *  - A log level of `\yii\log\Logger` => LEVEL_ERROR, LEVEL_WARNING, LEVEL_INFO, LEVEL_TRACE.
511
     *  - `0` => disable logging for HTTP Client Errors
512
     *  - null => The `YII_DEBUG` constant will be used to determine the log level.
513
     *            If `true` LEVEL_ERROR will be used, LEVEL_INFO otherwise.
514
     * @var int|null
515
     * @see \yii\log\Logger
516
     */
517
    public $httpClientErrorsLogLevel = null;
518
519
    /**
520
     * @var Oauth2AuthorizationServerInterface|null Cache for the authorization server
521
     * @since 1.0.0
522
     */
523
    protected $_authorizationServer = null;
524
525
    /**
526
     * @var Oauth2ResourceServerInterface|null Cache for the resource server
527
     * @since 1.0.0
528
     */
529
    protected $_resourceServer = null;
530
531
    /**
532
     * @var Oauth2CryptographerInterface|null Cache for the Oauth2Cryptographer
533
     * @since 1.0.0
534
     */
535
    protected $_cryptographer = null;
536
537
    /**
538
     * @var string|null The authorization header used when the authorization request was validated.
539
     * @since 1.0.0
540
     */
541
    protected $_oauthClaimsAuthorizationHeader = null;
542
543
    /**
544
     * @inheritDoc
545
     * @throws InvalidConfigException
546
     */
547 130
    public function init()
548
    {
549 130
        parent::init();
550
551 130
        $app = Yii::$app;
552
553 130
        if ($app instanceof WebApplication || $this->appType == static::APPLICATION_TYPE_WEB) {
554 29
            $controllerMap = static::CONTROLLER_MAP[static::APPLICATION_TYPE_WEB];
555 130
        } elseif ($app instanceof ConsoleApplication || $this->appType == static::APPLICATION_TYPE_CONSOLE) {
0 ignored issues
show
introduced by
$app is always a sub-type of yii\console\Application.
Loading history...
556 130
            $controllerMap = static::CONTROLLER_MAP[static::APPLICATION_TYPE_CONSOLE];
557
        } else {
558 1
            throw new InvalidConfigException(
559 1
                'Unable to detect application type, configure it manually by setting `$appType`.'
560 1
            );
561
        }
562 130
        $controllerMap = array_filter(
563 130
            $controllerMap,
564 130
            fn($controllerSettings) => $controllerSettings['serverRole'] & $this->serverRole
565 130
        );
566 130
        $this->controllerMap = ArrayHelper::getColumn($controllerMap, 'controller');
567
568 130
        if (empty($this->identityClass)) {
569 1
            throw new InvalidConfigException('$identityClass must be set.');
570 130
        } elseif (!is_a($this->identityClass, Oauth2UserInterface::class, true)) {
571 1
            throw new InvalidConfigException(
572 1
                $this->identityClass . ' must implement ' . Oauth2UserInterface::class
573 1
            );
574
        }
575
576 130
        foreach (static::DEFAULT_INTERFACE_IMPLEMENTATIONS as $interface => $implementation) {
577 130
            if (!Yii::$container->has($interface)) {
578 130
                Yii::$container->set($interface, $implementation);
579
            }
580
        }
581
582 130
        if (empty($this->urlRulesPrefix)) {
583 130
            $this->urlRulesPrefix = $this->uniqueId;
584
        }
585
586 130
        $this->registerTranslations();
587
    }
588
589
    /**
590
     * @inheritdoc
591
     * @throws InvalidConfigException
592
     */
593 130
    public function bootstrap($app)
594
    {
595
        if (
596 130
            $app instanceof WebApplication
597 130
            && $this->serverRole & static::SERVER_ROLE_AUTHORIZATION_SERVER
598
        ) {
599 29
            $rules = [
600 29
                $this->accessTokenPath => Oauth2ServerControllerInterface::CONTROLLER_NAME
601 29
                    . '/' . Oauth2ServerControllerInterface::ACTION_NAME_ACCESS_TOKEN,
602 29
                $this->authorizePath => Oauth2ServerControllerInterface::CONTROLLER_NAME
603 29
                    . '/' . Oauth2ServerControllerInterface::ACTION_NAME_AUTHORIZE,
604 29
                $this->jwksPath => Oauth2CertificatesControllerInterface::CONTROLLER_NAME
605 29
                    . '/' . Oauth2CertificatesControllerInterface::ACTION_NAME_JWKS,
606 29
            ];
607
608 29
            if (empty($this->clientAuthorizationUrl)) {
609 28
                $rules[$this->clientAuthorizationPath] = Oauth2ConsentControllerInterface::CONTROLLER_NAME
610 28
                    . '/' . Oauth2ConsentControllerInterface::ACTION_NAME_AUTHORIZE_CLIENT;
611
            }
612
613 29
            if ($this->enableOpenIdConnect && $this->openIdConnectUserinfoEndpoint === true) {
614 29
                $rules[$this->openIdConnectUserinfoPath] =
615 29
                    Oauth2OidcControllerInterface::CONTROLLER_NAME
616 29
                    . '/' . Oauth2OidcControllerInterface::ACTION_NAME_USERINFO;
617
            }
618
619 29
            if ($this->enableOpenIdConnect && $this->openIdConnectRpInitiatedLogoutEndpoint === true) {
620
                $rules[$this->openIdConnectRpInitiatedLogoutPath] =
621
                    Oauth2OidcControllerInterface::CONTROLLER_NAME
622
                    . '/' . Oauth2OidcControllerInterface::ACTION_END_SESSION;
623
            }
624
625 29
            $urlManager = $app->getUrlManager();
626 29
            $urlManager->addRules([
627 29
                Yii::createObject([
628 29
                    'class' => GroupUrlRule::class,
629 29
                    'prefix' => $this->urlRulesPrefix,
630 29
                    'routePrefix' => $this->id,
631 29
                    'rules' => $rules,
632 29
                ]),
633 29
            ]);
634
635
            if (
636 29
                $this->enableOpenIdConnect
637 29
                && $this->enableOpenIdConnectDiscovery
638 29
                && $this->openIdConnectProviderConfigurationInformationPath
639
            ) {
640 29
                $urlManager->addRules([
641 29
                    Yii::createObject([
642 29
                        'class' => UrlRule::class,
643 29
                        'pattern' => $this->openIdConnectProviderConfigurationInformationPath,
644 29
                        'route' => $this->id
645 29
                            . '/' . Oauth2WellKnownControllerInterface::CONTROLLER_NAME
646 29
                            . '/' . Oauth2WellKnownControllerInterface::ACTION_NAME_OPENID_CONFIGURATION,
647 29
                    ]),
648 29
                ]);
649
            }
650
        }
651
    }
652
653
    /**
654
     * Registers the translations for the module
655
     * @param bool $force Force the setting of the translations (even if they are already defined).
656
     * @since 1.0.0
657
     */
658 130
    public function registerTranslations($force = false)
659
    {
660 130
        if ($force || !array_key_exists('oauth2', Yii::$app->i18n->translations)) {
661 130
            Yii::$app->i18n->translations['oauth2'] = [
662 130
                'class' => PhpMessageSource::class,
663 130
                'sourceLanguage' => 'en-US',
664 130
                'basePath' => __DIR__ . DIRECTORY_SEPARATOR . 'messages',
665 130
                'fileMap' => [
666 130
                    'oauth2' => 'oauth2.php',
667 130
                ],
668 130
            ];
669
        }
670
    }
671
672
    /**
673
     * @param string $identifier The client identifier
674
     * @param string $name The (user-friendly) name of the client
675
     * @param int $grantTypes The grant types enabled for this client.
676
     *        Use bitwise `OR` to combine multiple types,
677
     *        e.g. `Oauth2Module::GRANT_TYPE_AUTH_CODE | Oauth2Module::GRANT_TYPE_REFRESH_TOKEN`
678
     * @param string|string[] $redirectURIs One or multiple redirect URIs for the client
679
     * @param int $type The client type (e.g. Confidential or Public)
680
     *        See `\rhertogh\Yii2Oauth2Server\interfaces\models\Oauth2ClientInterface::TYPES` for possible values
681
     * @param string|null $secret The client secret in case the client `type` is `confidential`.
682
     * @param string|string[]|array[]|Oauth2ScopeInterface[]|null $scopes
683
     * @param int|null $userId
684
     * @return Oauth2ClientInterface
685
     * @throws InvalidConfigException
686
     * @throws \yii\db\Exception
687
     */
688 5
    public function createClient(
689
        $identifier,
690
        $name,
691
        $grantTypes,
692
        $redirectURIs,
693
        $type,
694
        $secret = null,
695
        $scopes = null,
696
        $userId = null,
697
        $endUsersMayAuthorizeClient = null,
698
        $skipAuthorizationIfScopeIsAllowed = null
699
    ) {
700 5
        if (!($this->serverRole & static::SERVER_ROLE_AUTHORIZATION_SERVER)) {
701 1
            throw new InvalidCallException('Oauth2 server role does not include authorization server.');
702
        }
703
704
        /** @var Oauth2ClientInterface $client */
705 4
        $client = Yii::createObject([
706 4
            'class' => Oauth2ClientInterface::class,
707 4
            'identifier' => $identifier,
708 4
            'type' => $type,
709 4
            'name' => $name,
710 4
            'redirectUri' => $redirectURIs,
711 4
            'grantTypes' => $grantTypes,
712 4
            'endUsersMayAuthorizeClient' => $endUsersMayAuthorizeClient,
713 4
            'skip_authorization_if_scope_is_allowed' => $skipAuthorizationIfScopeIsAllowed,
714 4
            'clientCredentialsGrantUserId' => $userId
715 4
        ]);
716
717 4
        $transaction = $client::getDb()->beginTransaction();
718
719
        try {
720 4
            if ($type == Oauth2ClientInterface::TYPE_CONFIDENTIAL) {
721 4
                $client->setSecret($secret, $this->getCryptographer());
722
            }
723
724 3
            $client
725 3
                ->persist()
726 3
                ->syncClientScopes($scopes, $this->getScopeRepository());
727
728 3
            $transaction->commit();
729 1
        } catch (\Exception $e) {
730 1
            $transaction->rollBack();
731 1
            throw $e;
732
        }
733
734 3
        return $client;
735
    }
736
737
    /**
738
     * @return CryptKey The private key of the server.
739
     * @throws InvalidConfigException
740
     * @since 1.0.0
741
     */
742 22
    public function getPrivateKey()
743
    {
744 22
        $privateKey = $this->privateKey;
745 22
        if (StringHelper::startsWith($privateKey, '@')) {
746 19
            $privateKey = 'file://' . Yii::getAlias($privateKey);
0 ignored issues
show
Bug introduced by
Are you sure Yii::getAlias($privateKey) of type false|string can be used in concatenation? ( Ignorable by Annotation )

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

746
            $privateKey = 'file://' . /** @scrutinizer ignore-type */ Yii::getAlias($privateKey);
Loading history...
747
        }
748 22
        return Yii::createObject(CryptKey::class, [$privateKey, $this->privateKeyPassphrase]);
749
    }
750
751
    /**
752
     * @return CryptKey The public key of the server.
753
     * @throws InvalidConfigException
754
     * @since 1.0.0
755
     */
756 9
    public function getPublicKey()
757
    {
758 9
        $publicKey = $this->publicKey;
759 9
        if (StringHelper::startsWith($publicKey, '@')) {
760 6
            $publicKey = 'file://' . Yii::getAlias($publicKey);
0 ignored issues
show
Bug introduced by
Are you sure Yii::getAlias($publicKey) of type false|string can be used in concatenation? ( Ignorable by Annotation )

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

760
            $publicKey = 'file://' . /** @scrutinizer ignore-type */ Yii::getAlias($publicKey);
Loading history...
761
        }
762 9
        return Yii::createObject(CryptKey::class, [$publicKey]);
763
    }
764
765
    /**
766
     * @return Oauth2AuthorizationServerInterface The authorization server.
767
     * @throws InvalidConfigException
768
     * @since 1.0.0
769
     */
770 27
    public function getAuthorizationServer()
771
    {
772 27
        if (!($this->serverRole & static::SERVER_ROLE_AUTHORIZATION_SERVER)) {
773 1
            throw new InvalidCallException('Oauth2 server role does not include authorization server.');
774
        }
775
776 26
        if (!$this->_authorizationServer) {
777 26
            $this->ensureProperties(static::REQUIRED_SETTINGS_AUTHORIZATION_SERVER);
778
779 21
            if (!$this->getCryptographer()->hasKey($this->defaultStorageEncryptionKey)) {
780 1
                throw new InvalidConfigException(
781 1
                    'Key "' . $this->defaultStorageEncryptionKey . '" is not set in $storageEncryptionKeys'
782 1
                );
783
            }
784
785
            /** @var Oauth2EncryptionKeyFactoryInterface $keyFactory */
786 19
            $keyFactory = Yii::createObject(Oauth2EncryptionKeyFactoryInterface::class);
787
            try {
788 19
                $codesEncryptionKey = $keyFactory->createFromAsciiSafeString($this->codesEncryptionKey);
789 1
            } catch (BadFormatException $e) {
790 1
                throw new InvalidConfigException(
791 1
                    '$codesEncryptionKey is malformed: ' . $e->getMessage(),
792 1
                    0,
793 1
                    $e
794 1
                );
795
            } catch (EnvironmentIsBrokenException $e) {
796
                throw new InvalidConfigException(
797
                    'Could not instantiate $codesEncryptionKey: ' . $e->getMessage(),
798
                    0,
799
                    $e
800
                );
801
            }
802
803 18
            $responseType = null;
804 18
            if ($this->enableOpenIdConnect) {
805 18
                $responseType = Yii::createObject(Oauth2OidcBearerTokenResponseInterface::class, [
806 18
                    $this,
807 18
                ]);
808
            }
809
810 18
            $this->_authorizationServer = Yii::createObject(Oauth2AuthorizationServerInterface::class, [
811 18
                $this->getClientRepository(),
812 18
                $this->getAccessTokenRepository(),
813 18
                $this->getScopeRepository(),
814 18
                $this->getPrivateKey(),
815 18
                $codesEncryptionKey,
816 18
                $responseType
817 18
            ]);
818
819 18
            if (!empty($this->grantTypes)) {
820 18
                $grantTypes = $this->grantTypes;
821
822 18
                if (is_callable($grantTypes)) {
823 1
                    call_user_func($grantTypes, $this->_authorizationServer, $this);
0 ignored issues
show
Bug introduced by
It seems like $grantTypes can also be of type League\OAuth2\Server\Grant\GrantTypeInterface and rhertogh\Yii2Oauth2Serve...antTypeFactoryInterface; however, parameter $callback of call_user_func() does only seem to accept callable, 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

823
                    call_user_func(/** @scrutinizer ignore-type */ $grantTypes, $this->_authorizationServer, $this);
Loading history...
824
                } else {
825 17
                    if (!is_array($grantTypes)) {
826 2
                        $grantTypes = [$grantTypes];
827
                    }
828
829 17
                    foreach ($grantTypes as $grantTypeDefinition) {
830 17
                        if ($grantTypeDefinition instanceof GrantTypeInterface) {
831 1
                            $accessTokenTTL = $this->getDefaultAccessTokenTTL();
832 1
                            $this->_authorizationServer->enableGrantType($grantTypeDefinition, $accessTokenTTL);
833
                        } elseif (
834
                            (
835 16
                                is_numeric($grantTypeDefinition)
836 16
                                && array_key_exists($grantTypeDefinition, static::DEFAULT_GRANT_TYPE_FACTORIES)
837
                            )
838 16
                            || is_a($grantTypeDefinition, Oauth2GrantTypeFactoryInterface::class, true)
839
                        ) {
840
                            if (
841 15
                                is_numeric($grantTypeDefinition)
842 15
                                && array_key_exists($grantTypeDefinition, static::DEFAULT_GRANT_TYPE_FACTORIES)
843
                            ) {
844 15
                                $grantTypeDefinition = static::DEFAULT_GRANT_TYPE_FACTORIES[$grantTypeDefinition];
845
                            }
846
847
                            /** @var Oauth2GrantTypeFactoryInterface $factory */
848 15
                            $factory = Yii::createObject([
849 15
                                'class' => $grantTypeDefinition,
850 15
                                'module' => $this,
851 15
                            ]);
852 15
                            $accessTokenTTL = $factory->getDefaultAccessTokenTTL()
853 15
                                ?? $this->getDefaultAccessTokenTTL();
854 15
                            $this->_authorizationServer->enableGrantType($factory->getGrantType(), $accessTokenTTL);
855
                        } else {
856 1
                            throw new InvalidConfigException(
857 1
                                'Unknown grantType '
858 1
                                . (
859 1
                                    is_scalar($grantTypeDefinition)
860 1
                                        ? '"' . $grantTypeDefinition . '".'
861 1
                                        : 'with data type ' . gettype($grantTypeDefinition)
862 1
                                )
863 1
                            );
864
                        }
865
                    }
866
                }
867
            }
868
        }
869
870 17
        return $this->_authorizationServer;
871
    }
872
873
    /**
874
     * @inheritDoc
875
     * @throws InvalidConfigException
876
     */
877 6
    public function getOidcScopeCollection()
878
    {
879 6
        if ($this->_oidcScopeCollection === null) {
880 6
            $openIdConnectScopes = $this->getOpenIdConnectScopes();
881 6
            if ($openIdConnectScopes instanceof Oauth2OidcScopeCollectionInterface) {
882 1
                $this->_oidcScopeCollection = $openIdConnectScopes;
883 5
            } elseif (is_callable($openIdConnectScopes)) {
884 1
                $this->_oidcScopeCollection = call_user_func($openIdConnectScopes, $this);
885 1
                if (!($this->_oidcScopeCollection instanceof Oauth2OidcScopeCollectionInterface)) {
886 1
                    throw new InvalidConfigException(
887 1
                        '$openIdConnectScopes must return an instance of '
888 1
                            . Oauth2OidcScopeCollectionInterface::class
889 1
                    );
890
                }
891 4
            } elseif (is_array($openIdConnectScopes) || is_string($openIdConnectScopes)) {
892 3
                $this->_oidcScopeCollection = Yii::createObject([
893 3
                    'class' => Oauth2OidcScopeCollectionInterface::class,
894 3
                    'oidcScopes' => (array)$openIdConnectScopes,
895 3
                ]);
896
            } else {
897 1
                throw new InvalidConfigException(
898 1
                    '$openIdConnectScopes must be a callable, array, string or '
899 1
                        . Oauth2OidcScopeCollectionInterface::class
900 1
                );
901
            }
902
        }
903
904 5
        return $this->_oidcScopeCollection;
905
    }
906
907
    /**
908
     * @return Oauth2ResourceServerInterface The resource server.
909
     * @throws InvalidConfigException
910
     * @since 1.0.0
911
     */
912 7
    public function getResourceServer()
913
    {
914 7
        if (!($this->serverRole & static::SERVER_ROLE_RESOURCE_SERVER)) {
915 1
            throw new InvalidCallException('Oauth2 server role does not include resource server.');
916
        }
917
918 6
        if (!$this->_resourceServer) {
919 6
            $this->ensureProperties(static::REQUIRED_SETTINGS_RESOURCE_SERVER);
920
921 5
            $accessTokenRepository = $this->getAccessTokenRepository()
922 5
                ->setRevocationValidation($this->resourceServerAccessTokenRevocationValidation);
923
924 5
            $this->_resourceServer = Yii::createObject(Oauth2ResourceServerInterface::class, [
925 5
                $accessTokenRepository,
926 5
                $this->getPublicKey(),
927 5
            ]);
928
        }
929
930 5
        return $this->_resourceServer;
931
    }
932
933
    /**
934
     * @return Oauth2CryptographerInterface The data cryptographer for the module.
935
     * @throws InvalidConfigException
936
     * @since 1.0.0
937
     */
938 27
    public function getCryptographer()
939
    {
940 27
        if (!$this->_cryptographer) {
941 27
            $this->_cryptographer = Yii::createObject([
942 27
                'class' => Oauth2CryptographerInterface::class,
943 27
                'keys' => $this->storageEncryptionKeys,
944 27
                'defaultKeyName' => $this->defaultStorageEncryptionKey,
945 27
            ]);
946
        }
947
948 26
        return $this->_cryptographer;
949
    }
950
951
    /**
952
     * @param string|null $newKeyName
953
     * @return array
954
     * @throws InvalidConfigException
955
     */
956 1
    public function rotateStorageEncryptionKeys($newKeyName = null)
957
    {
958 1
        $cryptographer = $this->getCryptographer();
959
960 1
        $result = [];
961 1
        foreach (static::ENCRYPTED_MODELS as $modelInterface) {
962 1
            $modelClass = DiHelper::getValidatedClassName($modelInterface);
963 1
            if (!is_a($modelClass, Oauth2EncryptedStorageInterface::class, true)) {
964
                throw new InvalidConfigException($modelInterface . ' must implement '
965
                    . Oauth2EncryptedStorageInterface::class);
966
            }
967 1
            $result[$modelClass] = $modelClass::rotateStorageEncryptionKeys($cryptographer, $newKeyName);
968
        }
969
970 1
        return $result;
971
    }
972
973
    /**
974
     * Checks if the connection is using TLS or if the remote IP address is allowed to connect without TLS.
975
     * @return bool
976
     */
977 12
    public function validateTlsConnection()
978
    {
979 12
        if (Yii::$app->request->getIsSecureConnection()) {
980 1
            return true;
981
        }
982
983
        if (
984 11
            !empty($this->nonTlsAllowedRanges)
985 11
            && (new IpValidator(['ranges' => $this->nonTlsAllowedRanges]))->validate(Yii::$app->request->getRemoteIP())
986
        ) {
987 7
            return true;
988
        }
989
990 4
        return false;
991
    }
992
993
    /**
994
     * @return array
995
     * @throws InvalidConfigException
996
     */
997
    public function getStorageEncryptionKeyUsage()
998
    {
999
        $cryptographer = $this->getCryptographer();
1000
1001
        $result = [];
1002
        foreach (static::ENCRYPTED_MODELS as $modelInterface) {
1003
            $modelClass = DiHelper::getValidatedClassName($modelInterface);
1004
            if (!is_a($modelClass, Oauth2EncryptedStorageInterface::class, true)) {
1005
                throw new InvalidConfigException($modelInterface . ' must implement '
1006
                    . Oauth2EncryptedStorageInterface::class);
1007
            }
1008
1009
            $result[$modelClass] = $modelClass::getUsedStorageEncryptionKeys($cryptographer);
1010
        }
1011
1012
        return $result;
1013
    }
1014
1015
    /**
1016
     * @param Oauth2ClientInterface $client
1017
     * @param string[] $requestedScopeIdentifiers
1018
     * @throws Oauth2ServerException
1019
     */
1020 6
    public function validateAuthRequestScopes($client, $requestedScopeIdentifiers, $redirectUri = null)
1021
    {
1022 6
        if (!$client->validateAuthRequestScopes($requestedScopeIdentifiers, $unknownScopes, $unauthorizedScopes)) {
1023
            Yii::info('Invalid scope for client "' . $client->getIdentifier() . '": '
1024
                . VarDumper::export(['unauthorizedScopes' => $unauthorizedScopes, 'unknownScopes' => $unknownScopes]));
1025
1026
            if (
1027
                $client->getExceptionOnInvalidScope() === true
1028
                || (
1029
                    $client->getExceptionOnInvalidScope() === null
1030
                    && $this->exceptionOnInvalidScope === true
1031
                )
1032
            ) {
1033
                if ($unknownScopes) {
1034
                    throw Oauth2ServerException::unknownScope(array_shift($unknownScopes), $redirectUri);
1035
                }
1036
                throw Oauth2ServerException::scopeNotAllowedForClient(array_shift($unauthorizedScopes), $redirectUri);
1037
            }
1038
        }
1039
    }
1040
1041
    /**
1042
     * Generates a redirect Response to the client authorization page where the user is prompted to authorize the
1043
     * client and requested scope.
1044
     * @param Oauth2ClientAuthorizationRequestInterface $clientAuthorizationRequest
1045
     * @return Response
1046
     * @since 1.0.0
1047
     */
1048 5
    public function generateClientAuthReqRedirectResponse($clientAuthorizationRequest)
1049
    {
1050 5
        $this->setClientAuthReqSession($clientAuthorizationRequest);
1051 5
        if (!empty($this->clientAuthorizationUrl)) {
1052 1
            $url = $this->clientAuthorizationUrl;
1053
        } else {
1054 4
            $url = $this->uniqueId
1055 4
                . '/' . Oauth2ConsentControllerInterface::CONTROLLER_NAME
1056 4
                . '/' . Oauth2ConsentControllerInterface::ACTION_NAME_AUTHORIZE_CLIENT;
1057
        }
1058 5
        return Yii::$app->response->redirect([
1059 5
            $url,
1060 5
            'clientAuthorizationRequestId' => $clientAuthorizationRequest->getRequestId(),
1061 5
        ]);
1062
    }
1063
1064
    /**
1065
     * Get a previously stored Client Authorization Request from the session.
1066
     * @param string $requestId
1067
     * @return Oauth2ClientAuthorizationRequestInterface|null
1068
     * @since 1.0.0
1069
     */
1070 5
    public function getClientAuthReqSession($requestId)
1071
    {
1072 5
        if (empty($requestId)) {
1073
            return null;
1074
        }
1075 5
        $key = static::CLIENT_AUTHORIZATION_REQUEST_SESSION_PREFIX . $requestId;
1076 5
        $clientAuthorizationRequest = Yii::$app->session->get($key);
0 ignored issues
show
Bug introduced by
The method get() does not exist on null. ( Ignorable by Annotation )

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

1076
        /** @scrutinizer ignore-call */ 
1077
        $clientAuthorizationRequest = Yii::$app->session->get($key);

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...
1077 5
        if (!($clientAuthorizationRequest instanceof Oauth2ClientAuthorizationRequestInterface)) {
1078 2
            if (!empty($clientAuthorizationRequest)) {
1079 1
                Yii::warning(
1080 1
                    'Found a ClientAuthorizationRequestSession with key "' . $key
1081 1
                        . '", but it\'s not a ' . Oauth2ClientAuthorizationRequestInterface::class
1082 1
                );
1083
            }
1084 2
            return null;
1085
        }
1086 5
        if ($clientAuthorizationRequest->getRequestId() !== $requestId) {
1087 1
            Yii::warning(
1088 1
                'Found a ClientAuthorizationRequestSession with key "' . $key
1089 1
                    . '", but its request id does not match "' . $requestId . '".'
1090 1
            );
1091 1
            return null;
1092
        }
1093 5
        $clientAuthorizationRequest->setModule($this);
1094
1095 5
        return $clientAuthorizationRequest;
1096
    }
1097
1098
    /**
1099
     * Stores the Client Authorization Request in the session.
1100
     * @param Oauth2ClientAuthorizationRequestInterface $clientAuthorizationRequest
1101
     * @since 1.0.0
1102
     */
1103 8
    public function setClientAuthReqSession($clientAuthorizationRequest)
1104
    {
1105 8
        $requestId = $clientAuthorizationRequest->getRequestId();
1106 8
        if (empty($requestId)) {
1107 1
            throw new InvalidArgumentException('$scopeAuthorization must return a request id.');
1108
        }
1109 7
        $key = static::CLIENT_AUTHORIZATION_REQUEST_SESSION_PREFIX . $requestId;
1110 7
        Yii::$app->session->set($key, $clientAuthorizationRequest);
1111
    }
1112
1113
    /**
1114
     * Stores whether the user was authenticated during the completion of the Client Authorization Request.
1115
     * @param string $clientAuthorizationRequestId
1116
     * @param bool $authenticatedDuringRequest
1117
     * @since 1.0.0
1118
     */
1119
    public function setUserAuthenticatedDuringClientAuthRequest(
1120
        $clientAuthorizationRequestId,
1121
        $authenticatedDuringRequest
1122
    ) {
1123
        $clientAuthorizationRequest = $this->getClientAuthReqSession($clientAuthorizationRequestId);
1124
        if ($clientAuthorizationRequest) {
1125
            $clientAuthorizationRequest->setUserAuthenticatedDuringRequest($authenticatedDuringRequest);
1126
            $this->setClientAuthReqSession($clientAuthorizationRequest);
1127
        }
1128
    }
1129
1130
    /**
1131
     * Stores the user identity selected during the completion of the Client Authorization Request.
1132
     * @param string $clientAuthorizationRequestId
1133
     * @param Oauth2UserInterface $userIdentity
1134
     * @since 1.0.0
1135
     */
1136
    public function setClientAuthRequestUserIdentity($clientAuthorizationRequestId, $userIdentity)
1137
    {
1138
        $clientAuthorizationRequest = $this->getClientAuthReqSession($clientAuthorizationRequestId);
1139
        if ($clientAuthorizationRequest) {
1140
            $clientAuthorizationRequest->setUserIdentity($userIdentity);
1141
            $this->setClientAuthReqSession($clientAuthorizationRequest);
1142
        }
1143
    }
1144
1145
    /**
1146
     * Clears a Client Authorization Request from the session storage.
1147
     * @param string $requestId
1148
     * @since 1.0.0
1149
     */
1150 2
    public function removeClientAuthReqSession($requestId)
1151
    {
1152 2
        if (empty($requestId)) {
1153 1
            throw new InvalidArgumentException('$requestId can not be empty.');
1154
        }
1155 1
        $key = static::CLIENT_AUTHORIZATION_REQUEST_SESSION_PREFIX . $requestId;
1156 1
        Yii::$app->session->remove($key);
1157
    }
1158
1159
    /**
1160
     * Generates a redirect Response when the Client Authorization Request is completed.
1161
     * @param Oauth2ClientAuthorizationRequestInterface $clientAuthorizationRequest
1162
     * @return Response
1163
     * @since 1.0.0
1164
     */
1165 1
    public function generateClientAuthReqCompledRedirectResponse($clientAuthorizationRequest)
1166
    {
1167 1
        $clientAuthorizationRequest->processAuthorization();
1168 1
        $this->setClientAuthReqSession($clientAuthorizationRequest);
1169 1
        return Yii::$app->response->redirect($clientAuthorizationRequest->getAuthorizationRequestUrl());
1170
    }
1171
1172
    /**
1173
     * @return IdentityInterface|Oauth2UserInterface|Oauth2OidcUserInterface|null
1174
     * @throws InvalidConfigException
1175
     * @since 1.0.0
1176
     */
1177 5
    public function getUserIdentity()
1178
    {
1179 5
        $user = Yii::$app->user->identity;
1180 5
        if (!empty($user) && !($user instanceof Oauth2UserInterface)) {
1181 1
            throw new InvalidConfigException(
1182 1
                'Yii::$app->user->identity (currently ' . get_class($user)
1183 1
                    . ') must implement ' . Oauth2UserInterface::class
1184 1
            );
1185
        }
1186 4
        return $user;
1187
    }
1188
1189
    /**
1190
     * Validates a bearer token authenticated request. Note: this method does not return a result but will throw
1191
     * an exception when the authentication fails.
1192
     * @throws InvalidConfigException
1193
     * @throws Oauth2ServerException
1194
     * @since 1.0.0
1195
     */
1196 3
    public function validateAuthenticatedRequest()
1197
    {
1198 3
        $psr7Request = Psr7Helper::yiiToPsr7Request(Yii::$app->request);
0 ignored issues
show
Bug introduced by
It seems like Yii::app->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

1198
        $psr7Request = Psr7Helper::yiiToPsr7Request(/** @scrutinizer ignore-type */ Yii::$app->request);
Loading history...
1199
1200 3
        $psr7Request = $this->getResourceServer()->validateAuthenticatedRequest($psr7Request);
1201
1202 3
        $this->_oauthClaims = $psr7Request->getAttributes();
1203 3
        $this->_oauthClaimsAuthorizationHeader = Yii::$app->request->getHeaders()->get('Authorization');
0 ignored issues
show
Documentation Bug introduced by
It seems like Yii::app->request->getHe...)->get('Authorization') can also be of type array. However, the property $_oauthClaimsAuthorizationHeader is declared as type null|string. 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...
1204
    }
1205
1206
    /**
1207
     * Find a user identity bases on an access token.
1208
     * Note: validateAuthenticatedRequest() must be called before this method is called.
1209
     * @param string $token
1210
     * @param string $type
1211
     * @return Oauth2UserInterface|null
1212
     * @throws InvalidConfigException
1213
     * @throws Oauth2ServerException
1214
     * @see validateAuthenticatedRequest()
1215
     * @since 1.0.0
1216
     */
1217 4
    public function findIdentityByAccessToken($token, $type)
1218
    {
1219 4
        if (!is_a($type, Oauth2HttpBearerAuthInterface::class, true)) {
1220 1
            throw new InvalidCallException($type . ' must implement ' . Oauth2HttpBearerAuthInterface::class);
1221
        }
1222
1223
        if (
1224 3
            !preg_match('/^Bearer\s+(.*?)$/', $this->_oauthClaimsAuthorizationHeader, $matches)
0 ignored issues
show
Bug introduced by
It seems like $this->_oauthClaimsAuthorizationHeader can also be of type null; however, parameter $subject of preg_match() does only seem to accept string, 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

1224
            !preg_match('/^Bearer\s+(.*?)$/', /** @scrutinizer ignore-type */ $this->_oauthClaimsAuthorizationHeader, $matches)
Loading history...
1225 3
            || !Yii::$app->security->compareString($matches[1], $token)
1226
        ) {
1227 1
            throw new InvalidCallException(
1228 1
                'validateAuthenticatedRequest() must be called before findIdentityByAccessToken().'
1229 1
            );
1230
        }
1231
1232 2
        $userId = $this->getRequestOauthUserId();
1233 2
        if (empty($userId)) {
1234 1
            return null;
1235
        }
1236
1237 1
        return $this->identityClass::findIdentity($userId);
1238
    }
1239
1240
    /**
1241
     * Generate a "Personal Access Token" (PAT) which can be used as an alternative to using passwords
1242
     * for authentication (e.g. when using an API or command line).
1243
     *
1244
     * Note: Personal Access Tokens are intended to access resources on behalf users themselves.
1245
     *       To grant access to resources on behalf of an organization, or for long-lived integrations,
1246
     *       you most likely want to define an Oauth2 Client with the "Client Credentials" grant
1247
     *       (https://oauth.net/2/grant-types/client-credentials).
1248
     *
1249
     * @param string $clientIdentifier The Oauth2 client identifier for which the PAT should be generated.
1250
     * @param int|string $userIdentifier The identifier (primary key) of the user for which the PAT should be generated.
1251
     * @param Oauth2ScopeInterface[]|string[]|string|null $scope The Access Token scope.
1252
     * @param string|true|null $clientSecret If the client is a "confidential" client the secret is required.
1253
     *        If the boolean value `true` is passed, the client secret is automatically injected.
1254
     * @return Oauth2AccessTokenData
1255
     */
1256 3
    public function generatePersonalAccessToken($clientIdentifier, $userIdentifier, $scope = null, $clientSecret = null)
1257
    {
1258 3
        if (is_array($scope)) {
1259 2
            $scopeIdentifiers = [];
1260 2
            foreach ($scope as $scopeItem) {
1261 2
                if (is_string($scopeItem)) {
1262 1
                    $scopeIdentifiers[] = $scopeItem;
1263 1
                } elseif ($scopeItem instanceof Oauth2ScopeInterface) {
1264 1
                    $scopeIdentifiers[] = $scopeItem->getIdentifier();
1265
                } else {
1266
                    throw new InvalidArgumentException('If $scope is an array its elements must be either'
1267
                        . ' a string or an instance of ' . Oauth2ScopeInterface::class);
1268
                }
1269
            }
1270 2
            $scope = implode(' ', $scopeIdentifiers);
1271
        }
1272
1273 3
        if ($clientSecret === true) {
1274
            /** @var Oauth2ClientInterface $client */
1275 3
            $client = $this->getClientRepository()->findModelByIdentifier($clientIdentifier);
1276 3
            if ($client && $client->isConfidential()) {
1277 3
                $clientSecret = $client->getDecryptedSecret($this->getCryptographer());
1278
            } else {
1279
                $clientSecret = null;
1280
            }
1281
        }
1282
1283 3
        $request = (new Psr7ServerRequest('POST', ''))->withParsedBody([
1284 3
            'grant_type' => static::GRANT_TYPE_IDENTIFIER_PERSONAL_ACCESS_TOKEN,
1285 3
            'client_id' => $clientIdentifier,
1286 3
            'client_secret' => $clientSecret,
1287 3
            'user_id' => $userIdentifier,
1288 3
            'scope' => $scope,
1289 3
        ]);
1290
1291 3
        return new Oauth2AccessTokenData(Json::decode(
0 ignored issues
show
Bug introduced by
It seems like yii\helpers\Json::decode...etBody()->__toString()) can also be of type null; however, parameter $data of rhertogh\Yii2Oauth2Serve...okenData::__construct() does only seem to accept array, 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

1291
        return new Oauth2AccessTokenData(/** @scrutinizer ignore-type */ Json::decode(
Loading history...
1292 3
            $this->getAuthorizationServer()
1293 3
                ->respondToAccessTokenRequest(
1294 3
                    $request,
1295 3
                    new Psr7Response()
1296 3
                )
1297 3
                ->getBody()
1298 3
                ->__toString()
1299 3
        ));
1300
    }
1301
1302
    /**
1303
     * @inheritDoc
1304
     */
1305 5
    protected function getRequestOauthClaim($attribute, $default = null)
1306
    {
1307 5
        if (empty($this->_oauthClaimsAuthorizationHeader)) {
1308
            // User authorization was not processed by Oauth2Module.
1309 1
            return $default;
1310
        }
1311 4
        if (Yii::$app->request->getHeaders()->get('Authorization') !== $this->_oauthClaimsAuthorizationHeader) {
1312 1
            throw new InvalidCallException(
1313 1
                'App Request Authorization header does not match the processed Oauth header.'
1314 1
            );
1315
        }
1316 3
        return $this->_oauthClaims[$attribute] ?? $default;
1317
    }
1318
1319
    /**
1320
     * Helper function to ensure the required properties are configured for the module.
1321
     * @param string[] $properties
1322
     * @throws InvalidConfigException
1323
     * @since 1.0.0
1324
     */
1325 32
    protected function ensureProperties($properties)
1326
    {
1327 32
        foreach ($properties as $property) {
1328 32
            if (empty($this->$property)) {
1329 6
                throw new InvalidConfigException(__CLASS__ . '::$' . $property . ' must be set.');
1330
            }
1331
        }
1332
    }
1333
1334
    public function logoutUser()
1335
    {
1336
        Yii::$app->user->logout();
0 ignored issues
show
Bug introduced by
The method logout() does not exist on null. ( Ignorable by Annotation )

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

1336
        Yii::$app->user->/** @scrutinizer ignore-call */ 
1337
                         logout();

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...
1337
    }
1338
1339
    public function getElaboratedHttpClientErrorsLogLevel()
1340
    {
1341
        if ($this->httpClientErrorsLogLevel === null) {
1342
            return YII_DEBUG ? Logger::LEVEL_ERROR : Logger::LEVEL_INFO;
1343
        }
1344
1345
        return $this->httpClientErrorsLogLevel;
1346
    }
1347
}
1348