Passed
Push — master ( f55946...ccddd5 )
by Rutger
13:14
created

Oauth2Module   F

Complexity

Total Complexity 111

Size/Duplication

Total Lines 1118
Duplicated Lines 0 %

Test Coverage

Coverage 82.81%

Importance

Changes 5
Bugs 0 Features 0
Metric Value
wmc 111
eloc 420
c 5
b 0
f 0
dl 0
loc 1118
rs 2
ccs 236
cts 285
cp 0.8281

25 Methods

Rating   Name   Duplication   Size   Complexity  
A rotateStorageEncryptionKeys() 0 15 3
A removeClientAuthReqSession() 0 7 2
B bootstrap() 0 44 8
D getAuthorizationServer() 0 105 20
A validateAuthenticatedRequest() 0 8 1
A generateClientAuthReqRedirectResponse() 0 13 2
A getPrivateKey() 0 7 2
A getUserIdentity() 0 10 3
A registerTranslations() 0 9 3
A getResourceServer() 0 19 3
B createClient() 0 74 9
A getClientAuthReqSession() 0 26 5
A getEncryptor() 0 11 2
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
A ensureProperties() 0 5 3
A getRequestOauthClaim() 0 12 3
B generatePersonalAccessToken() 0 41 8

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\controllers\console\Oauth2ClientController;
19
use rhertogh\Yii2Oauth2Server\controllers\console\Oauth2DebugController;
20
use rhertogh\Yii2Oauth2Server\controllers\console\Oauth2EncryptionController;
21
use rhertogh\Yii2Oauth2Server\controllers\console\Oauth2MigrationsController;
22
use rhertogh\Yii2Oauth2Server\controllers\console\Oauth2PersonalAccessTokenController;
23
use rhertogh\Yii2Oauth2Server\exceptions\Oauth2ServerException;
24
use rhertogh\Yii2Oauth2Server\helpers\DiHelper;
25
use rhertogh\Yii2Oauth2Server\helpers\Psr7Helper;
26
use rhertogh\Yii2Oauth2Server\interfaces\components\authorization\Oauth2ClientAuthorizationRequestInterface;
27
use rhertogh\Yii2Oauth2Server\interfaces\components\encryption\Oauth2EncryptorInterface;
28
use rhertogh\Yii2Oauth2Server\interfaces\components\factories\encryption\Oauth2EncryptionKeyFactoryInterface;
29
use rhertogh\Yii2Oauth2Server\interfaces\components\factories\grants\base\Oauth2GrantTypeFactoryInterface;
30
use rhertogh\Yii2Oauth2Server\interfaces\components\openidconnect\scope\Oauth2OidcScopeCollectionInterface;
31
use rhertogh\Yii2Oauth2Server\interfaces\components\openidconnect\server\Oauth2OidcBearerTokenResponseInterface;
32
use rhertogh\Yii2Oauth2Server\interfaces\components\server\Oauth2AuthorizationServerInterface;
33
use rhertogh\Yii2Oauth2Server\interfaces\components\server\Oauth2ResourceServerInterface;
34
use rhertogh\Yii2Oauth2Server\interfaces\controllers\web\Oauth2CertificatesControllerInterface;
35
use rhertogh\Yii2Oauth2Server\interfaces\controllers\web\Oauth2ConsentControllerInterface;
36
use rhertogh\Yii2Oauth2Server\interfaces\controllers\web\Oauth2OidcControllerInterface;
37
use rhertogh\Yii2Oauth2Server\interfaces\controllers\web\Oauth2ServerControllerInterface;
38
use rhertogh\Yii2Oauth2Server\interfaces\controllers\web\Oauth2WellKnownControllerInterface;
39
use rhertogh\Yii2Oauth2Server\interfaces\filters\auth\Oauth2HttpBearerAuthInterface;
40
use rhertogh\Yii2Oauth2Server\interfaces\models\base\Oauth2EncryptedStorageInterface;
41
use rhertogh\Yii2Oauth2Server\interfaces\models\external\user\Oauth2OidcUserInterface;
42
use rhertogh\Yii2Oauth2Server\interfaces\models\external\user\Oauth2UserInterface;
43
use rhertogh\Yii2Oauth2Server\interfaces\models\Oauth2ClientInterface;
44
use rhertogh\Yii2Oauth2Server\interfaces\models\Oauth2ClientScopeInterface;
45
use rhertogh\Yii2Oauth2Server\interfaces\models\Oauth2ScopeInterface;
46
use Yii;
47
use yii\base\BootstrapInterface;
48
use yii\base\InvalidArgumentException;
49
use yii\base\InvalidCallException;
50
use yii\base\InvalidConfigException;
51
use yii\console\Application as ConsoleApplication;
52
use yii\helpers\ArrayHelper;
53
use yii\helpers\Json;
54
use yii\helpers\StringHelper;
55
use yii\i18n\PhpMessageSource;
56
use yii\web\Application as WebApplication;
57
use yii\web\GroupUrlRule;
58
use yii\web\IdentityInterface;
59
use yii\web\Response;
60
use yii\web\UrlRule;
61
62
/**
63
 * This is the main module class for the Yii2 Oauth2 Server module.
64
 * To use it, include it as a module in the application configuration like the following:
65
 *
66
 * ~~~
67
 * return [
68
 *     'bootstrap' => ['oauth2'],
69
 *     'modules' => [
70
 *         'oauth2' => [
71
 *             'class' => 'rhertogh\Yii2Oauth2Server\Oauth2Module',
72
 *             // ... Please check docs/guide/start-installation.md further details
73
 *          ],
74
 *     ],
75
 * ]
76
 * ~~~
77
 *
78
 * @since 1.0.0
79
 */
80
class Oauth2Module extends Oauth2BaseModule implements BootstrapInterface
81
{
82
    /**
83
     * Application type "web": http response.
84
     * @since 1.0.0
85
     */
86
    public const APPLICATION_TYPE_WEB = 'web';
87
    /**
88
     * Application type "console": cli response.
89
     * @since 1.0.0
90
     */
91
    public const APPLICATION_TYPE_CONSOLE = 'console';
92
    /**
93
     * Supported Application types.
94
     * @since 1.0.0
95
     */
96
    public const APPLICATION_TYPES = [
97
        self::APPLICATION_TYPE_WEB,
98
        self::APPLICATION_TYPE_CONSOLE,
99
    ];
100
101
    /**
102
     * "Authorization Server" Role, please see guide for details.
103
     * @since 1.0.0
104
     */
105
    public const SERVER_ROLE_AUTHORIZATION_SERVER = 1;
106
    /**
107
     * "Resource Server" Role, please see guide for details.
108
     * @since 1.0.0
109
     */
110
    public const SERVER_ROLE_RESOURCE_SERVER = 2;
111
112
    /**
113
     * Required settings when the server role includes Authorization Server
114
     * @since 1.0.0
115
     */
116
    protected const REQUIRED_SETTINGS_AUTHORIZATION_SERVER = [
117
        'codesEncryptionKey',
118
        'storageEncryptionKeys',
119
        'defaultStorageEncryptionKey',
120
        'privateKey',
121
        'publicKey',
122
    ];
123
124
    /**
125
     * Encrypted Models
126
     *
127
     * @since 1.0.0
128
     */
129
    protected const ENCRYPTED_MODELS = [
130
        Oauth2ClientInterface::class,
131
    ];
132
133
    /**
134
     * Required settings when the server role includes Resource Server
135
     * @since 1.0.0
136
     */
137
    protected const REQUIRED_SETTINGS_RESOURCE_SERVER = [
138
        'publicKey',
139
    ];
140
141
    /**
142
     * Prefix used in session storage of Client Authorization Requests
143
     * @since 1.0.0
144
     */
145
    protected const CLIENT_AUTHORIZATION_REQUEST_SESSION_PREFIX = 'OATH2_CLIENT_AUTHORIZATION_REQUEST_';
146
147
    /**
148
     * Controller mapping for the module. Will be parsed on `init()`.
149
     * @since 1.0.0
150
     */
151
    protected const CONTROLLER_MAP = [
152
        self::APPLICATION_TYPE_WEB => [
153
            Oauth2ServerControllerInterface::CONTROLLER_NAME => [
154
                'controller' => Oauth2ServerControllerInterface::class,
155
                'serverRole' => self::SERVER_ROLE_AUTHORIZATION_SERVER,
156
            ],
157
            Oauth2ConsentControllerInterface::CONTROLLER_NAME => [
158
                'controller' => Oauth2ConsentControllerInterface::class,
159
                'serverRole' => self::SERVER_ROLE_AUTHORIZATION_SERVER,
160
            ],
161
            Oauth2WellKnownControllerInterface::CONTROLLER_NAME => [
162
                'controller' => Oauth2WellKnownControllerInterface::class,
163
                'serverRole' => self::SERVER_ROLE_AUTHORIZATION_SERVER,
164
            ],
165
            Oauth2CertificatesControllerInterface::CONTROLLER_NAME => [
166
                'controller' => Oauth2CertificatesControllerInterface::class,
167
                'serverRole' => self::SERVER_ROLE_AUTHORIZATION_SERVER,
168
            ],
169
            Oauth2OidcControllerInterface::CONTROLLER_NAME => [
170
                'controller' => Oauth2OidcControllerInterface::class,
171
                'serverRole' => self::SERVER_ROLE_AUTHORIZATION_SERVER,
172
            ],
173
        ],
174
        self::APPLICATION_TYPE_CONSOLE => [
175
            'migrations' => [
176
                'controller' => Oauth2MigrationsController::class,
177
                'serverRole' => self::SERVER_ROLE_AUTHORIZATION_SERVER | self::SERVER_ROLE_RESOURCE_SERVER,
178
            ],
179
            'client' => [
180
                'controller' => Oauth2ClientController::class,
181
                'serverRole' => self::SERVER_ROLE_AUTHORIZATION_SERVER,
182
            ],
183
            'encryption' => [
184
                'controller' => Oauth2EncryptionController::class,
185
                'serverRole' => self::SERVER_ROLE_AUTHORIZATION_SERVER,
186
            ],
187
            'debug' => [
188
                'controller' => Oauth2DebugController::class,
189
                'serverRole' => self::SERVER_ROLE_AUTHORIZATION_SERVER | self::SERVER_ROLE_RESOURCE_SERVER,
190
            ],
191
            'pat' => [
192
                'controller' => Oauth2PersonalAccessTokenController::class,
193
                'serverRole' => self::SERVER_ROLE_AUTHORIZATION_SERVER,
194
            ]
195
        ]
196
    ];
197
198
    /**
199
     * @inheritdoc
200
     */
201
    public $controllerNamespace = __NAMESPACE__ . '\-'; // Set explicitly via $controllerMap in `init()`.
202
203
    /**
204
     * @var string|null The application type. If `null` the type will be automatically detected.
205
     * @see APPLICATION_TYPES
206
     */
207
    public $appType = null;
208
209
    /**
210
     * @var int The Oauth 2.0 Server Roles the module will perform.
211
     * @since 1.0.0
212
     */
213
    public $serverRole = self::SERVER_ROLE_AUTHORIZATION_SERVER | self::SERVER_ROLE_RESOURCE_SERVER;
214
215
    /**
216
     * @var string|null The private key for the server. Can be a string containing the key itself or point to a file.
217
     * When pointing to a file it's recommended to use an absolute path prefixed with 'file://' or start with
218
     * '@' to use a Yii path alias.
219
     * @see $privateKeyPassphrase For setting a passphrase for the private key.
220
     * @since 1.0.0
221
     */
222
    public $privateKey = null;
223
224
    /**
225
     * @var string|null The passphrase for the private key.
226
     * @since 1.0.0
227
     */
228
    public $privateKeyPassphrase = null;
229
    /**
230
     * @var string|null The public key for the server. Can be a string containing the key itself or point to a file.
231
     * When pointing to a file it's recommended to use an absolute path prefixed with 'file://' or start with
232
     * '@' to use a Yii path alias.
233
     * @since 1.0.0
234
     */
235
    public $publicKey = null;
236
237
    /**
238
     * @var string|null The encryption key for authorization and refresh codes.
239
     * @since 1.0.0
240
     */
241
    public $codesEncryptionKey = null;
242
243
    /**
244
     * @var string[]|string|null The encryption keys for storage like client secrets.
245
     * Where the array key is the name of the key, and the value the key itself. E.g.
246
     * `['2022-01-01' => 'def00000cb36fd6ed6641e0ad70805b28d....']`
247
     * If a string (instead of an array of strings) is specified it will be JSON decoded
248
     * it should contain an object where each property name is the name of the key, its value the key itself. E.g.
249
     * `{"2022-01-01": "def00000cb36fd6ed6641e0ad70805b28d...."}`
250
     *
251
     * @since 1.0.0
252
     */
253
    public $storageEncryptionKeys = null;
254
255
    /**
256
     * @var string|null The index of the default key in storageEncryptionKeys. E.g. 'myKey'.
257
     * @since 1.0.0
258
     */
259
    public $defaultStorageEncryptionKey = null;
260
261
    /**
262
     * @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...
263
     * most likely the same as the 'identityClass' of your application's User Component.
264
     * @since 1.0.0
265
     */
266
    public $identityClass = null;
267
268
    /**
269
     * @var null|string Prefix used for url rules. When `null` the module's uniqueId will be used.
270
     * @since 1.0.0
271
     */
272
    public $urlRulesPrefix = null;
273
274
    /**
275
     * @var string URL path for the access token endpoint (will be prefixed with $urlRulesPrefix).
276
     * @since 1.0.0
277
     */
278
    public $authorizePath = 'authorize';
279
280
    /**
281
     * @var string URL path for the access token endpoint (will be prefixed with $urlRulesPrefix).
282
     * @since 1.0.0
283
     */
284
    public $accessTokenPath = 'access-token';
285
286
    /**
287
     * @var string URL path for the certificates jwks endpoint (will be prefixed with $urlRulesPrefix).
288
     * @since 1.0.0
289
     */
290
    public $jwksPath = 'certs';
291
292
    /**
293
     * The URL to the page where the user can perform the client/scope authorization
294
     * (if `null` the build in page will be used).
295
     * @return string
296
     * @since 1.0.0
297
     */
298
    public $clientAuthorizationUrl = null;
299
300
    /**
301
     * @var string The URL path to the build in page where the user can authorize the client for the requested scopes
302
     * (will be prefixed with $urlRulesPrefix).
303
     * Note: This setting will only be used if $clientAuthorizationUrl is `null`.
304
     * @since 1.0.0
305
     */
306
    public $clientAuthorizationPath = 'authorize-client';
307
308
    /**
309
     * @var string The view to use in the "client authorization action" for the page where the user can
310
     * authorize the client for the requested scopes.
311
     * Note: This setting will only be used if $clientAuthorizationUrl is `null`.
312
     * @since 1.0.0
313
     */
314
    public $clientAuthorizationView = 'authorize-client';
315
316
    /**
317
     * @var string The URL path to the OpenID Connect Userinfo Action (will be prefixed with $urlRulesPrefix).
318
     * Note: This setting will only be used if $enableOpenIdConnect and $openIdConnectUserinfoEndpoint are `true`.
319
     * @since 1.0.0
320
     */
321
    public $openIdConnectUserinfoPath = 'oidc/userinfo';
322
323
    /**
324
     * @var Oauth2GrantTypeFactoryInterface[]|GrantTypeInterface[]|string[]|Oauth2GrantTypeFactoryInterface|GrantTypeInterface|string|callable
325
     * The Oauth 2.0 Grant Types that the module will serve.
326
     * @since 1.0.0
327
     */
328
    public $grantTypes = [];
329
330
    /**
331
     * @var string|null Default Time To Live for the access token, used when the Grant Type does not specify it.
332
     * When `null` default value of 1 hour is used.
333
     * The format should be a DateInterval duration (https://www.php.net/manual/en/dateinterval.construct.php).
334
     * @since 1.0.0
335
     */
336
    public $defaultAccessTokenTTL = null;
337
338
    /**
339
     * @var bool Should the resource server check for revocation of the access token.
340
     * @since 1.0.0
341
     */
342
    public $resourceServerAccessTokenRevocationValidation = true;
343
344
    /**
345
     * @var bool Enable support for OpenIdvConnect.
346
     * @since 1.0.0
347
     */
348
    public $enableOpenIdConnect = false;
349
350
    /**
351
     * @var bool Enable the .well-known/openid-configuration discovery endpoint.
352
     * @since 1.0.0
353
     */
354
    public $enableOpenIdConnectDiscovery = true;
355
356
    /**
357
     * @var bool include `grant_types_supported` in the OpenIdConnect Discovery.
358
     * Note: Since grant types can be specified per client not all clients might support all enabled grant types.
359
     * @since 1.0.0
360
     */
361
    public $openIdConnectDiscoveryIncludeSupportedGrantTypes = true;
362
363
    /**
364
     * @var string URL to include in the OpenID Connect Discovery Service of a page containing
365
     * human-readable information that developers might want or need to know when using the OpenID Provider.
366
     * @see 'service_documentation' in https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3
367
     * @since 1.0.0
368
     */
369
    public $openIdConnectDiscoveryServiceDocumentationUrl = null;
370
371
    /**
372
     * @var string|bool A string to a custom userinfo endpoint or `true` to enable the build in endpoint.
373
     * @since 1.0.0
374
     */
375
    public $openIdConnectUserinfoEndpoint = true;
376
377
    /**
378
     * Warning! Enabling this setting might introduce privacy concerns since the client could poll for the
379
     * online status of a user.
380
     *
381
     * @var bool If this setting is disabled in case of OpenID Connect Context the Access Token won't include a
382
     * Refresh Token when the 'offline_access' scope is not included in the authorization request.
383
     * In some cases it might be needed to always include a Refresh Token, in that case enable this setting and
384
     * implement the `Oauth2OidcUserSessionStatusInterface` on the User Identity model.
385
     * @since 1.0.0
386
     */
387
    public $openIdConnectIssueRefreshTokenWithoutOfflineAccessScope = false;
388
389
    /**
390
     * @var int The default option for "User Account Selection' when not specified for a client.
391
     * @since 1.0.0
392
     */
393
    public $defaultUserAccountSelection = self::USER_ACCOUNT_SELECTION_DISABLED;
394
395
    /**
396
     * @var bool|null Display exception messages that might leak server details. This could be useful for debugging.
397
     * In case of `null` (default) the YII_DEBUG constant will be used.
398
     * Warning: Should NOT be enabled in production!
399
     * @since 1.0.0
400
     */
401
    public $displayConfidentialExceptionMessages = null;
402
403
    /**
404
     * @var string|null The namespace with which migrations will be created (and by which they will be located).
405
     * Note: The specified namespace must be defined as a Yii alias (e.g. '@app').
406
     * @since 1.0.0
407
     */
408
    public $migrationsNamespace = null;
409
    /**
410
     * @var string|null Optional prefix used in the name of generated migrations
411
     * @since 1.0.0
412
     */
413
    public $migrationsPrefix = null;
414
    /**
415
     * @var string|array|int|null Sets the file ownership of generated migrations
416
     * @see \yii\helpers\BaseFileHelper::changeOwnership()
417
     * @since 1.0.0
418
     */
419
    public $migrationsFileOwnership = null;
420
    /**
421
     * @var int|null Sets the file mode of generated migrations
422
     * @see \yii\helpers\BaseFileHelper::changeOwnership()
423
     * @since 1.0.0
424
     */
425
    public $migrationsFileMode = null;
426
427
    /**
428
     * @var Oauth2AuthorizationServerInterface|null Cache for the authorization server
429
     * @since 1.0.0
430
     */
431
    protected $_authorizationServer = null;
432
433
    /**
434
     * @var Oauth2ResourceServerInterface|null Cache for the resource server
435
     * @since 1.0.0
436
     */
437
    protected $_resourceServer = null;
438
439
    /**
440
     * @var Oauth2EncryptorInterface|null Cache for the Oauth2Encryptor
441
     * @since 1.0.0
442
     */
443
    protected $_encryptor = null;
444
445
    /**
446
     * @var string|null The authorization header used when the authorization request was validated.
447
     * @since 1.0.0
448
     */
449
    protected $_oauthClaimsAuthorizationHeader = null;
450
451
    /**
452
     * @inheritDoc
453
     * @throws InvalidConfigException
454
     */
455 116
    public function init()
456
    {
457 116
        parent::init();
458
459 116
        $app = Yii::$app;
460
461 116
        if ($app instanceof WebApplication || $this->appType == static::APPLICATION_TYPE_WEB) {
462 18
            $controllerMap = static::CONTROLLER_MAP[static::APPLICATION_TYPE_WEB];
463 116
        } elseif ($app instanceof ConsoleApplication || $this->appType == static::APPLICATION_TYPE_CONSOLE) {
464 116
            $controllerMap = static::CONTROLLER_MAP[static::APPLICATION_TYPE_CONSOLE];
465
        } else {
466
            throw new InvalidConfigException(
467
                'Unable to detect application type, configure it manually by setting `$appType`.'
468
            );
469
        }
470 116
        $controllerMap = array_filter(
471
            $controllerMap,
472 116
            fn($controllerSettings) => $controllerSettings['serverRole'] & $this->serverRole
473
        );
474 116
        $this->controllerMap = ArrayHelper::getColumn($controllerMap, 'controller');
475
476 116
        if (empty($this->identityClass)) {
477 1
            throw new InvalidConfigException('$identityClass must be set.');
478 116
        } elseif (!is_a($this->identityClass, Oauth2UserInterface::class, true)) {
479 1
            throw new InvalidConfigException(
480 1
                $this->identityClass . ' must implement ' . Oauth2UserInterface::class
481
            );
482
        }
483
484 116
        foreach (static::DEFAULT_INTERFACE_IMPLEMENTATIONS as $interface => $implementation) {
485 116
            if (!Yii::$container->has($interface)) {
486 116
                Yii::$container->set($interface, $implementation);
487
            }
488
        }
489
490 116
        if (empty($this->urlRulesPrefix)) {
491 116
            $this->urlRulesPrefix = $this->uniqueId;
492
        }
493
494 116
        $this->registerTranslations();
495
    }
496
497
    /**
498
     * @inheritdoc
499
     * @throws InvalidConfigException
500
     */
501 116
    public function bootstrap($app)
502
    {
503
        if (
504 116
            $app instanceof WebApplication
505 116
            && $this->serverRole & static::SERVER_ROLE_AUTHORIZATION_SERVER
506
        ) {
507
            $rules = [
508 18
                $this->accessTokenPath => Oauth2ServerControllerInterface::CONTROLLER_NAME
509
                    . '/' . Oauth2ServerControllerInterface::ACTION_NAME_ACCESS_TOKEN,
510 18
                $this->authorizePath => Oauth2ServerControllerInterface::CONTROLLER_NAME
511
                    . '/' . Oauth2ServerControllerInterface::ACTION_NAME_AUTHORIZE,
512 18
                $this->jwksPath => Oauth2CertificatesControllerInterface::CONTROLLER_NAME
513
                    . '/' . Oauth2CertificatesControllerInterface::ACTION_NAME_JWKS,
514
            ];
515
516 18
            if (empty($this->clientAuthorizationUrl)) {
517 17
                $rules[$this->clientAuthorizationPath] = Oauth2ConsentControllerInterface::CONTROLLER_NAME
518
                    . '/' . Oauth2ConsentControllerInterface::ACTION_NAME_AUTHORIZE_CLIENT;
519
            }
520
521 18
            if ($this->enableOpenIdConnect && $this->openIdConnectUserinfoEndpoint === true) {
522 18
                $rules[$this->openIdConnectUserinfoPath] =
523 18
                    Oauth2OidcControllerInterface::CONTROLLER_NAME
524
                    . '/' . Oauth2OidcControllerInterface::ACTION_NAME_USERINFO;
525
            }
526
527 18
            $urlManager = $app->getUrlManager();
528 18
            $urlManager->addRules([
529 18
                Yii::createObject([
530
                    'class' => GroupUrlRule::class,
531 18
                    'prefix' => $this->urlRulesPrefix,
532 18
                    'routePrefix' => $this->id,
533
                    'rules' => $rules,
534
                ]),
535
            ]);
536
537 18
            if ($this->enableOpenIdConnect && $this->enableOpenIdConnectDiscovery) {
538 18
                $urlManager->addRules([
539 18
                    Yii::createObject([
540
                        'class' => UrlRule::class,
541
                        'pattern' => '.well-known/openid-configuration',
542 18
                        'route' => $this->id
543
                            . '/' . Oauth2WellKnownControllerInterface::CONTROLLER_NAME
544
                            . '/' . Oauth2WellKnownControllerInterface::ACTION_NAME_OPENID_CONFIGURATION,
545
                    ]),
546
                ]);
547
            }
548
        }
549
    }
550
551
    /**
552
     * Registers the translations for the module
553
     * @param bool $force Force the setting of the translations (even if they are already defined).
554
     * @since 1.0.0
555
     */
556 116
    public function registerTranslations($force = false)
557
    {
558 116
        if ($force || !array_key_exists('oauth2', Yii::$app->i18n->translations)) {
559 116
            Yii::$app->i18n->translations['oauth2'] = [
560
                'class' => PhpMessageSource::class,
561
                'sourceLanguage' => 'en-US',
562 116
                'basePath' => __DIR__ . DIRECTORY_SEPARATOR . 'messages',
563
                'fileMap' => [
564
                    'oauth2' => 'oauth2.php',
565
                ],
566
            ];
567
        }
568
    }
569
570
    /**
571
     * @param string $identifier
572
     * @param string $name
573
     * @param int $grantTypes
574
     * @param string|string[] $redirectURIs
575
     * @param string $type
576
     * @param string|null $secret
577
     * @param string|string[]|array[]|null $scopes
578
     * @param int|null $userId
579
     * @return Oauth2ClientInterface
580
     * @throws InvalidConfigException
581
     * @throws \yii\db\Exception
582
     */
583 4
    public function createClient(
584
        $identifier,
585
        $name,
586
        $grantTypes,
587
        $redirectURIs,
588
        $type,
589
        $secret = null,
590
        $scopes = null,
591
        $userId = null,
592
        $endUsersMayAuthorizeClient = false
593
    ) {
594 4
        if (!($this->serverRole & static::SERVER_ROLE_AUTHORIZATION_SERVER)) {
595 1
            throw new InvalidCallException('Oauth2 server role does not include authorization server.');
596
        }
597
598
        /** @var Oauth2ClientInterface $client */
599 3
        $client = Yii::createObject([
600
            'class' => Oauth2ClientInterface::class,
601
            'identifier' => $identifier,
602
            'type' => $type,
603
            'name' => $name,
604
            'redirectUri' => $redirectURIs,
605
            'grantTypes' => $grantTypes,
606
            'endUsersMayAuthorizeClient' => $endUsersMayAuthorizeClient,
607
            'clientCredentialsGrantUserId' => $userId
608
        ]);
609
610 3
        $transaction = $client::getDb()->beginTransaction();
611
612
        try {
613 3
            if ($type == Oauth2ClientInterface::TYPE_CONFIDENTIAL) {
614 3
                $client->setSecret($secret, $this->getEncryptor());
615
            }
616
617 2
            $client->persist();
618
619 2
            if (!empty($scopes)) {
620 2
                if (is_string($scopes)) {
621 2
                    $scopeIdentifiers = explode(' ', $scopes);
622
                } else {
623
                    $scopeIdentifiers = $scopes;
624
                }
625
626 2
                foreach ($scopeIdentifiers as $scopeIdentifier => $scopeConfig) {
627 2
                    if (is_string($scopeConfig)) {
628 2
                        $scopeIdentifier = $scopeConfig;
629 2
                        $scopeConfig = [];
630
                    }
631 2
                    $scope = $this->getScopeRepository()->findModelByIdentifier($scopeIdentifier);
632 2
                    if (empty($scope)) {
633 1
                        throw new InvalidArgumentException('No scope with identifier "'
634
                            . $scopeIdentifier . '" found.');
635
                    }
636
637
                    /** @var Oauth2ClientScopeInterface $clientScope */
638 1
                    $clientScope = Yii::createObject(ArrayHelper::merge(
639
                        $scopeConfig,
640
                        [
641 1
                            'class' => Oauth2ClientScopeInterface::class,
642 1
                            'client_id' => $client->getPrimaryKey(),
643 1
                            'scope_id' => $scope->getPrimaryKey(),
644
                        ]
645
                    ));
646 1
                    $clientScope->persist();
647
                }
648
            }
649
650 1
            $transaction->commit();
651 2
        } catch (\Exception $e) {
652 2
            $transaction->rollBack();
653 2
            throw $e;
654
        }
655
656 1
        return $client;
657
    }
658
659
    /**
660
     * @return CryptKey The private key of the server.
661
     * @throws InvalidConfigException
662
     * @since 1.0.0
663
     */
664 17
    public function getPrivateKey()
665
    {
666 17
        $privateKey = $this->privateKey;
667 17
        if (StringHelper::startsWith($privateKey, '@')) {
668 14
            $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

668
            $privateKey = 'file://' . /** @scrutinizer ignore-type */ Yii::getAlias($privateKey);
Loading history...
669
        }
670 17
        return Yii::createObject(CryptKey::class, [$privateKey, $this->privateKeyPassphrase]);
671
    }
672
673
    /**
674
     * @return CryptKey The public key of the server.
675
     * @throws InvalidConfigException
676
     * @since 1.0.0
677
     */
678 9
    public function getPublicKey()
679
    {
680 9
        $publicKey = $this->publicKey;
681 9
        if (StringHelper::startsWith($publicKey, '@')) {
682 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

682
            $publicKey = 'file://' . /** @scrutinizer ignore-type */ Yii::getAlias($publicKey);
Loading history...
683
        }
684 9
        return Yii::createObject(CryptKey::class, [$publicKey]);
685
    }
686
687
    /**
688
     * @return Oauth2AuthorizationServerInterface The authorization server.
689
     * @throws InvalidConfigException
690
     * @since 1.0.0
691
     */
692 22
    public function getAuthorizationServer()
693
    {
694 22
        if (!($this->serverRole & static::SERVER_ROLE_AUTHORIZATION_SERVER)) {
695 1
            throw new InvalidCallException('Oauth2 server role does not include authorization server.');
696
        }
697
698 21
        if (!$this->_authorizationServer) {
699 21
            $this->ensureProperties(static::REQUIRED_SETTINGS_AUTHORIZATION_SERVER);
700
701 16
            if (!$this->getEncryptor()->hasKey($this->defaultStorageEncryptionKey)) {
702 1
                throw new InvalidConfigException(
703 1
                    'Key "' . $this->defaultStorageEncryptionKey . '" is not set in $storageEncryptionKeys'
704
                );
705
            }
706
707
            /** @var Oauth2EncryptionKeyFactoryInterface $keyFactory */
708 14
            $keyFactory = Yii::createObject(Oauth2EncryptionKeyFactoryInterface::class);
709
            try {
710 14
                $codesEncryptionKey = $keyFactory->createFromAsciiSafeString($this->codesEncryptionKey);
711 1
            } catch (BadFormatException $e) {
712 1
                throw new InvalidConfigException(
713 1
                    '$codesEncryptionKey is malformed: ' . $e->getMessage(),
714
                    0,
715
                    $e
716
                );
717
            } catch (EnvironmentIsBrokenException $e) {
718
                throw new InvalidConfigException(
719
                    'Could not instantiate $codesEncryptionKey: ' . $e->getMessage(),
720
                    0,
721
                    $e
722
                );
723
            }
724
725 13
            $responseType = null;
726 13
            if ($this->enableOpenIdConnect) {
727 13
                $responseType = Yii::createObject(Oauth2OidcBearerTokenResponseInterface::class, [
728
                    $this,
729
                ]);
730
            }
731
732 13
            $this->_authorizationServer = Yii::createObject(Oauth2AuthorizationServerInterface::class, [
733 13
                $this->getClientRepository(),
734 13
                $this->getAccessTokenRepository(),
735 13
                $this->getScopeRepository(),
736 13
                $this->getPrivateKey(),
737
                $codesEncryptionKey,
738
                $responseType
739
            ]);
740
741 13
            if (!empty($this->grantTypes)) {
742 13
                $grantTypes = $this->grantTypes;
743
744 13
                if (is_callable($grantTypes)) {
745 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

745
                    call_user_func(/** @scrutinizer ignore-type */ $grantTypes, $this->_authorizationServer, $this);
Loading history...
746
                } else {
747 12
                    if (!is_array($grantTypes)) {
748 2
                        $grantTypes = [$grantTypes];
749
                    }
750
751 12
                    foreach ($grantTypes as $grantTypeDefinition) {
752 12
                        if ($grantTypeDefinition instanceof GrantTypeInterface) {
753 1
                            $accessTokenTTL = $this->defaultAccessTokenTTL
754
                                ? new \DateInterval($this->defaultAccessTokenTTL)
755 1
                                : null;
756 1
                            $this->_authorizationServer->enableGrantType($grantTypeDefinition, $accessTokenTTL);
757
                        } elseif (
758
                            (
759 11
                                is_numeric($grantTypeDefinition)
760 11
                                && array_key_exists($grantTypeDefinition, static::DEFAULT_GRANT_TYPE_FACTORIES)
761
                            )
762 11
                            || is_a($grantTypeDefinition, Oauth2GrantTypeFactoryInterface::class, true)
763
                        ) {
764
                            if (
765 10
                                is_numeric($grantTypeDefinition)
766 10
                                && array_key_exists($grantTypeDefinition, static::DEFAULT_GRANT_TYPE_FACTORIES)
767
                            ) {
768 10
                                $grantTypeDefinition = static::DEFAULT_GRANT_TYPE_FACTORIES[$grantTypeDefinition];
769
                            }
770
771
                            /** @var Oauth2GrantTypeFactoryInterface $factory */
772 10
                            $factory = Yii::createObject([
773
                                'class' => $grantTypeDefinition,
774
                                'module' => $this,
775
                            ]);
776 10
                            $accessTokenTTL = $factory->accessTokenTTL ?? $this->defaultAccessTokenTTL ?? null;
0 ignored issues
show
Bug introduced by
Accessing accessTokenTTL on the interface rhertogh\Yii2Oauth2Serve...antTypeFactoryInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
777 10
                            $this->_authorizationServer->enableGrantType(
778 10
                                $factory->getGrantType(),
779 10
                                $accessTokenTTL ? new \DateInterval($accessTokenTTL) : null
780
                            );
781
                        } else {
782 1
                            throw new InvalidConfigException(
783
                                'Unknown grantType '
784
                                . (
785 1
                                    is_scalar($grantTypeDefinition)
786 1
                                        ? '"' . $grantTypeDefinition . '".'
787 1
                                        : 'with data type ' . gettype($grantTypeDefinition)
788
                                )
789
                            );
790
                        }
791
                    }
792
                }
793
            }
794
        }
795
796 12
        return $this->_authorizationServer;
797
    }
798
799
    /**
800
     * @inheritDoc
801
     * @throws InvalidConfigException
802
     */
803 5
    public function getOidcScopeCollection()
804
    {
805 5
        if ($this->_oidcScopeCollection === null) {
806 5
            $openIdConnectScopes = $this->getOpenIdConnectScopes();
807 5
            if ($openIdConnectScopes instanceof Oauth2OidcScopeCollectionInterface) {
808 1
                $this->_oidcScopeCollection = $openIdConnectScopes;
809 4
            } elseif (is_callable($openIdConnectScopes)) {
810 1
                $this->_oidcScopeCollection = call_user_func($openIdConnectScopes, $this);
811 1
                if (!($this->_oidcScopeCollection instanceof Oauth2OidcScopeCollectionInterface)) {
812
                    throw new InvalidConfigException(
813
                        '$openIdConnectScopes must return an instance of '
814
                            . Oauth2OidcScopeCollectionInterface::class
815
                    );
816
                }
817 3
            } elseif (is_array($openIdConnectScopes) || is_string($openIdConnectScopes)) {
818 2
                $this->_oidcScopeCollection = Yii::createObject([
819
                    'class' => Oauth2OidcScopeCollectionInterface::class,
820 2
                    'oidcScopes' => (array)$openIdConnectScopes,
821
                ]);
822
            } else {
823 1
                throw new InvalidConfigException(
824
                    '$openIdConnectScopes must be a callable, array, string or '
825
                        . Oauth2OidcScopeCollectionInterface::class
826
                );
827
            }
828
        }
829
830 4
        return $this->_oidcScopeCollection;
831
    }
832
833
    /**
834
     * @return Oauth2ResourceServerInterface The resource server.
835
     * @throws InvalidConfigException
836
     * @since 1.0.0
837
     */
838 7
    public function getResourceServer()
839
    {
840 7
        if (!($this->serverRole & static::SERVER_ROLE_RESOURCE_SERVER)) {
841 1
            throw new InvalidCallException('Oauth2 server role does not include resource server.');
842
        }
843
844 6
        if (!$this->_resourceServer) {
845 6
            $this->ensureProperties(static::REQUIRED_SETTINGS_RESOURCE_SERVER);
846
847 5
            $accessTokenRepository = $this->getAccessTokenRepository()
848 5
                ->setRevocationValidation($this->resourceServerAccessTokenRevocationValidation);
849
850 5
            $this->_resourceServer = Yii::createObject(Oauth2ResourceServerInterface::class, [
851
                $accessTokenRepository,
852 5
                $this->getPublicKey(),
853
            ]);
854
        }
855
856 5
        return $this->_resourceServer;
857
    }
858
859
    /**
860
     * @return Oauth2EncryptorInterface The data encryptor for the module.
861
     * @throws InvalidConfigException
862
     * @since 1.0.0
863
     */
864 21
    public function getEncryptor()
865
    {
866 21
        if (!$this->_encryptor) {
867 21
            $this->_encryptor = Yii::createObject([
868
                'class' => Oauth2EncryptorInterface::class,
869 21
                'keys' => $this->storageEncryptionKeys,
870 21
                'defaultKeyName' => $this->defaultStorageEncryptionKey,
871
            ]);
872
        }
873
874 20
        return $this->_encryptor;
875
    }
876
877
    /**
878
     * @param string|null $newKeyName
879
     * @return array
880
     * @throws InvalidConfigException
881
     */
882 1
    public function rotateStorageEncryptionKeys($newKeyName = null)
883
    {
884 1
        $encryptor = $this->getEncryptor();
885
886 1
        $result = [];
887 1
        foreach (static::ENCRYPTED_MODELS as $modelInterface) {
888 1
            $modelClass = DiHelper::getValidatedClassName($modelInterface);
889 1
            if (!is_a($modelClass, Oauth2EncryptedStorageInterface::class, true)) {
890
                throw new InvalidConfigException($modelInterface . ' must implement '
891
                    . Oauth2EncryptedStorageInterface::class);
892
            }
893 1
            $result[$modelClass] = $modelClass::rotateStorageEncryptionKeys($encryptor, $newKeyName);
894
        }
895
896 1
        return $result;
897
    }
898
899
    /**
900
     * @return array
901
     * @throws InvalidConfigException
902
     */
903
    public function getStorageEncryptionKeyUsage()
904
    {
905
        $encryptor = $this->getEncryptor();
906
907
        $result = [];
908
        foreach (static::ENCRYPTED_MODELS as $modelInterface) {
909
            $modelClass = DiHelper::getValidatedClassName($modelInterface);
910
            if (!is_a($modelClass, Oauth2EncryptedStorageInterface::class, true)) {
911
                throw new InvalidConfigException($modelInterface . ' must implement '
912
                    . Oauth2EncryptedStorageInterface::class);
913
            }
914
915
            $result[$modelClass] = $modelClass::getUsedStorageEncryptionKeys($encryptor);
916
        }
917
918
        return $result;
919
    }
920
921
    /**
922
     * Generates a redirect Response to the client authorization page where the user is prompted to authorize the
923
     * client and requested scope.
924
     * @param Oauth2ClientAuthorizationRequestInterface $clientAuthorizationRequest
925
     * @return Response
926
     * @since 1.0.0
927
     */
928 5
    public function generateClientAuthReqRedirectResponse($clientAuthorizationRequest)
929
    {
930 5
        $this->setClientAuthReqSession($clientAuthorizationRequest);
931 5
        if (!empty($this->clientAuthorizationUrl)) {
932 1
            $url = $this->clientAuthorizationUrl;
933
        } else {
934 4
            $url = $this->uniqueId
935
                . '/' . Oauth2ConsentControllerInterface::CONTROLLER_NAME
936
                . '/' . Oauth2ConsentControllerInterface::ACTION_NAME_AUTHORIZE_CLIENT;
937
        }
938 5
        return Yii::$app->response->redirect([
939
            $url,
940 5
            'clientAuthorizationRequestId' => $clientAuthorizationRequest->getRequestId(),
941
        ]);
942
    }
943
944
    /**
945
     * Get a previously stored Client Authorization Request from the session.
946
     * @param string $requestId
947
     * @return Oauth2ClientAuthorizationRequestInterface|null
948
     * @since 1.0.0
949
     */
950 5
    public function getClientAuthReqSession($requestId)
951
    {
952 5
        if (empty($requestId)) {
953
            return null;
954
        }
955 5
        $key = static::CLIENT_AUTHORIZATION_REQUEST_SESSION_PREFIX . $requestId;
956 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

956
        /** @scrutinizer ignore-call */ 
957
        $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...
957 5
        if (!($clientAuthorizationRequest instanceof Oauth2ClientAuthorizationRequestInterface)) {
958 2
            if (!empty($clientAuthorizationRequest)) {
959 1
                Yii::warning(
960 1
                    'Found a ClientAuthorizationRequestSession with key "' . $key
961
                        . '", but it\'s not a ' . Oauth2ClientAuthorizationRequestInterface::class
962
                );
963
            }
964 2
            return null;
965
        }
966 5
        if ($clientAuthorizationRequest->getRequestId() !== $requestId) {
967 1
            Yii::warning(
968 1
                'Found a ClientAuthorizationRequestSession with key "' . $key
969
                    . '", but it\'s request id does not match "' . $requestId . '".'
970
            );
971 1
            return null;
972
        }
973 5
        $clientAuthorizationRequest->setModule($this);
974
975 5
        return $clientAuthorizationRequest;
976
    }
977
978
    /**
979
     * Stores the Client Authorization Request in the session.
980
     * @param Oauth2ClientAuthorizationRequestInterface $clientAuthorizationRequest
981
     * @since 1.0.0
982
     */
983 8
    public function setClientAuthReqSession($clientAuthorizationRequest)
984
    {
985 8
        $requestId = $clientAuthorizationRequest->getRequestId();
986 8
        if (empty($requestId)) {
987 1
            throw new InvalidArgumentException('$scopeAuthorization must return a request id.');
988
        }
989 7
        $key = static::CLIENT_AUTHORIZATION_REQUEST_SESSION_PREFIX . $requestId;
990 7
        Yii::$app->session->set($key, $clientAuthorizationRequest);
991
    }
992
993
    /**
994
     * Stores whether the user was authenticated during the completion of the Client Authorization Request.
995
     * @param string $clientAuthorizationRequestId
996
     * @param bool $authenticatedDuringRequest
997
     * @since 1.0.0
998
     */
999
    public function setUserAuthenticatedDuringClientAuthRequest(
1000
        $clientAuthorizationRequestId,
1001
        $authenticatedDuringRequest
1002
    ) {
1003
        $clientAuthorizationRequest = $this->getClientAuthReqSession($clientAuthorizationRequestId);
1004
        if ($clientAuthorizationRequest) {
1005
            $clientAuthorizationRequest->setUserAuthenticatedDuringRequest($authenticatedDuringRequest);
1006
            $this->setClientAuthReqSession($clientAuthorizationRequest);
1007
        }
1008
    }
1009
1010
    /**
1011
     * Stores the user identity selected during the completion of the Client Authorization Request.
1012
     * @param string $clientAuthorizationRequestId
1013
     * @param Oauth2UserInterface $userIdentity
1014
     * @since 1.0.0
1015
     */
1016
    public function setClientAuthRequestUserIdentity($clientAuthorizationRequestId, $userIdentity)
1017
    {
1018
        $clientAuthorizationRequest = $this->getClientAuthReqSession($clientAuthorizationRequestId);
1019
        if ($clientAuthorizationRequest) {
1020
            $clientAuthorizationRequest->setUserIdentity($userIdentity);
1021
            $this->setClientAuthReqSession($clientAuthorizationRequest);
1022
        }
1023
    }
1024
1025
    /**
1026
     * Clears a Client Authorization Request from the session storage.
1027
     * @param string $requestId
1028
     * @since 1.0.0
1029
     */
1030 2
    public function removeClientAuthReqSession($requestId)
1031
    {
1032 2
        if (empty($requestId)) {
1033 1
            throw new InvalidArgumentException('$requestId can not be empty.');
1034
        }
1035 1
        $key = static::CLIENT_AUTHORIZATION_REQUEST_SESSION_PREFIX . $requestId;
1036 1
        Yii::$app->session->remove($key);
1037
    }
1038
1039
    /**
1040
     * Generates a redirect Response when the Client Authorization Request is completed.
1041
     * @param Oauth2ClientAuthorizationRequestInterface $clientAuthorizationRequest
1042
     * @return Response
1043
     * @since 1.0.0
1044
     */
1045 1
    public function generateClientAuthReqCompledRedirectResponse($clientAuthorizationRequest)
1046
    {
1047 1
        $clientAuthorizationRequest->processAuthorization();
1048 1
        $this->setClientAuthReqSession($clientAuthorizationRequest);
1049 1
        return Yii::$app->response->redirect($clientAuthorizationRequest->getAuthorizationRequestUrl());
1050
    }
1051
1052
    /**
1053
     * @return IdentityInterface|Oauth2UserInterface|Oauth2OidcUserInterface|null
1054
     * @throws InvalidConfigException
1055
     * @since 1.0.0
1056
     */
1057 5
    public function getUserIdentity()
1058
    {
1059 5
        $user = Yii::$app->user->identity;
1060 5
        if (!empty($user) && !($user instanceof Oauth2UserInterface)) {
1061 1
            throw new InvalidConfigException(
1062 1
                'Yii::$app->user->identity (currently ' . get_class($user)
1063
                    . ') must implement ' . Oauth2UserInterface::class
1064
            );
1065
        }
1066 4
        return $user;
1067
    }
1068
1069
    /**
1070
     * Validates a bearer token authenticated request. Note: this method does not return a result but will throw
1071
     * an exception when the authentication fails.
1072
     * @throws InvalidConfigException
1073
     * @throws Oauth2ServerException
1074
     * @since 1.0.0
1075
     */
1076 3
    public function validateAuthenticatedRequest()
1077
    {
1078 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

1078
        $psr7Request = Psr7Helper::yiiToPsr7Request(/** @scrutinizer ignore-type */ Yii::$app->request);
Loading history...
1079
1080 3
        $psr7Request = $this->getResourceServer()->validateAuthenticatedRequest($psr7Request);
1081
1082 3
        $this->_oauthClaims = $psr7Request->getAttributes();
1083 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...
1084
    }
1085
1086
    /**
1087
     * Find a user identity bases on an access token.
1088
     * Note: validateAuthenticatedRequest() must be called before this method is called.
1089
     * @param string $token
1090
     * @param string $type
1091
     * @return Oauth2UserInterface|null
1092
     * @throws InvalidConfigException
1093
     * @throws Oauth2ServerException
1094
     * @see validateAuthenticatedRequest()
1095
     * @since 1.0.0
1096
     */
1097 4
    public function findIdentityByAccessToken($token, $type)
1098
    {
1099 4
        if (!is_a($type, Oauth2HttpBearerAuthInterface::class, true)) {
1100 1
            throw new InvalidCallException($type . ' must implement ' . Oauth2HttpBearerAuthInterface::class);
1101
        }
1102
1103
        if (
1104 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

1104
            !preg_match('/^Bearer\s+(.*?)$/', /** @scrutinizer ignore-type */ $this->_oauthClaimsAuthorizationHeader, $matches)
Loading history...
1105 3
            || !Yii::$app->security->compareString($matches[1], $token)
1106
        ) {
1107 1
            throw new InvalidCallException(
1108
                'validateAuthenticatedRequest() must be called before findIdentityByAccessToken().'
1109
            );
1110
        }
1111
1112 2
        $userId = $this->getRequestOauthUserId();
1113 2
        if (empty($userId)) {
1114 1
            return null;
1115
        }
1116
1117 1
        return $this->identityClass::findIdentity($userId);
1118
    }
1119
1120
    /**
1121
     * @param $clientIdentifier
1122
     * @param $userIdentifier
1123
     * @param Oauth2ScopeInterface[]|string[]|string|null $scope
1124
     * @param string|true|null $clientSecret
1125
     * @return mixed|null
1126
     */
1127
    public function generatePersonalAccessToken($clientIdentifier, $userIdentifier, $scope = null, $clientSecret = null)
1128
    {
1129
        if (is_array($scope)) {
1130
            $scopeIdentifiers = [];
1131
            foreach ($scope as $scopeItem) {
1132
                if (is_string($scopeItem)) {
1133
                    $scopeIdentifiers[] = $scopeItem;
1134
                } elseif ($scopeItem instanceof Oauth2ScopeInterface) {
1135
                    $scopeIdentifiers[] = $scopeItem->getIdentifier();
1136
                }
1137
            }
1138
            $scope = implode(' ', $scopeIdentifiers);
1139
        }
1140
1141
        if ($clientSecret === true) {
1142
            /** @var Oauth2ClientInterface $client */
1143
            $client = $this->getClientRepository()->findModelByIdentifier($clientIdentifier);
1144
            if ($client && $client->isConfidential()) {
1145
                $clientSecret = $client->getDecryptedSecret($this->getEncryptor());
1146
            }
1147
        }
1148
1149
        $request = (new Psr7ServerRequest('POST', ''))->withParsedBody([
1150
            'grant_type' => static::GRANT_TYPE_IDENTIFIER_PERSONAL_ACCESS_TOKEN,
1151
            'client_id' => $clientIdentifier,
1152
            'client_secret' => $clientSecret,
1153
            'user_id' => $userIdentifier,
1154
            'scope' => $scope,
1155
        ]);
1156
1157
        $response = Json::decode(
1158
            $this->getAuthorizationServer()
1159
                ->respondToAccessTokenRequest(
1160
                    $request,
1161
                    new Psr7Response()
1162
                )
1163
                ->getBody()
1164
                ->__toString()
1165
        );
1166
1167
        return $response;
1168
    }
1169
1170
    /**
1171
     * @inheritDoc
1172
     */
1173 5
    protected function getRequestOauthClaim($attribute, $default = null)
1174
    {
1175 5
        if (empty($this->_oauthClaimsAuthorizationHeader)) {
1176
            // User authorization was not processed by Oauth2Module.
1177 1
            return $default;
1178
        }
1179 4
        if (Yii::$app->request->getHeaders()->get('Authorization') !== $this->_oauthClaimsAuthorizationHeader) {
1180 1
            throw new InvalidCallException(
1181
                'App Request Authorization header does not match the processed Oauth header.'
1182
            );
1183
        }
1184 3
        return $this->_oauthClaims[$attribute] ?? $default;
1185
    }
1186
1187
    /**
1188
     * Helper function to ensure the required properties are configured for the module.
1189
     * @param string[] $properties
1190
     * @throws InvalidConfigException
1191
     * @since 1.0.0
1192
     */
1193 27
    protected function ensureProperties($properties)
1194
    {
1195 27
        foreach ($properties as $property) {
1196 27
            if (empty($this->$property)) {
1197 6
                throw new InvalidConfigException( __CLASS__ . '::$' . $property . ' must be set.');
1198
            }
1199
        }
1200
    }
1201
}
1202