Passed
Push — master ( 305862...fff288 )
by Rutger
11:51 queued 08:42
created

Oauth2Module::getResourceServer()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 19
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 3

Importance

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

764
            $privateKey = 'file://' . /** @scrutinizer ignore-type */ Yii::getAlias($privateKey);
Loading history...
765
        }
766 22
        return Yii::createObject(CryptKey::class, [$privateKey, $this->privateKeyPassphrase]);
767
    }
768
769
    /**
770
     * @return CryptKey The public key of the server.
771
     * @throws InvalidConfigException
772
     * @since 1.0.0
773
     */
774 9
    public function getPublicKey()
775
    {
776 9
        $publicKey = $this->publicKey;
777 9
        if (StringHelper::startsWith($publicKey, '@')) {
778 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

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

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

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

1218
        $psr7Request = Psr7Helper::yiiToPsr7Request(/** @scrutinizer ignore-type */ Yii::$app->request);
Loading history...
1219
1220 3
        $psr7Request = $this->getResourceServer()->validateAuthenticatedRequest($psr7Request);
1221
1222 3
        $this->_oauthClaims = $psr7Request->getAttributes();
1223 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...
1224
    }
1225
1226
    /**
1227
     * Find a user identity bases on an access token.
1228
     * Note: validateAuthenticatedRequest() must be called before this method is called.
1229
     * @param string $token
1230
     * @param string $type
1231
     * @return Oauth2UserInterface|null
1232
     * @throws InvalidConfigException
1233
     * @throws Oauth2ServerException
1234
     * @see validateAuthenticatedRequest()
1235
     * @since 1.0.0
1236
     */
1237 4
    public function findIdentityByAccessToken($token, $type)
1238
    {
1239 4
        if (!is_a($type, Oauth2HttpBearerAuthInterface::class, true)) {
1240 1
            throw new InvalidCallException($type . ' must implement ' . Oauth2HttpBearerAuthInterface::class);
1241
        }
1242
1243
        if (
1244 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

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

1311
        return new Oauth2AccessTokenData(/** @scrutinizer ignore-type */ Json::decode(
Loading history...
1312 3
            $this->getAuthorizationServer()
1313 3
                ->respondToAccessTokenRequest(
1314 3
                    $request,
1315 3
                    new Psr7Response()
1316 3
                )
1317 3
                ->getBody()
1318 3
                ->__toString()
1319 3
        ));
1320
    }
1321
1322
    /**
1323
     * @inheritDoc
1324
     */
1325 5
    protected function getRequestOauthClaim($attribute, $default = null)
1326
    {
1327 5
        if (empty($this->_oauthClaimsAuthorizationHeader)) {
1328
            // User authorization was not processed by Oauth2Module.
1329 1
            return $default;
1330
        }
1331 4
        if (Yii::$app->request->getHeaders()->get('Authorization') !== $this->_oauthClaimsAuthorizationHeader) {
1332 1
            throw new InvalidCallException(
1333 1
                'App Request Authorization header does not match the processed Oauth header.'
1334 1
            );
1335
        }
1336 3
        return $this->_oauthClaims[$attribute] ?? $default;
1337
    }
1338
1339
    /**
1340
     * Helper function to ensure the required properties are configured for the module.
1341
     * @param string[] $properties
1342
     * @throws InvalidConfigException
1343
     * @since 1.0.0
1344
     */
1345 32
    protected function ensureProperties($properties)
1346
    {
1347 32
        foreach ($properties as $property) {
1348 32
            if (empty($this->$property)) {
1349 6
                throw new InvalidConfigException(__CLASS__ . '::$' . $property . ' must be set.');
1350
            }
1351
        }
1352
    }
1353
1354
    public function logoutUser()
1355
    {
1356
        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

1356
        Yii::$app->user->/** @scrutinizer ignore-call */ 
1357
                         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...
1357
    }
1358
1359
    public function getElaboratedHttpClientErrorsLogLevel()
1360
    {
1361
        if ($this->httpClientErrorsLogLevel === null) {
1362
            return YII_DEBUG ? Logger::LEVEL_ERROR : Logger::LEVEL_INFO;
1363
        }
1364
1365
        return $this->httpClientErrorsLogLevel;
1366
    }
1367
}
1368