Passed
Push — master ( 1437aa...6e82fd )
by Rutger
13:37
created

Oauth2Module::ensureProperties()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3

Importance

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

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

750
            $privateKey = 'file://' . /** @scrutinizer ignore-type */ Yii::getAlias($privateKey);
Loading history...
751
        }
752 22
        return Yii::createObject(CryptKey::class, [$privateKey, $this->privateKeyPassphrase]);
753
    }
754
755
    /**
756
     * @return CryptKey The public key of the server.
757
     * @throws InvalidConfigException
758
     * @since 1.0.0
759
     */
760 9
    public function getPublicKey()
761
    {
762 9
        $publicKey = $this->publicKey;
763 9
        if (StringHelper::startsWith($publicKey, '@')) {
764 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

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

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

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

1202
        $psr7Request = Psr7Helper::yiiToPsr7Request(/** @scrutinizer ignore-type */ Yii::$app->request);
Loading history...
1203
1204 3
        $psr7Request = $this->getResourceServer()->validateAuthenticatedRequest($psr7Request);
1205
1206 3
        $this->_oauthClaims = $psr7Request->getAttributes();
1207 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...
1208
    }
1209
1210
    /**
1211
     * Find a user identity bases on an access token.
1212
     * Note: validateAuthenticatedRequest() must be called before this method is called.
1213
     * @param string $token
1214
     * @param string $type
1215
     * @return Oauth2UserInterface|null
1216
     * @throws InvalidConfigException
1217
     * @throws Oauth2ServerException
1218
     * @see validateAuthenticatedRequest()
1219
     * @since 1.0.0
1220
     */
1221 4
    public function findIdentityByAccessToken($token, $type)
1222
    {
1223 4
        if (!is_a($type, Oauth2HttpBearerAuthInterface::class, true)) {
1224 1
            throw new InvalidCallException($type . ' must implement ' . Oauth2HttpBearerAuthInterface::class);
1225
        }
1226
1227
        if (
1228 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

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

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

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

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

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
1341
    }
1342
}
1343