Passed
Push — master ( 2b2d6e...305862 )
by Rutger
13:36
created

setUserAuthenticatedDuringClientAuthRequest()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 4
c 1
b 0
f 0
dl 0
loc 8
rs 10
ccs 0
cts 3
cp 0
cc 2
nc 2
nop 2
crap 6
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 130
     */
548
    protected $_cryptographer = null;
549 130
550
    /**
551 130
     * @var string|null The authorization header used when the authorization request was validated.
552
     * @since 1.0.0
553 130
     */
554 29
    protected $_oauthClaimsAuthorizationHeader = null;
555 130
556 130
    /**
557
     * @inheritDoc
558 1
     * @throws InvalidConfigException
559 1
     */
560 1
    public function init()
561
    {
562 130
        parent::init();
563 130
564 130
        $app = Yii::$app;
565 130
566 130
        if ($app instanceof WebApplication || $this->appType == static::APPLICATION_TYPE_WEB) {
567
            $controllerMap = static::CONTROLLER_MAP[static::APPLICATION_TYPE_WEB];
568 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...
569 1
            $controllerMap = static::CONTROLLER_MAP[static::APPLICATION_TYPE_CONSOLE];
570 130
        } else {
571 1
            throw new InvalidConfigException(
572 1
                'Unable to detect application type, configure it manually by setting `$appType`.'
573 1
            );
574
        }
575
        $controllerMap = array_filter(
576 130
            $controllerMap,
577 130
            fn($controllerSettings) => $controllerSettings['serverRole'] & $this->serverRole
578 130
        );
579
        $this->controllerMap = ArrayHelper::getColumn($controllerMap, 'controller');
580
581
        if (empty($this->identityClass)) {
582 130
            throw new InvalidConfigException('$identityClass must be set.');
583 130
        } elseif (!is_a($this->identityClass, Oauth2UserInterface::class, true)) {
584
            throw new InvalidConfigException(
585
                $this->identityClass . ' must implement ' . Oauth2UserInterface::class
586 130
            );
587
        }
588
589
        foreach (static::DEFAULT_INTERFACE_IMPLEMENTATIONS as $interface => $implementation) {
590
            if (!Yii::$container->has($interface)) {
591
                Yii::$container->set($interface, $implementation);
592
            }
593 130
        }
594
595
        if (empty($this->urlRulesPrefix)) {
596 130
            $this->urlRulesPrefix = $this->uniqueId;
597 130
        }
598
599 29
        $this->registerTranslations();
600 29
    }
601 29
602 29
    /**
603 29
     * @inheritdoc
604 29
     * @throws InvalidConfigException
605 29
     */
606 29
    public function bootstrap($app)
607
    {
608 29
        if (
609 28
            $app instanceof WebApplication
610 28
            && $this->serverRole & static::SERVER_ROLE_AUTHORIZATION_SERVER
611
        ) {
612
            $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
                $this->jwksPath => Oauth2CertificatesControllerInterface::CONTROLLER_NAME
618
                    . '/' . Oauth2CertificatesControllerInterface::ACTION_NAME_JWKS,
619 29
            ];
620
621
            if ($this->enableTokenRevocation) {
622
                $rules[$this->tokenRevocationPath] = Oauth2ServerControllerInterface::CONTROLLER_NAME
623
                . '/' . Oauth2ServerControllerInterface::ACTION_NAME_REVOKE;
624
            }
625 29
626 29
            if (empty($this->clientAuthorizationUrl)) {
627 29
                $rules[$this->clientAuthorizationPath] = Oauth2ConsentControllerInterface::CONTROLLER_NAME
628 29
                    . '/' . Oauth2ConsentControllerInterface::ACTION_NAME_AUTHORIZE_CLIENT;
629 29
            }
630 29
631 29
            if ($this->enableOpenIdConnect && $this->openIdConnectUserinfoEndpoint === true) {
632 29
                $rules[$this->openIdConnectUserinfoPath] =
633 29
                    Oauth2OidcControllerInterface::CONTROLLER_NAME
634
                    . '/' . Oauth2OidcControllerInterface::ACTION_NAME_USERINFO;
635
            }
636 29
637 29
            if ($this->enableOpenIdConnect && $this->openIdConnectRpInitiatedLogoutEndpoint === true) {
638 29
                $rules[$this->openIdConnectRpInitiatedLogoutPath] =
639
                    Oauth2OidcControllerInterface::CONTROLLER_NAME
640 29
                    . '/' . Oauth2OidcControllerInterface::ACTION_END_SESSION;
641 29
            }
642 29
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
                    'rules' => $rules,
650
                ]),
651
            ]);
652
653
            if (
654
                $this->enableOpenIdConnect
655
                && $this->enableOpenIdConnectDiscovery
656
                && $this->openIdConnectProviderConfigurationInformationPath
657
            ) {
658 130
                $urlManager->addRules([
659
                    Yii::createObject([
660 130
                        'class' => UrlRule::class,
661 130
                        'pattern' => $this->openIdConnectProviderConfigurationInformationPath,
662 130
                        'route' => $this->id
663 130
                            . '/' . Oauth2WellKnownControllerInterface::CONTROLLER_NAME
664 130
                            . '/' . Oauth2WellKnownControllerInterface::ACTION_NAME_OPENID_CONFIGURATION,
665 130
                    ]),
666 130
                ]);
667 130
            }
668 130
        }
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
    public function registerTranslations($force = false)
677
    {
678
        if ($force || !array_key_exists('oauth2', Yii::$app->i18n->translations)) {
679
            Yii::$app->i18n->translations['oauth2'] = [
680
                'class' => PhpMessageSource::class,
681
                'sourceLanguage' => 'en-US',
682
                'basePath' => __DIR__ . DIRECTORY_SEPARATOR . 'messages',
683
                'fileMap' => [
684
                    'oauth2' => 'oauth2.php',
685
                ],
686
            ];
687
        }
688 5
    }
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 5
     * @param string|string[]|array[]|Oauth2ScopeInterface[]|null $scopes
701 1
     * @param int|null $userId
702
     * @return Oauth2ClientInterface
703
     * @throws InvalidConfigException
704
     * @throws \yii\db\Exception
705 4
     */
706 4
    public function createClient(
707 4
        $identifier,
708 4
        $name,
709 4
        $grantTypes,
710 4
        $redirectURIs,
711 4
        $type,
712 4
        $secret = null,
713 4
        $scopes = null,
714 4
        $userId = null,
715 4
        $endUsersMayAuthorizeClient = null,
716
        $skipAuthorizationIfScopeIsAllowed = null
717 4
    ) {
718
        if (!($this->serverRole & static::SERVER_ROLE_AUTHORIZATION_SERVER)) {
719
            throw new InvalidCallException('Oauth2 server role does not include authorization server.');
720 4
        }
721 4
722
        /** @var Oauth2ClientInterface $client */
723
        $client = Yii::createObject([
724 3
            'class' => Oauth2ClientInterface::class,
725 3
            'identifier' => $identifier,
726 3
            'type' => $type,
727
            'name' => $name,
728 3
            'redirectUri' => $redirectURIs,
729 1
            'grantTypes' => $grantTypes,
730 1
            'endUsersMayAuthorizeClient' => $endUsersMayAuthorizeClient,
731 1
            'skip_authorization_if_scope_is_allowed' => $skipAuthorizationIfScopeIsAllowed,
732
            'clientCredentialsGrantUserId' => $userId
733
        ]);
734 3
735
        $transaction = $client::getDb()->beginTransaction();
736
737
        try {
738
            if ($type == Oauth2ClientInterface::TYPE_CONFIDENTIAL) {
739
                $client->setSecret($secret, $this->getCryptographer());
740
            }
741
742 22
            $client
743
                ->persist()
744 22
                ->syncClientScopes($scopes, $this->getScopeRepository());
745 22
746 19
            $transaction->commit();
747
        } catch (\Exception $e) {
748 22
            $transaction->rollBack();
749
            throw $e;
750
        }
751
752
        return $client;
753
    }
754
755
    /**
756 9
     * @return CryptKey The private key of the server.
757
     * @throws InvalidConfigException
758 9
     * @since 1.0.0
759 9
     */
760 6
    public function getPrivateKey()
761
    {
762 9
        $privateKey = $this->privateKey;
763
        if (StringHelper::startsWith($privateKey, '@')) {
764
            $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
        return Yii::createObject(CryptKey::class, [$privateKey, $this->privateKeyPassphrase]);
767
    }
768
769
    /**
770 27
     * @return CryptKey The public key of the server.
771
     * @throws InvalidConfigException
772 27
     * @since 1.0.0
773 1
     */
774
    public function getPublicKey()
775
    {
776 26
        $publicKey = $this->publicKey;
777 26
        if (StringHelper::startsWith($publicKey, '@')) {
778
            $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 21
        }
780 1
        return Yii::createObject(CryptKey::class, [$publicKey]);
781 1
    }
782 1
783
    /**
784
     * @return Oauth2AuthorizationServerInterface The authorization server.
785
     * @throws InvalidConfigException
786 19
     * @since 1.0.0
787
     */
788 19
    public function getAuthorizationServer()
789 1
    {
790 1
        if (!($this->serverRole & static::SERVER_ROLE_AUTHORIZATION_SERVER)) {
791 1
            throw new InvalidCallException('Oauth2 server role does not include authorization server.');
792 1
        }
793 1
794 1
        if (!$this->_authorizationServer) {
795
            $this->ensureProperties(static::REQUIRED_SETTINGS_AUTHORIZATION_SERVER);
796
797
            if (!$this->getCryptographer()->hasKey($this->defaultStorageEncryptionKey)) {
798
                throw new InvalidConfigException(
799
                    'Key "' . $this->defaultStorageEncryptionKey . '" is not set in $storageEncryptionKeys'
800
                );
801
            }
802
803 18
            /** @var Oauth2EncryptionKeyFactoryInterface $keyFactory */
804 18
            $keyFactory = Yii::createObject(Oauth2EncryptionKeyFactoryInterface::class);
805 18
            try {
806 18
                $codesEncryptionKey = $keyFactory->createFromAsciiSafeString($this->codesEncryptionKey);
807 18
            } catch (BadFormatException $e) {
808
                throw new InvalidConfigException(
809
                    '$codesEncryptionKey is malformed: ' . $e->getMessage(),
810 18
                    0,
811 18
                    $e
812 18
                );
813 18
            } catch (EnvironmentIsBrokenException $e) {
814 18
                throw new InvalidConfigException(
815 18
                    'Could not instantiate $codesEncryptionKey: ' . $e->getMessage(),
816 18
                    0,
817 18
                    $e
818
                );
819 18
            }
820 18
821
            if ($this->enableOpenIdConnect) {
822 18
                $responseTypeClass = Oauth2OidcBearerTokenResponseInterface::class;
823 1
            } else {
824
                $responseTypeClass = Oauth2BearerTokenResponseInterface::class;
825 17
            }
826 2
            $responseType = Yii::createObject($responseTypeClass, [
827
                $this,
828
            ]);
829 17
830 17
            $this->_authorizationServer = Yii::createObject(Oauth2AuthorizationServerInterface::class, [
831 1
                $this->getClientRepository(),
832 1
                $this->getAccessTokenRepository(),
833
                $this->getScopeRepository(),
834
                $this->getPrivateKey(),
835 16
                $codesEncryptionKey,
836 16
                $responseType
837
            ]);
838 16
839
            if (!empty($this->grantTypes)) {
840
                $grantTypes = $this->grantTypes;
841 15
842 15
                if (is_callable($grantTypes)) {
843
                    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 15
                } else {
845
                    if (!is_array($grantTypes)) {
846
                        $grantTypes = [$grantTypes];
847
                    }
848 15
849 15
                    foreach ($grantTypes as $grantTypeDefinition) {
850 15
                        if ($grantTypeDefinition instanceof GrantTypeInterface) {
851 15
                            $accessTokenTTL = $this->getDefaultAccessTokenTTL();
852 15
                            $this->_authorizationServer->enableGrantType($grantTypeDefinition, $accessTokenTTL);
853 15
                        } elseif (
854 15
                            (
855
                                is_numeric($grantTypeDefinition)
856 1
                                && array_key_exists($grantTypeDefinition, static::DEFAULT_GRANT_TYPE_FACTORIES)
857 1
                            )
858 1
                            || is_a($grantTypeDefinition, Oauth2GrantTypeFactoryInterface::class, true)
859 1
                        ) {
860 1
                            if (
861 1
                                is_numeric($grantTypeDefinition)
862 1
                                && array_key_exists($grantTypeDefinition, static::DEFAULT_GRANT_TYPE_FACTORIES)
863 1
                            ) {
864
                                $grantTypeDefinition = static::DEFAULT_GRANT_TYPE_FACTORIES[$grantTypeDefinition];
865
                            }
866
867
                            /** @var Oauth2GrantTypeFactoryInterface $factory */
868
                            $factory = Yii::createObject([
869
                                'class' => $grantTypeDefinition,
870 17
                                'module' => $this,
871
                            ]);
872
                            $accessTokenTTL = $factory->getDefaultAccessTokenTTL()
873
                                ?? $this->getDefaultAccessTokenTTL();
874
                            $this->_authorizationServer->enableGrantType($factory->getGrantType(), $accessTokenTTL);
875
                        } else {
876
                            throw new InvalidConfigException(
877 6
                                'Unknown grantType '
878
                                . (
879 6
                                    is_scalar($grantTypeDefinition)
880 6
                                        ? '"' . $grantTypeDefinition . '".'
881 6
                                        : 'with data type ' . gettype($grantTypeDefinition)
882 1
                                )
883 5
                            );
884 1
                        }
885 1
                    }
886 1
                }
887 1
            }
888 1
        }
889 1
890
        return $this->_authorizationServer;
891 4
    }
892 3
893 3
    /**
894 3
     * @inheritDoc
895 3
     * @throws InvalidConfigException
896
     */
897 1
    public function getOidcScopeCollection()
898 1
    {
899 1
        if ($this->_oidcScopeCollection === null) {
900 1
            $openIdConnectScopes = $this->getOpenIdConnectScopes();
901
            if ($openIdConnectScopes instanceof Oauth2OidcScopeCollectionInterface) {
902
                $this->_oidcScopeCollection = $openIdConnectScopes;
903
            } elseif (is_callable($openIdConnectScopes)) {
904 5
                $this->_oidcScopeCollection = call_user_func($openIdConnectScopes, $this);
905
                if (!($this->_oidcScopeCollection instanceof Oauth2OidcScopeCollectionInterface)) {
906
                    throw new InvalidConfigException(
907
                        '$openIdConnectScopes must return an instance of '
908
                            . Oauth2OidcScopeCollectionInterface::class
909
                    );
910
                }
911
            } elseif (is_array($openIdConnectScopes) || is_string($openIdConnectScopes)) {
912 7
                $this->_oidcScopeCollection = Yii::createObject([
913
                    'class' => Oauth2OidcScopeCollectionInterface::class,
914 7
                    'oidcScopes' => (array)$openIdConnectScopes,
915 1
                ]);
916
            } else {
917
                throw new InvalidConfigException(
918 6
                    '$openIdConnectScopes must be a callable, array, string or '
919 6
                        . Oauth2OidcScopeCollectionInterface::class
920
                );
921 5
            }
922 5
        }
923
924 5
        return $this->_oidcScopeCollection;
925 5
    }
926 5
927 5
    /**
928
     * @return Oauth2ResourceServerInterface The resource server.
929
     * @throws InvalidConfigException
930 5
     * @since 1.0.0
931
     */
932
    public function getResourceServer()
933
    {
934
        if (!($this->serverRole & static::SERVER_ROLE_RESOURCE_SERVER)) {
935
            throw new InvalidCallException('Oauth2 server role does not include resource server.');
936
        }
937
938 27
        if (!$this->_resourceServer) {
939
            $this->ensureProperties(static::REQUIRED_SETTINGS_RESOURCE_SERVER);
940 27
941 27
            $accessTokenRepository = $this->getAccessTokenRepository()
942 27
                ->setRevocationValidation($this->resourceServerAccessTokenRevocationValidation);
943 27
944 27
            $this->_resourceServer = Yii::createObject(Oauth2ResourceServerInterface::class, [
945 27
                $accessTokenRepository,
946
                $this->getPublicKey(),
947
            ]);
948 26
        }
949
950
        return $this->_resourceServer;
951
    }
952
953
    /**
954
     * @return Oauth2CryptographerInterface The data cryptographer for the module.
955
     * @throws InvalidConfigException
956 1
     * @since 1.0.0
957
     */
958 1
    public function getCryptographer()
959
    {
960 1
        if (!$this->_cryptographer) {
961 1
            $this->_cryptographer = Yii::createObject([
962 1
                'class' => Oauth2CryptographerInterface::class,
963 1
                'keys' => $this->storageEncryptionKeys,
964
                'defaultKeyName' => $this->defaultStorageEncryptionKey,
965
            ]);
966
        }
967 1
968
        return $this->_cryptographer;
969
    }
970 1
971
    /**
972
     * @param string|null $newKeyName
973
     * @return array
974
     * @throws InvalidConfigException
975
     */
976
    public function rotateStorageEncryptionKeys($newKeyName = null)
977 12
    {
978
        $cryptographer = $this->getCryptographer();
979 12
980 1
        $result = [];
981
        foreach (static::ENCRYPTED_MODELS as $modelInterface) {
982
            $modelClass = DiHelper::getValidatedClassName($modelInterface);
983
            if (!is_a($modelClass, Oauth2EncryptedStorageInterface::class, true)) {
984 11
                throw new InvalidConfigException($modelInterface . ' must implement '
985 11
                    . Oauth2EncryptedStorageInterface::class);
986
            }
987 7
            $result[$modelClass] = $modelClass::rotateStorageEncryptionKeys($cryptographer, $newKeyName);
988
        }
989
990 4
        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
    public function validateTlsConnection()
998
    {
999
        if (Yii::$app->request->getIsSecureConnection()) {
1000
            return true;
1001
        }
1002
1003
        if (
1004
            !empty($this->nonTlsAllowedRanges)
1005
            && (new IpValidator(['ranges' => $this->nonTlsAllowedRanges]))->validate(Yii::$app->request->getRemoteIP())
1006
        ) {
1007
            return true;
1008
        }
1009
1010
        return false;
1011
    }
1012
1013
    /**
1014
     * @return array
1015
     * @throws InvalidConfigException
1016
     */
1017
    public function getStorageEncryptionKeyUsage()
1018
    {
1019
        $cryptographer = $this->getCryptographer();
1020 6
1021
        $result = [];
1022 6
        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
    public function validateAuthRequestScopes($client, $requestedScopeIdentifiers, $redirectUri = null)
1041
    {
1042
        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 5
                || (
1049
                    $client->getExceptionOnInvalidScope() === null
1050 5
                    && $this->exceptionOnInvalidScope === true
1051 5
                )
1052 1
            ) {
1053
                if ($unknownScopes) {
1054 4
                    throw Oauth2ServerException::unknownScope(array_shift($unknownScopes), $redirectUri);
1055 4
                }
1056 4
                throw Oauth2ServerException::scopeNotAllowedForClient(array_shift($unauthorizedScopes), $redirectUri);
1057
            }
1058 5
        }
1059 5
    }
1060 5
1061 5
    /**
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
    public function generateClientAuthReqRedirectResponse($clientAuthorizationRequest)
1069
    {
1070 5
        $this->setClientAuthReqSession($clientAuthorizationRequest);
1071
        if (!empty($this->clientAuthorizationUrl)) {
1072 5
            $url = $this->clientAuthorizationUrl;
1073
        } else {
1074
            $url = $this->uniqueId
1075 5
                . '/' . Oauth2ConsentControllerInterface::CONTROLLER_NAME
1076 5
                . '/' . Oauth2ConsentControllerInterface::ACTION_NAME_AUTHORIZE_CLIENT;
1077 5
        }
1078 2
        return Yii::$app->response->redirect([
1079 1
            $url,
1080 1
            'clientAuthorizationRequestId' => $clientAuthorizationRequest->getRequestId(),
1081 1
        ]);
1082 1
    }
1083
1084 2
    /**
1085
     * Get a previously stored Client Authorization Request from the session.
1086 5
     * @param string $requestId
1087 1
     * @return Oauth2ClientAuthorizationRequestInterface|null
1088 1
     * @since 1.0.0
1089 1
     */
1090 1
    public function getClientAuthReqSession($requestId)
1091 1
    {
1092
        if (empty($requestId)) {
1093 5
            return null;
1094
        }
1095 5
        $key = static::CLIENT_AUTHORIZATION_REQUEST_SESSION_PREFIX . $requestId;
1096
        $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
        if (!($clientAuthorizationRequest instanceof Oauth2ClientAuthorizationRequestInterface)) {
1098
            if (!empty($clientAuthorizationRequest)) {
1099
                Yii::warning(
1100
                    'Found a ClientAuthorizationRequestSession with key "' . $key
1101
                        . '", but it\'s not a ' . Oauth2ClientAuthorizationRequestInterface::class
1102
                );
1103 8
            }
1104
            return null;
1105 8
        }
1106 8
        if ($clientAuthorizationRequest->getRequestId() !== $requestId) {
1107 1
            Yii::warning(
1108
                'Found a ClientAuthorizationRequestSession with key "' . $key
1109 7
                    . '", but its request id does not match "' . $requestId . '".'
1110 7
            );
1111
            return null;
1112
        }
1113
        $clientAuthorizationRequest->setModule($this);
1114
1115
        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
    public function setClientAuthReqSession($clientAuthorizationRequest)
1124
    {
1125
        $requestId = $clientAuthorizationRequest->getRequestId();
1126
        if (empty($requestId)) {
1127
            throw new InvalidArgumentException('$scopeAuthorization must return a request id.');
1128
        }
1129
        $key = static::CLIENT_AUTHORIZATION_REQUEST_SESSION_PREFIX . $requestId;
1130
        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 2
    /**
1151
     * Stores the user identity selected during the completion of the Client Authorization Request.
1152 2
     * @param string $clientAuthorizationRequestId
1153 1
     * @param Oauth2UserInterface $userIdentity
1154
     * @since 1.0.0
1155 1
     */
1156 1
    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 1
    /**
1166
     * Clears a Client Authorization Request from the session storage.
1167 1
     * @param string $requestId
1168 1
     * @since 1.0.0
1169 1
     */
1170
    public function removeClientAuthReqSession($requestId)
1171
    {
1172
        if (empty($requestId)) {
1173
            throw new InvalidArgumentException('$requestId can not be empty.');
1174
        }
1175
        $key = static::CLIENT_AUTHORIZATION_REQUEST_SESSION_PREFIX . $requestId;
1176
        Yii::$app->session->remove($key);
1177 5
    }
1178
1179 5
    /**
1180 5
     * Generates a redirect Response when the Client Authorization Request is completed.
1181 1
     * @param Oauth2ClientAuthorizationRequestInterface $clientAuthorizationRequest
1182 1
     * @return Response
1183 1
     * @since 1.0.0
1184 1
     */
1185
    public function generateClientAuthReqCompledRedirectResponse($clientAuthorizationRequest)
1186 4
    {
1187
        $clientAuthorizationRequest->processAuthorization();
1188
        $this->setClientAuthReqSession($clientAuthorizationRequest);
1189
        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 3
     */
1197
    public function getUserIdentity()
1198 3
    {
1199
        $user = Yii::$app->user->identity;
1200 3
        if (!empty($user) && !($user instanceof Oauth2UserInterface)) {
1201
            throw new InvalidConfigException(
1202 3
                'Yii::$app->user->identity (currently ' . get_class($user)
1203 3
                    . ') must implement ' . Oauth2UserInterface::class
1204
            );
1205
        }
1206
        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
    public function validateAuthenticatedRequest()
1217 4
    {
1218
        $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 4
1220 1
        $psr7Request = $this->getResourceServer()->validateAuthenticatedRequest($psr7Request);
1221
1222
        $this->_oauthClaims = $psr7Request->getAttributes();
1223
        $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 3
    }
1225 3
1226
    /**
1227 1
     * Find a user identity bases on an access token.
1228 1
     * Note: validateAuthenticatedRequest() must be called before this method is called.
1229 1
     * @param string $token
1230
     * @param string $type
1231
     * @return Oauth2UserInterface|null
1232 2
     * @throws InvalidConfigException
1233 2
     * @throws Oauth2ServerException
1234 1
     * @see validateAuthenticatedRequest()
1235
     * @since 1.0.0
1236
     */
1237 1
    public function findIdentityByAccessToken($token, $type)
1238
    {
1239
        if (!is_a($type, Oauth2HttpBearerAuthInterface::class, true)) {
1240
            throw new InvalidCallException($type . ' must implement ' . Oauth2HttpBearerAuthInterface::class);
1241
        }
1242
1243
        if (
1244
            !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
            || !Yii::$app->security->compareString($matches[1], $token)
1246
        ) {
1247
            throw new InvalidCallException(
1248
                'validateAuthenticatedRequest() must be called before findIdentityByAccessToken().'
1249
            );
1250
        }
1251
1252
        $userId = $this->getRequestOauthUserId();
1253
        if (empty($userId)) {
1254
            return null;
1255
        }
1256 3
1257
        return $this->identityClass::findIdentity($userId);
1258 3
    }
1259 2
1260 2
    /**
1261 2
     * Generate a "Personal Access Token" (PAT) which can be used as an alternative to using passwords
1262 1
     * for authentication (e.g. when using an API or command line).
1263 1
     *
1264 1
     * 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 2
     * @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 3
     *        If the boolean value `true` is passed, the client secret is automatically injected.
1274
     * @return Oauth2AccessTokenData
1275 3
     */
1276 3
    public function generatePersonalAccessToken($clientIdentifier, $userIdentifier, $scope = null, $clientSecret = null)
1277 3
    {
1278
        if (is_array($scope)) {
1279
            $scopeIdentifiers = [];
1280
            foreach ($scope as $scopeItem) {
1281
                if (is_string($scopeItem)) {
1282
                    $scopeIdentifiers[] = $scopeItem;
1283 3
                } elseif ($scopeItem instanceof Oauth2ScopeInterface) {
1284 3
                    $scopeIdentifiers[] = $scopeItem->getIdentifier();
1285 3
                } else {
1286 3
                    throw new InvalidArgumentException('If $scope is an array its elements must be either'
1287 3
                        . ' a string or an instance of ' . Oauth2ScopeInterface::class);
1288 3
                }
1289 3
            }
1290
            $scope = implode(' ', $scopeIdentifiers);
1291 3
        }
1292 3
1293 3
        if ($clientSecret === true) {
1294 3
            /** @var Oauth2ClientInterface $client */
1295 3
            $client = $this->getClientRepository()->findModelByIdentifier($clientIdentifier);
1296 3
            if ($client && $client->isConfidential()) {
1297 3
                $clientSecret = $client->getDecryptedSecret($this->getCryptographer());
1298 3
            } else {
1299 3
                $clientSecret = null;
1300
            }
1301
        }
1302
1303
        $request = (new Psr7ServerRequest('POST', ''))->withParsedBody([
1304
            'grant_type' => static::GRANT_TYPE_IDENTIFIER_PERSONAL_ACCESS_TOKEN,
1305 5
            'client_id' => $clientIdentifier,
1306
            'client_secret' => $clientSecret,
1307 5
            'user_id' => $userIdentifier,
1308
            'scope' => $scope,
1309 1
        ]);
1310
1311 4
        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 1
            $this->getAuthorizationServer()
1313 1
                ->respondToAccessTokenRequest(
1314 1
                    $request,
1315
                    new Psr7Response()
1316 3
                )
1317
                ->getBody()
1318
                ->__toString()
1319
        ));
1320
    }
1321
1322
    /**
1323
     * @inheritDoc
1324
     */
1325 32
    protected function getRequestOauthClaim($attribute, $default = null)
1326
    {
1327 32
        if (empty($this->_oauthClaimsAuthorizationHeader)) {
1328 32
            // User authorization was not processed by Oauth2Module.
1329 6
            return $default;
1330
        }
1331
        if (Yii::$app->request->getHeaders()->get('Authorization') !== $this->_oauthClaimsAuthorizationHeader) {
1332
            throw new InvalidCallException(
1333
                'App Request Authorization header does not match the processed Oauth header.'
1334
            );
1335
        }
1336
        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
    protected function ensureProperties($properties)
1346
    {
1347
        foreach ($properties as $property) {
1348
            if (empty($this->$property)) {
1349
                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