Passed
Push — master ( e9591a...c0a685 )
by Rutger
13:20
created

Oauth2Module   F

Complexity

Total Complexity 118

Size/Duplication

Total Lines 1163
Duplicated Lines 0 %

Test Coverage

Coverage 91.89%

Importance

Changes 7
Bugs 0 Features 0
Metric Value
wmc 118
eloc 446
c 7
b 0
f 0
dl 0
loc 1163
ccs 374
cts 407
cp 0.9189
rs 2

25 Methods

Rating   Name   Duplication   Size   Complexity  
A removeClientAuthReqSession() 0 7 2
A validateAuthenticatedRequest() 0 8 1
A getPrivateKey() 0 7 2
A getPublicKey() 0 7 2
A setUserAuthenticatedDuringClientAuthRequest() 0 8 2
A setClientAuthRequestUserIdentity() 0 6 2
A generateClientAuthReqCompledRedirectResponse() 0 5 1
A setClientAuthReqSession() 0 8 2
A rotateStorageEncryptionKeys() 0 15 3
A ensureProperties() 0 5 3
B bootstrap() 0 44 8
A getRequestOauthClaim() 0 12 3
D getAuthorizationServer() 0 105 20
A generateClientAuthReqRedirectResponse() 0 13 2
A getUserIdentity() 0 10 3
B generatePersonalAccessToken() 0 43 8
A registerTranslations() 0 9 3
A getResourceServer() 0 19 3
F createClient() 0 104 16
A getClientAuthReqSession() 0 26 5
A getEncryptor() 0 11 2
B init() 0 40 10
B getOidcScopeCollection() 0 28 7
A getStorageEncryptionKeyUsage() 0 16 3
A findIdentityByAccessToken() 0 21 5

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

702
            $privateKey = 'file://' . /** @scrutinizer ignore-type */ Yii::getAlias($privateKey);
Loading history...
703
        }
704 20
        return Yii::createObject(CryptKey::class, [$privateKey, $this->privateKeyPassphrase]);
705
    }
706
707
    /**
708
     * @return CryptKey The public key of the server.
709
     * @throws InvalidConfigException
710
     * @since 1.0.0
711
     */
712 9
    public function getPublicKey()
713
    {
714 9
        $publicKey = $this->publicKey;
715 9
        if (StringHelper::startsWith($publicKey, '@')) {
716 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

716
            $publicKey = 'file://' . /** @scrutinizer ignore-type */ Yii::getAlias($publicKey);
Loading history...
717
        }
718 9
        return Yii::createObject(CryptKey::class, [$publicKey]);
719
    }
720
721
    /**
722
     * @return Oauth2AuthorizationServerInterface The authorization server.
723
     * @throws InvalidConfigException
724
     * @since 1.0.0
725
     */
726 25
    public function getAuthorizationServer()
727
    {
728 25
        if (!($this->serverRole & static::SERVER_ROLE_AUTHORIZATION_SERVER)) {
729 1
            throw new InvalidCallException('Oauth2 server role does not include authorization server.');
730
        }
731
732 24
        if (!$this->_authorizationServer) {
733 24
            $this->ensureProperties(static::REQUIRED_SETTINGS_AUTHORIZATION_SERVER);
734
735 19
            if (!$this->getEncryptor()->hasKey($this->defaultStorageEncryptionKey)) {
736 1
                throw new InvalidConfigException(
737 1
                    'Key "' . $this->defaultStorageEncryptionKey . '" is not set in $storageEncryptionKeys'
738 1
                );
739
            }
740
741
            /** @var Oauth2EncryptionKeyFactoryInterface $keyFactory */
742 17
            $keyFactory = Yii::createObject(Oauth2EncryptionKeyFactoryInterface::class);
743
            try {
744 17
                $codesEncryptionKey = $keyFactory->createFromAsciiSafeString($this->codesEncryptionKey);
745 1
            } catch (BadFormatException $e) {
746 1
                throw new InvalidConfigException(
747 1
                    '$codesEncryptionKey is malformed: ' . $e->getMessage(),
748 1
                    0,
749 1
                    $e
750 1
                );
751
            } catch (EnvironmentIsBrokenException $e) {
752
                throw new InvalidConfigException(
753
                    'Could not instantiate $codesEncryptionKey: ' . $e->getMessage(),
754
                    0,
755
                    $e
756
                );
757
            }
758
759 16
            $responseType = null;
760 16
            if ($this->enableOpenIdConnect) {
761 16
                $responseType = Yii::createObject(Oauth2OidcBearerTokenResponseInterface::class, [
762 16
                    $this,
763 16
                ]);
764
            }
765
766 16
            $this->_authorizationServer = Yii::createObject(Oauth2AuthorizationServerInterface::class, [
767 16
                $this->getClientRepository(),
768 16
                $this->getAccessTokenRepository(),
769 16
                $this->getScopeRepository(),
770 16
                $this->getPrivateKey(),
771 16
                $codesEncryptionKey,
772 16
                $responseType
773 16
            ]);
774
775 16
            if (!empty($this->grantTypes)) {
776 16
                $grantTypes = $this->grantTypes;
777
778 16
                if (is_callable($grantTypes)) {
779 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

779
                    call_user_func(/** @scrutinizer ignore-type */ $grantTypes, $this->_authorizationServer, $this);
Loading history...
780
                } else {
781 15
                    if (!is_array($grantTypes)) {
782 2
                        $grantTypes = [$grantTypes];
783
                    }
784
785 15
                    foreach ($grantTypes as $grantTypeDefinition) {
786 15
                        if ($grantTypeDefinition instanceof GrantTypeInterface) {
787 1
                            $accessTokenTTL = $this->defaultAccessTokenTTL
788
                                ? new \DateInterval($this->defaultAccessTokenTTL)
789 1
                                : null;
790 1
                            $this->_authorizationServer->enableGrantType($grantTypeDefinition, $accessTokenTTL);
791
                        } elseif (
792
                            (
793 14
                                is_numeric($grantTypeDefinition)
794 14
                                && array_key_exists($grantTypeDefinition, static::DEFAULT_GRANT_TYPE_FACTORIES)
795
                            )
796 14
                            || is_a($grantTypeDefinition, Oauth2GrantTypeFactoryInterface::class, true)
797
                        ) {
798
                            if (
799 13
                                is_numeric($grantTypeDefinition)
800 13
                                && array_key_exists($grantTypeDefinition, static::DEFAULT_GRANT_TYPE_FACTORIES)
801
                            ) {
802 13
                                $grantTypeDefinition = static::DEFAULT_GRANT_TYPE_FACTORIES[$grantTypeDefinition];
803
                            }
804
805
                            /** @var Oauth2GrantTypeFactoryInterface $factory */
806 13
                            $factory = Yii::createObject([
807 13
                                'class' => $grantTypeDefinition,
808 13
                                'module' => $this,
809 13
                            ]);
810 13
                            $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...
811 13
                            $this->_authorizationServer->enableGrantType(
812 13
                                $factory->getGrantType(),
813 13
                                $accessTokenTTL ? new \DateInterval($accessTokenTTL) : null
814 13
                            );
815
                        } else {
816 1
                            throw new InvalidConfigException(
817 1
                                'Unknown grantType '
818 1
                                . (
819 1
                                    is_scalar($grantTypeDefinition)
820 1
                                        ? '"' . $grantTypeDefinition . '".'
821 1
                                        : 'with data type ' . gettype($grantTypeDefinition)
822 1
                                )
823 1
                            );
824
                        }
825
                    }
826
                }
827
            }
828
        }
829
830 15
        return $this->_authorizationServer;
831
    }
832
833
    /**
834
     * @inheritDoc
835
     * @throws InvalidConfigException
836
     */
837 5
    public function getOidcScopeCollection()
838
    {
839 5
        if ($this->_oidcScopeCollection === null) {
840 5
            $openIdConnectScopes = $this->getOpenIdConnectScopes();
841 5
            if ($openIdConnectScopes instanceof Oauth2OidcScopeCollectionInterface) {
842 1
                $this->_oidcScopeCollection = $openIdConnectScopes;
843 4
            } elseif (is_callable($openIdConnectScopes)) {
844 1
                $this->_oidcScopeCollection = call_user_func($openIdConnectScopes, $this);
845 1
                if (!($this->_oidcScopeCollection instanceof Oauth2OidcScopeCollectionInterface)) {
846 1
                    throw new InvalidConfigException(
847 1
                        '$openIdConnectScopes must return an instance of '
848 1
                            . Oauth2OidcScopeCollectionInterface::class
849 1
                    );
850
                }
851 3
            } elseif (is_array($openIdConnectScopes) || is_string($openIdConnectScopes)) {
852 2
                $this->_oidcScopeCollection = Yii::createObject([
853 2
                    'class' => Oauth2OidcScopeCollectionInterface::class,
854 2
                    'oidcScopes' => (array)$openIdConnectScopes,
855 2
                ]);
856
            } else {
857 1
                throw new InvalidConfigException(
858 1
                    '$openIdConnectScopes must be a callable, array, string or '
859 1
                        . Oauth2OidcScopeCollectionInterface::class
860 1
                );
861
            }
862
        }
863
864 4
        return $this->_oidcScopeCollection;
865
    }
866
867
    /**
868
     * @return Oauth2ResourceServerInterface The resource server.
869
     * @throws InvalidConfigException
870
     * @since 1.0.0
871
     */
872 7
    public function getResourceServer()
873
    {
874 7
        if (!($this->serverRole & static::SERVER_ROLE_RESOURCE_SERVER)) {
875 1
            throw new InvalidCallException('Oauth2 server role does not include resource server.');
876
        }
877
878 6
        if (!$this->_resourceServer) {
879 6
            $this->ensureProperties(static::REQUIRED_SETTINGS_RESOURCE_SERVER);
880
881 5
            $accessTokenRepository = $this->getAccessTokenRepository()
882 5
                ->setRevocationValidation($this->resourceServerAccessTokenRevocationValidation);
883
884 5
            $this->_resourceServer = Yii::createObject(Oauth2ResourceServerInterface::class, [
885 5
                $accessTokenRepository,
886 5
                $this->getPublicKey(),
887 5
            ]);
888
        }
889
890 5
        return $this->_resourceServer;
891
    }
892
893
    /**
894
     * @return Oauth2EncryptorInterface The data encryptor for the module.
895
     * @throws InvalidConfigException
896
     * @since 1.0.0
897
     */
898 30
    public function getEncryptor()
899
    {
900 30
        if (!$this->_encryptor) {
901 30
            $this->_encryptor = Yii::createObject([
902 30
                'class' => Oauth2EncryptorInterface::class,
903 30
                'keys' => $this->storageEncryptionKeys,
904 30
                'defaultKeyName' => $this->defaultStorageEncryptionKey,
905 30
            ]);
906
        }
907
908 29
        return $this->_encryptor;
909
    }
910
911
    /**
912
     * @param string|null $newKeyName
913
     * @return array
914
     * @throws InvalidConfigException
915
     */
916 1
    public function rotateStorageEncryptionKeys($newKeyName = null)
917
    {
918 1
        $encryptor = $this->getEncryptor();
919
920 1
        $result = [];
921 1
        foreach (static::ENCRYPTED_MODELS as $modelInterface) {
922 1
            $modelClass = DiHelper::getValidatedClassName($modelInterface);
923 1
            if (!is_a($modelClass, Oauth2EncryptedStorageInterface::class, true)) {
924
                throw new InvalidConfigException($modelInterface . ' must implement '
925
                    . Oauth2EncryptedStorageInterface::class);
926
            }
927 1
            $result[$modelClass] = $modelClass::rotateStorageEncryptionKeys($encryptor, $newKeyName);
928
        }
929
930 1
        return $result;
931
    }
932
933
    /**
934
     * @return array
935
     * @throws InvalidConfigException
936
     */
937
    public function getStorageEncryptionKeyUsage()
938
    {
939
        $encryptor = $this->getEncryptor();
940
941
        $result = [];
942
        foreach (static::ENCRYPTED_MODELS as $modelInterface) {
943
            $modelClass = DiHelper::getValidatedClassName($modelInterface);
944
            if (!is_a($modelClass, Oauth2EncryptedStorageInterface::class, true)) {
945
                throw new InvalidConfigException($modelInterface . ' must implement '
946
                    . Oauth2EncryptedStorageInterface::class);
947
            }
948
949
            $result[$modelClass] = $modelClass::getUsedStorageEncryptionKeys($encryptor);
950
        }
951
952
        return $result;
953
    }
954
955
    /**
956
     * Generates a redirect Response to the client authorization page where the user is prompted to authorize the
957
     * client and requested scope.
958
     * @param Oauth2ClientAuthorizationRequestInterface $clientAuthorizationRequest
959
     * @return Response
960
     * @since 1.0.0
961
     */
962 5
    public function generateClientAuthReqRedirectResponse($clientAuthorizationRequest)
963
    {
964 5
        $this->setClientAuthReqSession($clientAuthorizationRequest);
965 5
        if (!empty($this->clientAuthorizationUrl)) {
966 1
            $url = $this->clientAuthorizationUrl;
967
        } else {
968 4
            $url = $this->uniqueId
969 4
                . '/' . Oauth2ConsentControllerInterface::CONTROLLER_NAME
970 4
                . '/' . Oauth2ConsentControllerInterface::ACTION_NAME_AUTHORIZE_CLIENT;
971
        }
972 5
        return Yii::$app->response->redirect([
973 5
            $url,
974 5
            'clientAuthorizationRequestId' => $clientAuthorizationRequest->getRequestId(),
975 5
        ]);
976
    }
977
978
    /**
979
     * Get a previously stored Client Authorization Request from the session.
980
     * @param string $requestId
981
     * @return Oauth2ClientAuthorizationRequestInterface|null
982
     * @since 1.0.0
983
     */
984 5
    public function getClientAuthReqSession($requestId)
985
    {
986 5
        if (empty($requestId)) {
987
            return null;
988
        }
989 5
        $key = static::CLIENT_AUTHORIZATION_REQUEST_SESSION_PREFIX . $requestId;
990 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

990
        /** @scrutinizer ignore-call */ 
991
        $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...
991 5
        if (!($clientAuthorizationRequest instanceof Oauth2ClientAuthorizationRequestInterface)) {
992 2
            if (!empty($clientAuthorizationRequest)) {
993 1
                Yii::warning(
994 1
                    'Found a ClientAuthorizationRequestSession with key "' . $key
995 1
                        . '", but it\'s not a ' . Oauth2ClientAuthorizationRequestInterface::class
996 1
                );
997
            }
998 2
            return null;
999
        }
1000 5
        if ($clientAuthorizationRequest->getRequestId() !== $requestId) {
1001 1
            Yii::warning(
1002 1
                'Found a ClientAuthorizationRequestSession with key "' . $key
1003 1
                    . '", but its request id does not match "' . $requestId . '".'
1004 1
            );
1005 1
            return null;
1006
        }
1007 5
        $clientAuthorizationRequest->setModule($this);
1008
1009 5
        return $clientAuthorizationRequest;
1010
    }
1011
1012
    /**
1013
     * Stores the Client Authorization Request in the session.
1014
     * @param Oauth2ClientAuthorizationRequestInterface $clientAuthorizationRequest
1015
     * @since 1.0.0
1016
     */
1017 8
    public function setClientAuthReqSession($clientAuthorizationRequest)
1018
    {
1019 8
        $requestId = $clientAuthorizationRequest->getRequestId();
1020 8
        if (empty($requestId)) {
1021 1
            throw new InvalidArgumentException('$scopeAuthorization must return a request id.');
1022
        }
1023 7
        $key = static::CLIENT_AUTHORIZATION_REQUEST_SESSION_PREFIX . $requestId;
1024 7
        Yii::$app->session->set($key, $clientAuthorizationRequest);
1025
    }
1026
1027
    /**
1028
     * Stores whether the user was authenticated during the completion of the Client Authorization Request.
1029
     * @param string $clientAuthorizationRequestId
1030
     * @param bool $authenticatedDuringRequest
1031
     * @since 1.0.0
1032
     */
1033
    public function setUserAuthenticatedDuringClientAuthRequest(
1034
        $clientAuthorizationRequestId,
1035
        $authenticatedDuringRequest
1036
    ) {
1037
        $clientAuthorizationRequest = $this->getClientAuthReqSession($clientAuthorizationRequestId);
1038
        if ($clientAuthorizationRequest) {
1039
            $clientAuthorizationRequest->setUserAuthenticatedDuringRequest($authenticatedDuringRequest);
1040
            $this->setClientAuthReqSession($clientAuthorizationRequest);
1041
        }
1042
    }
1043
1044
    /**
1045
     * Stores the user identity selected during the completion of the Client Authorization Request.
1046
     * @param string $clientAuthorizationRequestId
1047
     * @param Oauth2UserInterface $userIdentity
1048
     * @since 1.0.0
1049
     */
1050
    public function setClientAuthRequestUserIdentity($clientAuthorizationRequestId, $userIdentity)
1051
    {
1052
        $clientAuthorizationRequest = $this->getClientAuthReqSession($clientAuthorizationRequestId);
1053
        if ($clientAuthorizationRequest) {
1054
            $clientAuthorizationRequest->setUserIdentity($userIdentity);
1055
            $this->setClientAuthReqSession($clientAuthorizationRequest);
1056
        }
1057
    }
1058
1059
    /**
1060
     * Clears a Client Authorization Request from the session storage.
1061
     * @param string $requestId
1062
     * @since 1.0.0
1063
     */
1064 2
    public function removeClientAuthReqSession($requestId)
1065
    {
1066 2
        if (empty($requestId)) {
1067 1
            throw new InvalidArgumentException('$requestId can not be empty.');
1068
        }
1069 1
        $key = static::CLIENT_AUTHORIZATION_REQUEST_SESSION_PREFIX . $requestId;
1070 1
        Yii::$app->session->remove($key);
1071
    }
1072
1073
    /**
1074
     * Generates a redirect Response when the Client Authorization Request is completed.
1075
     * @param Oauth2ClientAuthorizationRequestInterface $clientAuthorizationRequest
1076
     * @return Response
1077
     * @since 1.0.0
1078
     */
1079 1
    public function generateClientAuthReqCompledRedirectResponse($clientAuthorizationRequest)
1080
    {
1081 1
        $clientAuthorizationRequest->processAuthorization();
1082 1
        $this->setClientAuthReqSession($clientAuthorizationRequest);
1083 1
        return Yii::$app->response->redirect($clientAuthorizationRequest->getAuthorizationRequestUrl());
1084
    }
1085
1086
    /**
1087
     * @return IdentityInterface|Oauth2UserInterface|Oauth2OidcUserInterface|null
1088
     * @throws InvalidConfigException
1089
     * @since 1.0.0
1090
     */
1091 5
    public function getUserIdentity()
1092
    {
1093 5
        $user = Yii::$app->user->identity;
1094 5
        if (!empty($user) && !($user instanceof Oauth2UserInterface)) {
1095 1
            throw new InvalidConfigException(
1096 1
                'Yii::$app->user->identity (currently ' . get_class($user)
1097 1
                    . ') must implement ' . Oauth2UserInterface::class
1098 1
            );
1099
        }
1100 4
        return $user;
1101
    }
1102
1103
    /**
1104
     * Validates a bearer token authenticated request. Note: this method does not return a result but will throw
1105
     * an exception when the authentication fails.
1106
     * @throws InvalidConfigException
1107
     * @throws Oauth2ServerException
1108
     * @since 1.0.0
1109
     */
1110 3
    public function validateAuthenticatedRequest()
1111
    {
1112 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

1112
        $psr7Request = Psr7Helper::yiiToPsr7Request(/** @scrutinizer ignore-type */ Yii::$app->request);
Loading history...
1113
1114 3
        $psr7Request = $this->getResourceServer()->validateAuthenticatedRequest($psr7Request);
1115
1116 3
        $this->_oauthClaims = $psr7Request->getAttributes();
1117 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...
1118
    }
1119
1120
    /**
1121
     * Find a user identity bases on an access token.
1122
     * Note: validateAuthenticatedRequest() must be called before this method is called.
1123
     * @param string $token
1124
     * @param string $type
1125
     * @return Oauth2UserInterface|null
1126
     * @throws InvalidConfigException
1127
     * @throws Oauth2ServerException
1128
     * @see validateAuthenticatedRequest()
1129
     * @since 1.0.0
1130
     */
1131 4
    public function findIdentityByAccessToken($token, $type)
1132
    {
1133 4
        if (!is_a($type, Oauth2HttpBearerAuthInterface::class, true)) {
1134 1
            throw new InvalidCallException($type . ' must implement ' . Oauth2HttpBearerAuthInterface::class);
1135
        }
1136
1137
        if (
1138 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

1138
            !preg_match('/^Bearer\s+(.*?)$/', /** @scrutinizer ignore-type */ $this->_oauthClaimsAuthorizationHeader, $matches)
Loading history...
1139 3
            || !Yii::$app->security->compareString($matches[1], $token)
1140
        ) {
1141 1
            throw new InvalidCallException(
1142 1
                'validateAuthenticatedRequest() must be called before findIdentityByAccessToken().'
1143 1
            );
1144
        }
1145
1146 2
        $userId = $this->getRequestOauthUserId();
1147 2
        if (empty($userId)) {
1148 1
            return null;
1149
        }
1150
1151 1
        return $this->identityClass::findIdentity($userId);
1152
    }
1153
1154
    /**
1155
     * Generate a "Personal Access Token" (PAT) which can be used as an alternative to using passwords
1156
     * for authentication (e.g. when using an API or command line).
1157
     *
1158
     * Note: Personal Access Tokens are intended to access resources on behalf users themselves.
1159
     *       To grant access to resources on behalf of an organization, or for long-lived integrations,
1160
     *       you most likely want to define an Oauth2 Client with the "Client Credentials" grant
1161
     *       (https://oauth.net/2/grant-types/client-credentials).
1162
     *
1163
     * @param string $clientIdentifier The Oauth2 client identifier for which the PAT should be generated.
1164
     * @param int|string $userIdentifier The identifier (primary key) of the user for which the PAT should be generated.
1165
     * @param Oauth2ScopeInterface[]|string[]|string|null $scope The Access Token scope.
1166
     * @param string|true|null $clientSecret If the client is a "confidential" client the secret is required.
1167
     *        If the boolean value `true` is passed, the client secret is automatically injected.
1168
     * @return Oauth2AccessTokenData
1169
     */
1170 3
    public function generatePersonalAccessToken($clientIdentifier, $userIdentifier, $scope = null, $clientSecret = null)
1171
    {
1172 3
        if (is_array($scope)) {
1173 2
            $scopeIdentifiers = [];
1174 2
            foreach ($scope as $scopeItem) {
1175 2
                if (is_string($scopeItem)) {
1176 1
                    $scopeIdentifiers[] = $scopeItem;
1177 1
                } elseif ($scopeItem instanceof Oauth2ScopeInterface) {
1178 1
                    $scopeIdentifiers[] = $scopeItem->getIdentifier();
1179
                } else {
1180
                    throw new InvalidArgumentException('If $scope is an array its elements must be either'
1181
                        . ' a string or an instance of ' . Oauth2ScopeInterface::class);
1182
                }
1183
            }
1184 2
            $scope = implode(' ', $scopeIdentifiers);
1185
        }
1186
1187 3
        if ($clientSecret === true) {
1188
            /** @var Oauth2ClientInterface $client */
1189 3
            $client = $this->getClientRepository()->findModelByIdentifier($clientIdentifier);
1190 3
            if ($client && $client->isConfidential()) {
1191 3
                $clientSecret = $client->getDecryptedSecret($this->getEncryptor());
1192
            } else {
1193
                $clientSecret = null;
1194
            }
1195
        }
1196
1197 3
        $request = (new Psr7ServerRequest('POST', ''))->withParsedBody([
1198 3
            'grant_type' => static::GRANT_TYPE_IDENTIFIER_PERSONAL_ACCESS_TOKEN,
1199 3
            'client_id' => $clientIdentifier,
1200 3
            'client_secret' => $clientSecret,
1201 3
            'user_id' => $userIdentifier,
1202 3
            'scope' => $scope,
1203 3
        ]);
1204
1205 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

1205
        return new Oauth2AccessTokenData(/** @scrutinizer ignore-type */ Json::decode(
Loading history...
1206 3
            $this->getAuthorizationServer()
1207 3
                ->respondToAccessTokenRequest(
1208 3
                    $request,
1209 3
                    new Psr7Response()
1210 3
                )
1211 3
                ->getBody()
1212 3
                ->__toString()
1213 3
        ));
1214
    }
1215
1216
    /**
1217
     * @inheritDoc
1218
     */
1219 5
    protected function getRequestOauthClaim($attribute, $default = null)
1220
    {
1221 5
        if (empty($this->_oauthClaimsAuthorizationHeader)) {
1222
            // User authorization was not processed by Oauth2Module.
1223 1
            return $default;
1224
        }
1225 4
        if (Yii::$app->request->getHeaders()->get('Authorization') !== $this->_oauthClaimsAuthorizationHeader) {
1226 1
            throw new InvalidCallException(
1227 1
                'App Request Authorization header does not match the processed Oauth header.'
1228 1
            );
1229
        }
1230 3
        return $this->_oauthClaims[$attribute] ?? $default;
1231
    }
1232
1233
    /**
1234
     * Helper function to ensure the required properties are configured for the module.
1235
     * @param string[] $properties
1236
     * @throws InvalidConfigException
1237
     * @since 1.0.0
1238
     */
1239 30
    protected function ensureProperties($properties)
1240
    {
1241 30
        foreach ($properties as $property) {
1242 30
            if (empty($this->$property)) {
1243 6
                throw new InvalidConfigException( __CLASS__ . '::$' . $property . ' must be set.');
1244
            }
1245
        }
1246
    }
1247
}
1248