Issues (150)

src/Oauth2Module.php (1 issue)

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
// phpcs:disable Generic.Files.LineLength.TooLong
12
use Defuse\Crypto\Exception\BadFormatException;
13
use Defuse\Crypto\Exception\EnvironmentIsBrokenException;
14
use GuzzleHttp\Psr7\Response as Psr7Response;
15
use GuzzleHttp\Psr7\ServerRequest as Psr7ServerRequest;
16
use Lcobucci\JWT\Configuration;
17
use Lcobucci\JWT\Signer\Key\InMemory;
18
use Lcobucci\JWT\Signer\Rsa\Sha256;
19
use Lcobucci\JWT\Token;
20
use Lcobucci\JWT\Validation\Constraint\SignedWith;
21
use League\OAuth2\Server\CryptKey;
22
use League\OAuth2\Server\Grant\GrantTypeInterface;
23
use rhertogh\Yii2Oauth2Server\base\Oauth2BaseModule;
24
use rhertogh\Yii2Oauth2Server\components\server\tokens\Oauth2AccessTokenData;
25
use rhertogh\Yii2Oauth2Server\controllers\console\Oauth2ClientController;
26
use rhertogh\Yii2Oauth2Server\controllers\console\Oauth2DebugController;
27
use rhertogh\Yii2Oauth2Server\controllers\console\Oauth2EncryptionController;
28
use rhertogh\Yii2Oauth2Server\controllers\console\Oauth2MigrationsController;
29
use rhertogh\Yii2Oauth2Server\controllers\console\Oauth2PersonalAccessTokenController;
30
use rhertogh\Yii2Oauth2Server\exceptions\Oauth2ServerException;
31
use rhertogh\Yii2Oauth2Server\helpers\DiHelper;
32
use rhertogh\Yii2Oauth2Server\helpers\Psr7Helper;
33
use rhertogh\Yii2Oauth2Server\interfaces\components\authorization\base\Oauth2BaseAuthorizationRequestInterface;
34
use rhertogh\Yii2Oauth2Server\interfaces\components\authorization\client\Oauth2ClientAuthorizationRequestInterface;
35
use rhertogh\Yii2Oauth2Server\interfaces\components\authorization\EndSession\Oauth2EndSessionAuthorizationRequestInterface;
36
use rhertogh\Yii2Oauth2Server\interfaces\components\common\DefaultAccessTokenTtlInterface;
37
use rhertogh\Yii2Oauth2Server\interfaces\components\encryption\Oauth2CryptographerInterface;
38
use rhertogh\Yii2Oauth2Server\interfaces\components\factories\encryption\Oauth2EncryptionKeyFactoryInterface;
39
use rhertogh\Yii2Oauth2Server\interfaces\components\factories\grants\base\Oauth2GrantTypeFactoryInterface;
40
use rhertogh\Yii2Oauth2Server\interfaces\components\openidconnect\request\Oauth2OidcAuthenticationRequestInterface;
41
use rhertogh\Yii2Oauth2Server\interfaces\components\openidconnect\scope\Oauth2OidcScopeCollectionInterface;
42
use rhertogh\Yii2Oauth2Server\interfaces\components\openidconnect\server\responses\Oauth2OidcBearerTokenResponseInterface;
43
use rhertogh\Yii2Oauth2Server\interfaces\components\server\Oauth2AuthorizationServerInterface;
44
use rhertogh\Yii2Oauth2Server\interfaces\components\server\Oauth2ResourceServerInterface;
45
use rhertogh\Yii2Oauth2Server\interfaces\components\server\responses\Oauth2BearerTokenResponseInterface;
46
use rhertogh\Yii2Oauth2Server\interfaces\controllers\web\Oauth2CertificatesControllerInterface;
47
use rhertogh\Yii2Oauth2Server\interfaces\controllers\web\Oauth2ConsentControllerInterface;
48
use rhertogh\Yii2Oauth2Server\interfaces\controllers\web\Oauth2OidcControllerInterface;
49
use rhertogh\Yii2Oauth2Server\interfaces\controllers\web\Oauth2ServerControllerInterface;
50
use rhertogh\Yii2Oauth2Server\interfaces\controllers\web\Oauth2WellKnownControllerInterface;
51
use rhertogh\Yii2Oauth2Server\interfaces\filters\auth\Oauth2HttpBearerAuthInterface;
52
use rhertogh\Yii2Oauth2Server\interfaces\models\base\Oauth2EncryptedStorageInterface;
53
use rhertogh\Yii2Oauth2Server\interfaces\models\external\user\Oauth2OidcUserInterface;
54
use rhertogh\Yii2Oauth2Server\interfaces\models\external\user\Oauth2UserInterface;
55
use rhertogh\Yii2Oauth2Server\interfaces\models\Oauth2ClientInterface;
56
use rhertogh\Yii2Oauth2Server\interfaces\models\Oauth2ScopeInterface;
57
use rhertogh\Yii2Oauth2Server\traits\DefaultAccessTokenTtlTrait;
58
use Yii;
59
use yii\base\BootstrapInterface;
60
use yii\base\InvalidArgumentException;
61
use yii\base\InvalidCallException;
62
use yii\base\InvalidConfigException;
63
use yii\console\Application as ConsoleApplication;
64
use yii\helpers\ArrayHelper;
65
use yii\helpers\Json;
66
use yii\helpers\StringHelper;
67
use yii\helpers\VarDumper;
68
use yii\i18n\PhpMessageSource;
69
use yii\log\Logger;
70
use yii\validators\IpValidator;
71
use yii\web\Application as WebApplication;
72
use yii\web\GroupUrlRule;
73
use yii\web\IdentityInterface;
74
use yii\web\Response;
75
use yii\web\UrlRule;
76
77
// phpcs:enable Generic.Files.LineLength.TooLong
78
79
/**
80
 * This is the main module class for the Yii2 Oauth2 Server module.
81
 * To use it, include it as a module in the application configuration like the following:
82
 *
83
 * ~~~
84
 * return [
85
 *     'bootstrap' => ['oauth2'],
86
 *     'modules' => [
87
 *         'oauth2' => [
88
 *             'class' => 'rhertogh\Yii2Oauth2Server\Oauth2Module',
89
 *             // ... Please check docs/guide/start-installation.md further details
90
 *          ],
91
 *     ],
92
 * ]
93
 * ~~~
94
 *
95
 * @property \DateInterval|string|null $defaultAccessTokenTTL
96
 * @since 1.0.0
97
 */
98
class Oauth2Module extends Oauth2BaseModule implements BootstrapInterface, DefaultAccessTokenTtlInterface
99
{
100
    use DefaultAccessTokenTtlTrait;
101
102
    /**
103
     * Application type "web": http response.
104
     * @since 1.0.0
105
     */
106
    public const APPLICATION_TYPE_WEB = 'web';
107
    /**
108
     * Application type "console": cli response.
109
     * @since 1.0.0
110
     */
111
    public const APPLICATION_TYPE_CONSOLE = 'console';
112
    /**
113
     * Supported Application types.
114
     * @since 1.0.0
115
     */
116
    public const APPLICATION_TYPES = [
117
        self::APPLICATION_TYPE_WEB,
118
        self::APPLICATION_TYPE_CONSOLE,
119
    ];
120
121
    /**
122
     * "Authorization Server" Role, please see guide for details.
123
     * @since 1.0.0
124
     */
125
    public const SERVER_ROLE_AUTHORIZATION_SERVER = 1;
126
    /**
127
     * "Resource Server" Role, please see guide for details.
128
     * @since 1.0.0
129
     */
130
    public const SERVER_ROLE_RESOURCE_SERVER = 2;
131
132
    /**
133
     * Required settings when the server role includes Authorization Server
134
     * @since 1.0.0
135
     */
136
    protected const REQUIRED_SETTINGS_AUTHORIZATION_SERVER = [
137
        'codesEncryptionKey',
138
        'storageEncryptionKeys',
139
        'defaultStorageEncryptionKey',
140
        'privateKey',
141
        'publicKey',
142
    ];
143
144
    /**
145
     * Encrypted Models
146
     *
147
     * @since 1.0.0
148
     */
149
    protected const ENCRYPTED_MODELS = [
150
        Oauth2ClientInterface::class,
151
    ];
152
153
    /**
154
     * Required settings when the server role includes Resource Server
155
     * @since 1.0.0
156
     */
157
    protected const REQUIRED_SETTINGS_RESOURCE_SERVER = [
158
        'publicKey',
159
    ];
160
161
    /**
162
     * Prefix used in session storage of Client Authorization Requests
163
     * @since 1.0.0
164
     */
165
    protected const CLIENT_AUTHORIZATION_REQUEST_SESSION_PREFIX = 'OAUTH2_CLIENT_AUTHORIZATION_REQUEST_';
166
167
    /**
168
     * Prefix used in session storage of End Session Authorization Requests.
169
     *
170
     * @since 1.0.0
171
     */
172
    protected const END_SESSION_AUTHORIZATION_REQUEST_SESSION_PREFIX = 'OAUTH2_END_SESSION_AUTHORIZATION_REQUEST_SESSION_PREFIX_'; // phpcs:ignore Generic.Files.LineLength.TooLong
173
174
    /**
175
     * Controller mapping for the module. Will be parsed on `init()`.
176
     * @since 1.0.0
177
     */
178
    protected const CONTROLLER_MAP = [
179
        self::APPLICATION_TYPE_WEB => [
180
            Oauth2ServerControllerInterface::CONTROLLER_NAME => [
181
                'controller' => Oauth2ServerControllerInterface::class,
182
                'serverRole' => self::SERVER_ROLE_AUTHORIZATION_SERVER,
183
            ],
184
            Oauth2ConsentControllerInterface::CONTROLLER_NAME => [
185
                'controller' => Oauth2ConsentControllerInterface::class,
186
                'serverRole' => self::SERVER_ROLE_AUTHORIZATION_SERVER,
187
            ],
188
            Oauth2WellKnownControllerInterface::CONTROLLER_NAME => [
189
                'controller' => Oauth2WellKnownControllerInterface::class,
190
                'serverRole' => self::SERVER_ROLE_AUTHORIZATION_SERVER,
191
            ],
192
            Oauth2CertificatesControllerInterface::CONTROLLER_NAME => [
193
                'controller' => Oauth2CertificatesControllerInterface::class,
194
                'serverRole' => self::SERVER_ROLE_AUTHORIZATION_SERVER,
195
            ],
196
            Oauth2OidcControllerInterface::CONTROLLER_NAME => [
197
                'controller' => Oauth2OidcControllerInterface::class,
198
                'serverRole' => self::SERVER_ROLE_AUTHORIZATION_SERVER,
199
            ],
200
        ],
201
        self::APPLICATION_TYPE_CONSOLE => [
202
            'migrations' => [
203
                'controller' => Oauth2MigrationsController::class,
204
                'serverRole' => self::SERVER_ROLE_AUTHORIZATION_SERVER | self::SERVER_ROLE_RESOURCE_SERVER,
205
            ],
206
            'client' => [
207
                'controller' => Oauth2ClientController::class,
208
                'serverRole' => self::SERVER_ROLE_AUTHORIZATION_SERVER,
209
            ],
210
            'encryption' => [
211
                'controller' => Oauth2EncryptionController::class,
212
                'serverRole' => self::SERVER_ROLE_AUTHORIZATION_SERVER,
213
            ],
214
            'debug' => [
215
                'controller' => Oauth2DebugController::class,
216
                'serverRole' => self::SERVER_ROLE_AUTHORIZATION_SERVER | self::SERVER_ROLE_RESOURCE_SERVER,
217
            ],
218
            'pat' => [
219
                'controller' => Oauth2PersonalAccessTokenController::class,
220
                'serverRole' => self::SERVER_ROLE_AUTHORIZATION_SERVER,
221
            ]
222
        ]
223
    ];
224
225
    /**
226
     * Offset of Bearer: in Authorization header
227
     */
228
    protected const BEARER_TOKEN_OFFSET = 7;
229
230
    /**
231
     * @inheritdoc
232
     */
233
    public $controllerNamespace = __NAMESPACE__ . '\-'; // Set explicitly via $controllerMap in `init()`.
234
235
    /**
236
     * @var string|null The application type. If `null` the type will be automatically detected.
237
     * @see APPLICATION_TYPES
238
     */
239
    public $appType = null;
240
241
    /**
242
     * @var int The Oauth 2.0 Server Roles the module will perform.
243
     * @since 1.0.0
244
     */
245
    public $serverRole = self::SERVER_ROLE_AUTHORIZATION_SERVER | self::SERVER_ROLE_RESOURCE_SERVER;
246
247
    /**
248
     * @var string|null The private key for the server. Can be a string containing the key itself or point to a file.
249
     * When pointing to a file it's recommended to use an absolute path prefixed with 'file://' or start with
250
     * '@' to use a Yii path alias.
251
     * @see $privateKeyPassphrase For setting a passphrase for the private key.
252
     * @since 1.0.0
253
     */
254
    public $privateKey = null;
255
256
    /**
257
     * @var string|null The passphrase for the private key.
258
     * @since 1.0.0
259
     */
260
    public $privateKeyPassphrase = null;
261
    /**
262
     * @var string|null The public key for the server. Can be a string containing the key itself or point to a file.
263
     * When pointing to a file it's recommended to use an absolute path prefixed with 'file://' or start with
264
     * '@' to use a Yii path alias.
265
     * @since 1.0.0
266
     */
267
    public $publicKey = null;
268
269
    /**
270
     * @var string|null The encryption key for authorization and refresh codes.
271
     * @since 1.0.0
272
     */
273
    public $codesEncryptionKey = null;
274
275
    /**
276
     * @var string[]|string|null The encryption keys for storage like client secrets.
277
     * Where the array key is the name of the key, and the value the key itself. E.g.
278
     * `['2022-01-01' => 'def00000cb36fd6ed6641e0ad70805b28d....']`
279
     * If a string (instead of an array of strings) is specified it will be JSON decoded
280
     * it should contain an object where each property name is the name of the key, its value the key itself. E.g.
281
     * `{"2022-01-01": "def00000cb36fd6ed6641e0ad70805b28d...."}`
282
     *
283
     * @since 1.0.0
284
     */
285
    public $storageEncryptionKeys = null;
286
287
    /**
288
     * @var string|null The index of the default key in storageEncryptionKeys. E.g. 'myKey'.
289
     * @since 1.0.0
290
     */
291
    public $defaultStorageEncryptionKey = null;
292
293
    /**
294
     * @var string|string[]|null IP addresses, CIDR ranges, or range aliases that are allowed to connect over a
295
     * non-TLS connection. If `null` or an empty array LTS is always required.
296
     *
297
     * Warning: Although you can use '*' or 'any' to allow a non-TLS connection from any ip address,
298
     * doing so would most likely introduce a security risk and should be done for debugging purposes only!
299
     *
300
     * @see \yii\validators\IpValidator::$networks for a list of available alliasses.
301
     */
302
    public $nonTlsAllowedRanges = 'localhost';
303
304
    /**
305
     * @var class-string<Oauth2UserInterface>|null The Identity Class of your application,
306
     * most likely the same as the 'identityClass' of your application's User Component.
307
     * @since 1.0.0
308
     */
309
    public $identityClass = null;
310
311
    /**
312
     * @var null|string Prefix used for url rules. When `null` the module's uniqueId will be used.
313
     * @since 1.0.0
314
     */
315
    public $urlRulesPrefix = null;
316
317
    /**
318
     * @var string URL path for the access token endpoint (will be prefixed with $urlRulesPrefix).
319
     * @since 1.0.0
320
     */
321
    public $authorizePath = 'authorize';
322
323
    /**
324
     * @var string URL path for the access token endpoint (will be prefixed with $urlRulesPrefix).
325
     * @since 1.0.0
326
     */
327
    public $accessTokenPath = 'access-token';
328
329
    /**
330
     * @var string URL path for the token revocation endpoint (will be prefixed with $urlRulesPrefix).
331
     * @since 1.0.0
332
     */
333
    public $tokenRevocationPath = 'revoke';
334
335
    /**
336
     * @var string URL path for the certificates jwks endpoint (will be prefixed with $urlRulesPrefix).
337
     * @since 1.0.0
338
     */
339
    public $jwksPath = 'certs';
340
341
    /**
342
     * The URL to the page where the user can perform the Client/Scope authorization
343
     * (if `null` the build in page will be used).
344
     * @return string
345
     * @since 1.0.0
346
     * @see $clientAuthorizationPath
347
     */
348
    public $clientAuthorizationUrl = null;
349
350
    /**
351
     * @var string The URL path to the build in page where the user can authorize the Client for the requested Scopes
352
     * (will be prefixed with $urlRulesPrefix).
353
     * Note: This setting will only be used if $clientAuthorizationUrl is `null`.
354
     * @since 1.0.0
355
     * @see $clientAuthorizationView
356
     */
357
    public $clientAuthorizationPath = 'authorize-client';
358
359
    /**
360
     * @var string The view to use in the "Client Authorization" action for the page where the user can
361
     * authorize the Client for the requested Scopes.
362
     * Note: This setting will only be used if $clientAuthorizationUrl is `null`.
363
     * @since 1.0.0
364
     * @see $clientAuthorizationPath
365
     */
366
    public $clientAuthorizationView = 'authorize-client';
367
368
    /**
369
     * @var bool Allow clients to invoke token revocation (RFC 7009).
370
     * @see https://datatracker.ietf.org/doc/html/rfc7009
371
     */
372
    public $enableTokenRevocation = true;
373
374
    /**
375
     * @var bool Will the server throw an exception when a Client requests an unknown or unauthorized scope
376
     * (would be silently ignored otherwise).
377
     * Note: this setting can be overwritten per client.
378
     */
379
    public $exceptionOnInvalidScope = false;
380
381
    /**
382
     * Configuration for `Oauth2Client::getRedirectUrisEnvVarConfig()` fallback (the
383
     * Oauth2Client::$envVarConfig['redirectUris'] has precedence).
384
     * When configured, environment variables specified in the `Oauth2Client` redirect URI(s) will be substituted with
385
     * their values. Please see `EnvironmentHelper::parseEnvVars()` for more details.
386
     *
387
     * Warning: This setting applies to all clients, for security it's recommended to specify this configuration at the
388
     * individual client level via its `envVarConfig` setting.
389
     *
390
     * @var array{
391
     *          allowList: array,
392
     *          denyList: array|null,
393
     *          parseNested: bool,
394
     *          exceptionWhenNotSet: bool,
395
     *          exceptionWhenNotAllowed: bool,
396
     *      }|null
397
     * @see Oauth2ClientInterface::setEnvVarConfig()
398
     * @see Oauth2ClientInterface::getRedirectUrisEnvVarConfig()
399
     * @see \rhertogh\Yii2Oauth2Server\helpers\EnvironmentHelper::parseEnvVars()
400
     */
401
    public $clientRedirectUrisEnvVarConfig = null;
402
403
    public $userAccountCreationUrl = null;
404
405
    /**
406
     * @var string|null The URL path to the OpenID Connect Provider Configuration Information Action.
407
     * If set to `null` the endpoint will be disabled.
408
     * Note: This path is defined in the
409
     *       [OpenID Connect Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.4)
410
     *       specification and should normally not be changed.
411
     * @since 1.0.0
412
     */
413
    public $openIdConnectProviderConfigurationInformationPath = '.well-known/openid-configuration';
414
415
    /**
416
     * @var string The URL path to the OpenID Connect Userinfo Action (will be prefixed with $urlRulesPrefix).
417
     * Note: This setting will only be used if $enableOpenIdConnect and $openIdConnectUserinfoEndpoint are `true`.
418
     * @since 1.0.0
419
     * @see $openIdConnectUserinfoEndpoint
420
     */
421
    public $openIdConnectUserinfoPath = 'oidc/userinfo';
422
423
    /**
424
     * @var string The URL path to the OpenID Connect End Session Action (will be prefixed with $urlRulesPrefix).
425
     * Note: This setting will only be used if $enableOpenIdConnect and
426
     * $openIdConnectRpInitiatedLogoutEndpoint are `true`.
427
     * @since 1.0.0
428
     * @see $openIdConnectRpInitiatedLogoutEndpoint
429
     * @see https://openid.net/specs/openid-connect-rpinitiated-1_0.html
430
     */
431
    public $openIdConnectRpInitiatedLogoutPath = 'oidc/end-session';
432
433
    /**
434
     * The URL to the page where the user can perform the End Session (logout) confirmation
435
     * (if `null` the build in page will be used).
436
     * @return string
437
     * @since 1.0.0
438
     * @see $openIdConnectLogoutConfirmationPath
439
     */
440
    public $openIdConnectLogoutConfirmationUrl = null;
441
442
    /**
443
     * @var string The URL path to the build in page where the user can confirm the End Session (logout) request
444
     * (will be prefixed with $urlRulesPrefix).
445
     * Note: This setting will only be used if $openIdConnectLogoutConfirmationUrl is `null`.
446
     * @since 1.0.0
447
     * @see $openIdConnectLogoutConfirmationView
448
     */
449
    public $openIdConnectLogoutConfirmationPath = 'confirm-logout';
450
451
    /**
452
     * @var string The view to use in the "End Session Authorization" action for the page where the user can
453
     * authorize the End Session (logout) request.
454
     * Note: This setting will only be used if $openIdConnectLogoutConfirmationUrl is `null`.
455
     * @since 1.0.0
456
     * @see $openIdConnectLogoutConfirmationPath
457
     */
458
    public $openIdConnectLogoutConfirmationView = 'confirm-logout';
459
460
461
    /**
462
     * @var Oauth2GrantTypeFactoryInterface[]|GrantTypeInterface[]|string[]|Oauth2GrantTypeFactoryInterface|GrantTypeInterface|string|callable
463
     * The Oauth 2.0 Grant Types that the module will serve.
464
     * @since 1.0.0
465
     */
466
    public $grantTypes = [];
467
468
    /**
469
     * @var bool Should the resource server check for revocation of the access token.
470
     * @since 1.0.0
471
     */
472
    public $resourceServerAccessTokenRevocationValidation = true;
473
474
    /**
475
     * @var bool Enable support for OpenIdvConnect.
476
     * @since 1.0.0
477
     */
478
    public $enableOpenIdConnect = false;
479
480
    /**
481
     * @var bool Enable the .well-known/openid-configuration discovery endpoint.
482
     * @since 1.0.0
483
     */
484
    public $enableOpenIdConnectDiscovery = true;
485
486
    /**
487
     * @var bool include `grant_types_supported` in the OpenIdConnect Discovery.
488
     * Note: Since grant types can be specified per client not all clients might support all enabled grant types.
489
     * @since 1.0.0
490
     */
491
    public $openIdConnectDiscoveryIncludeSupportedGrantTypes = true;
492
493
    /**
494
     * @var string URL to include in the OpenID Connect Discovery Service of a page containing
495
     * human-readable information that developers might want or need to know when using the OpenID Provider.
496
     * @see 'service_documentation' in https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3
497
     * @since 1.0.0
498
     */
499
    public $openIdConnectDiscoveryServiceDocumentationUrl = null;
500
501
    /**
502
     * @var string|bool A string to a custom userinfo endpoint or `true` to enable the build in endpoint.
503
     * @since 1.0.0
504
     * @see $openIdConnectUserinfoPath
505
     */
506
    public $openIdConnectUserinfoEndpoint = true;
507
508
    /**
509
     * @var string|bool A string to a custom logout endpoint or `true` to enable the build in endpoint.
510
     * @since 1.0.0
511
     * @see $openIdConnectRpInitiatedLogoutPath
512
     * @see https://openid.net/specs/openid-connect-rpinitiated-1_0.html
513
     */
514
    public $openIdConnectRpInitiatedLogoutEndpoint = false;
515
516
    /**
517
     * @var bool Allow access to the "end session" endpoint without user authentication (in the form of the
518
     * `id_token_hint` parameter). If enabled the "end session" endpoint will always prompt the user to verify the
519
     * logout if no `id_token_hint` is provided and no redirect after logout will be performed.
520
     * Note: If disabled the client's `oidc_rp_initiated_logout` will be used
521
     * to determine whether to prompt the end-user for logout validation.
522
     * @since 1.0.0
523
     * @see $openIdConnectRpInitiatedLogoutPath
524
     * @see https://openid.net/specs/openid-connect-rpinitiated-1_0.html
525
     */
526
    public $openIdConnectAllowAnonymousRpInitiatedLogout = false;
527
528
    /**
529
     * Warning! Enabling this setting might introduce privacy concerns since the client could poll for the
530
     * online status of a user.
531
     *
532
     * @var bool If this setting is disabled in case of OpenID Connect Context the Access Token won't include a
533
     * Refresh Token when the 'offline_access' scope is not included in the authorization request.
534
     * In some cases it might be needed to always include a Refresh Token, in that case enable this setting and
535
     * implement the `Oauth2OidcUserSessionStatusInterface` on the User Identity model.
536
     * @since 1.0.0
537
     */
538
    public $openIdConnectIssueRefreshTokenWithoutOfflineAccessScope = false;
539
540
    /**
541
     * @var int The default option for "User Account Selection' when not specified for a client.
542
     * @since 1.0.0
543
     */
544
    public $defaultUserAccountSelection = self::USER_ACCOUNT_SELECTION_DISABLED;
545
546
    /**
547
     * @var bool|null Display exception messages that might leak server details. This could be useful for debugging.
548
     * In case of `null` (default) the YII_DEBUG constant will be used.
549
     * Warning: Should NOT be enabled in production!
550
     * @since 1.0.0
551
     */
552
    public $displayConfidentialExceptionMessages = null;
553
554
    /**
555
     * @var string|null The namespace with which migrations will be created (and by which they will be located).
556
     * Note: The specified namespace must be defined as a Yii alias (e.g. '@app').
557
     * @since 1.0.0
558
     */
559
    public $migrationsNamespace = null;
560
    /**
561
     * @var string|null Optional prefix used in the name of generated migrations
562
     * @since 1.0.0
563
     */
564
    public $migrationsPrefix = null;
565
    /**
566
     * @var string|array|int|null Sets the file ownership of generated migrations
567
     * @see \yii\helpers\BaseFileHelper::changeOwnership()
568
     * @since 1.0.0
569
     */
570
    public $migrationsFileOwnership = null;
571
    /**
572
     * @var int|null Sets the file mode of generated migrations
573
     * @see \yii\helpers\BaseFileHelper::changeOwnership()
574
     * @since 1.0.0
575
     */
576
    public $migrationsFileMode = null;
577
578
    /**
579
     * The log level for HTTP Client Errors (HTTP status code 400 - 499). Can be one of the following:
580
     *  - A log level of `\yii\log\Logger` => LEVEL_ERROR, LEVEL_WARNING, LEVEL_INFO, LEVEL_TRACE.
581
     *  - `0` => disable logging for HTTP Client Errors
582
     *  - null => The `YII_DEBUG` constant will be used to determine the log level.
583
     *            If `true` LEVEL_ERROR will be used, LEVEL_INFO otherwise.
584
     * @var int|null
585
     * @see \yii\log\Logger
586
     */
587
    public $httpClientErrorsLogLevel = null;
588
589
    /**
590
     * @var Oauth2AuthorizationServerInterface|null Cache for the authorization server
591
     * @since 1.0.0
592
     */
593
    protected $_authorizationServer = null;
594
595
    /**
596
     * @var Oauth2ResourceServerInterface|null Cache for the resource server
597
     * @since 1.0.0
598
     */
599
    protected $_resourceServer = null;
600
601
    /**
602
     * @var Oauth2CryptographerInterface|null Cache for the Oauth2Cryptographer
603
     * @since 1.0.0
604
     */
605
    protected $_cryptographer = null;
606
607
    /**
608
     * @var string|null The authorization header used when the authorization request was validated.
609
     * @since 1.0.0
610
     */
611
    protected $_oauthClaimsAuthorizationHeader = null;
612
613
    /**
614
     * @inheritDoc
615
     * @throws InvalidConfigException
616
     */
617 161
    public function init()
618
    {
619 161
        parent::init();
620
621 161
        $app = Yii::$app;
622
623 161
        if ($app instanceof WebApplication || $this->appType == static::APPLICATION_TYPE_WEB) {
624 31
            $controllerMap = static::CONTROLLER_MAP[static::APPLICATION_TYPE_WEB];
625 161
        } elseif ($app instanceof ConsoleApplication || $this->appType == static::APPLICATION_TYPE_CONSOLE) {
626 161
            $controllerMap = static::CONTROLLER_MAP[static::APPLICATION_TYPE_CONSOLE];
627 161
            $this->defaultRoute = 'debug';
628
        } else {
629 1
            throw new InvalidConfigException(
630 1
                'Unable to detect application type, configure it manually by setting `$appType`.'
631 1
            );
632
        }
633 161
        $controllerMap = array_filter(
634 161
            $controllerMap,
635 161
            fn($controllerSettings) => $controllerSettings['serverRole'] & $this->serverRole
636 161
        );
637 161
        $this->controllerMap = ArrayHelper::getColumn($controllerMap, 'controller');
638
639 161
        if (empty($this->identityClass)) {
640 1
            throw new InvalidConfigException('$identityClass must be set.');
641 161
        } elseif (!is_a($this->identityClass, Oauth2UserInterface::class, true)) {
642 1
            throw new InvalidConfigException(
643 1
                $this->identityClass . ' must implement ' . Oauth2UserInterface::class
644 1
            );
645
        }
646
647 161
        foreach (static::DEFAULT_INTERFACE_IMPLEMENTATIONS as $interface => $implementation) {
648 161
            if (!Yii::$container->has($interface)) {
649 161
                Yii::$container->set($interface, $implementation);
650
            }
651
        }
652
653 161
        if (empty($this->urlRulesPrefix)) {
654 161
            $this->urlRulesPrefix = $this->uniqueId;
655
        }
656
657 161
        $this->registerTranslations();
658
    }
659
660
    /**
661
     * @inheritdoc
662
     * @throws InvalidConfigException
663
     */
664 161
    public function bootstrap($app)
665
    {
666
        if (
667 161
            $app instanceof WebApplication
668 161
            && $this->serverRole & static::SERVER_ROLE_AUTHORIZATION_SERVER
669
        ) {
670 31
            $rules = [
671 31
                $this->accessTokenPath => Oauth2ServerControllerInterface::CONTROLLER_NAME
672 31
                    . '/' . Oauth2ServerControllerInterface::ACTION_NAME_ACCESS_TOKEN,
673 31
                $this->authorizePath => Oauth2ServerControllerInterface::CONTROLLER_NAME
674 31
                    . '/' . Oauth2ServerControllerInterface::ACTION_NAME_AUTHORIZE,
675 31
                $this->jwksPath => Oauth2CertificatesControllerInterface::CONTROLLER_NAME
676 31
                    . '/' . Oauth2CertificatesControllerInterface::ACTION_NAME_JWKS,
677 31
            ];
678
679 31
            if ($this->enableTokenRevocation) {
680 31
                $rules[$this->tokenRevocationPath] = Oauth2ServerControllerInterface::CONTROLLER_NAME
681 31
                . '/' . Oauth2ServerControllerInterface::ACTION_NAME_REVOKE;
682
            }
683
684 31
            if (empty($this->clientAuthorizationUrl)) {
685 30
                $rules[$this->clientAuthorizationPath] = Oauth2ConsentControllerInterface::CONTROLLER_NAME
686 30
                    . '/' . Oauth2ConsentControllerInterface::ACTION_NAME_AUTHORIZE_CLIENT;
687
            }
688
689 31
            if ($this->enableOpenIdConnect && $this->openIdConnectUserinfoEndpoint === true) {
690 31
                $rules[$this->openIdConnectUserinfoPath] =
691 31
                    Oauth2OidcControllerInterface::CONTROLLER_NAME
692 31
                    . '/' . Oauth2OidcControllerInterface::ACTION_NAME_USERINFO;
693
            }
694
695 31
            if ($this->enableOpenIdConnect && $this->openIdConnectRpInitiatedLogoutEndpoint === true) {
696 31
                $rules[$this->openIdConnectRpInitiatedLogoutPath] =
697 31
                    Oauth2OidcControllerInterface::CONTROLLER_NAME
698 31
                    . '/' . Oauth2OidcControllerInterface::ACTION_END_SESSION;
699
700 31
                if (empty($this->openIdConnectLogoutConfirmationUrl)) {
701 31
                    $rules[$this->openIdConnectLogoutConfirmationPath] =
702 31
                        Oauth2ConsentControllerInterface::CONTROLLER_NAME
703 31
                        . '/' . Oauth2ConsentControllerInterface::ACTION_NAME_AUTHORIZE_END_SESSION;
704
                }
705
            }
706
707 31
            $urlManager = $app->getUrlManager();
708 31
            $urlManager->addRules([
709 31
                Yii::createObject([
710 31
                    'class' => GroupUrlRule::class,
711 31
                    'prefix' => $this->urlRulesPrefix,
712 31
                    'routePrefix' => $this->id,
713 31
                    'rules' => $rules,
714 31
                ]),
715 31
            ]);
716
717
            if (
718 31
                $this->enableOpenIdConnect
719 31
                && $this->enableOpenIdConnectDiscovery
720 31
                && $this->openIdConnectProviderConfigurationInformationPath
721
            ) {
722 31
                $urlManager->addRules([
723 31
                    Yii::createObject([
724 31
                        'class' => UrlRule::class,
725 31
                        'pattern' => $this->openIdConnectProviderConfigurationInformationPath,
726 31
                        'route' => $this->id
727 31
                            . '/' . Oauth2WellKnownControllerInterface::CONTROLLER_NAME
728 31
                            . '/' . Oauth2WellKnownControllerInterface::ACTION_NAME_OPENID_CONFIGURATION,
729 31
                    ]),
730 31
                ]);
731
            }
732
        }
733
    }
734
735
    /**
736
     * Registers the translations for the module
737
     * @param bool $force Force the setting of the translations (even if they are already defined).
738
     * @since 1.0.0
739
     */
740 161
    public function registerTranslations($force = false)
741
    {
742 161
        if ($force || !array_key_exists('oauth2', Yii::$app->i18n->translations)) {
743 161
            Yii::$app->i18n->translations['oauth2'] = [
744 161
                'class' => PhpMessageSource::class,
745 161
                'sourceLanguage' => 'en-US',
746 161
                'basePath' => __DIR__ . DIRECTORY_SEPARATOR . 'messages',
747 161
                'fileMap' => [
748 161
                    'oauth2' => 'oauth2.php',
749 161
                ],
750 161
            ];
751
        }
752
    }
753
754
    /**
755
     * @param string $identifier The client identifier
756
     * @param string $name The (user-friendly) name of the client
757
     * @param int $grantTypes The grant types enabled for this client.
758
     *        Use bitwise `OR` to combine multiple types,
759
     *        e.g. `Oauth2Module::GRANT_TYPE_AUTH_CODE | Oauth2Module::GRANT_TYPE_REFRESH_TOKEN`
760
     * @param string|string[] $redirectURIs One or multiple redirect URIs for the client
761
     * @param int $type The client type (e.g. Confidential or Public)
762
     *        See `\rhertogh\Yii2Oauth2Server\interfaces\models\Oauth2ClientInterface::TYPES` for possible values
763
     * @param string|null $secret The client secret in case the client `type` is `confidential`.
764
     * @param string|string[]|array[]|Oauth2ScopeInterface[]|null $scopes
765
     * @param int|null $userId
766
     * @return Oauth2ClientInterface
767
     * @throws InvalidConfigException
768
     * @throws \yii\db\Exception
769
     */
770 5
    public function createClient(
771
        $identifier,
772
        $name,
773
        $grantTypes,
774
        $redirectURIs,
775
        $type,
776
        $secret = null,
777
        $scopes = null,
778
        $userId = null,
779
        $endUsersMayAuthorizeClient = null,
780
        $skipAuthorizationIfScopeIsAllowed = null
781
    ) {
782 5
        if (!($this->serverRole & static::SERVER_ROLE_AUTHORIZATION_SERVER)) {
783 1
            throw new InvalidCallException('Oauth2 server role does not include authorization server.');
784
        }
785
786
        /** @var Oauth2ClientInterface $client */
787 4
        $client = Yii::createObject([
788 4
            'class' => Oauth2ClientInterface::class,
789 4
            'identifier' => $identifier,
790 4
            'type' => $type,
791 4
            'name' => $name,
792 4
            'redirectUri' => $redirectURIs,
793 4
            'grantTypes' => $grantTypes,
794 4
            'endUsersMayAuthorizeClient' => $endUsersMayAuthorizeClient,
795 4
            'skip_authorization_if_scope_is_allowed' => $skipAuthorizationIfScopeIsAllowed,
796 4
            'clientCredentialsGrantUserId' => $userId
797 4
        ]);
798
799 4
        $transaction = $client::getDb()->beginTransaction();
800
801
        try {
802 4
            if ($type == Oauth2ClientInterface::TYPE_CONFIDENTIAL) {
803 4
                $client->setSecret($secret, $this->getCryptographer());
804
            }
805
806 3
            $client
807 3
                ->persist()
808 3
                ->syncClientScopes($scopes, $this->getScopeRepository());
809
810 3
            $transaction->commit();
811 1
        } catch (\Exception $e) {
812 1
            $transaction->rollBack();
813 1
            throw $e;
814
        }
815
816 3
        return $client;
817
    }
818
819
    /**
820
     * @return CryptKey The private key of the server.
821
     * @throws InvalidConfigException
822
     * @since 1.0.0
823
     */
824 22
    public function getPrivateKey()
825
    {
826 22
        $privateKey = $this->privateKey;
827 22
        if (StringHelper::startsWith($privateKey, '@')) {
828 19
            $privateKey = 'file://' . Yii::getAlias($privateKey);
829
        }
830 22
        return Yii::createObject(CryptKey::class, [$privateKey, $this->privateKeyPassphrase]);
831
    }
832
833
    /**
834
     * @return CryptKey The public key of the server.
835
     * @throws InvalidConfigException
836
     * @since 1.0.0
837
     */
838 9
    public function getPublicKey()
839
    {
840 9
        $publicKey = $this->publicKey;
841 9
        if (StringHelper::startsWith($publicKey, '@')) {
842 6
            $publicKey = 'file://' . Yii::getAlias($publicKey);
843
        }
844 9
        return Yii::createObject(CryptKey::class, [$publicKey]);
845
    }
846
847
    /**
848
     * @return Oauth2AuthorizationServerInterface The authorization server.
849
     * @throws InvalidConfigException
850
     * @since 1.0.0
851
     */
852 27
    public function getAuthorizationServer()
853
    {
854 27
        if (!($this->serverRole & static::SERVER_ROLE_AUTHORIZATION_SERVER)) {
855 1
            throw new InvalidCallException('Oauth2 server role does not include authorization server.');
856
        }
857
858 26
        if (!$this->_authorizationServer) {
859 26
            $this->ensureProperties(static::REQUIRED_SETTINGS_AUTHORIZATION_SERVER);
860
861 21
            if (!$this->getCryptographer()->hasKey($this->defaultStorageEncryptionKey)) {
862 1
                throw new InvalidConfigException(
863 1
                    'Key "' . $this->defaultStorageEncryptionKey . '" is not set in $storageEncryptionKeys'
864 1
                );
865
            }
866
867
            /** @var Oauth2EncryptionKeyFactoryInterface $keyFactory */
868 19
            $keyFactory = Yii::createObject(Oauth2EncryptionKeyFactoryInterface::class);
869
            try {
870 19
                $codesEncryptionKey = $keyFactory->createFromAsciiSafeString($this->codesEncryptionKey);
871 1
            } catch (BadFormatException $e) {
872 1
                throw new InvalidConfigException(
873 1
                    '$codesEncryptionKey is malformed: ' . $e->getMessage(),
874 1
                    0,
875 1
                    $e
876 1
                );
877
            } catch (EnvironmentIsBrokenException $e) {
878
                throw new InvalidConfigException(
879
                    'Could not instantiate $codesEncryptionKey: ' . $e->getMessage(),
880
                    0,
881
                    $e
882
                );
883
            }
884
885 18
            if ($this->enableOpenIdConnect) {
886 18
                $responseTypeClass = Oauth2OidcBearerTokenResponseInterface::class;
887
            } else {
888
                $responseTypeClass = Oauth2BearerTokenResponseInterface::class;
889
            }
890 18
            $responseType = Yii::createObject($responseTypeClass, [
891 18
                $this,
892 18
            ]);
893
894 18
            $this->_authorizationServer = Yii::createObject(Oauth2AuthorizationServerInterface::class, [
895 18
                $this->getClientRepository(),
896 18
                $this->getAccessTokenRepository(),
897 18
                $this->getScopeRepository(),
898 18
                $this->getPrivateKey(),
899 18
                $codesEncryptionKey,
900 18
                $responseType
901 18
            ]);
902
903 18
            if (!empty($this->grantTypes)) {
904 18
                $grantTypes = $this->grantTypes;
905
906 18
                if (is_callable($grantTypes)) {
907 1
                    call_user_func($grantTypes, $this->_authorizationServer, $this);
908
                } else {
909 17
                    if (!is_array($grantTypes)) {
910 2
                        $grantTypes = [$grantTypes];
911
                    }
912
913 17
                    foreach ($grantTypes as $grantTypeDefinition) {
914 17
                        if ($grantTypeDefinition instanceof GrantTypeInterface) {
915 1
                            $accessTokenTTL = $this->getDefaultAccessTokenTTL();
916 1
                            $this->_authorizationServer->enableGrantType($grantTypeDefinition, $accessTokenTTL);
917
                        } elseif (
918
                            (
919 16
                                is_numeric($grantTypeDefinition)
920 16
                                && array_key_exists($grantTypeDefinition, static::DEFAULT_GRANT_TYPE_FACTORIES)
921
                            )
922 16
                            || is_a($grantTypeDefinition, Oauth2GrantTypeFactoryInterface::class, true)
923
                        ) {
924
                            if (
925 15
                                is_numeric($grantTypeDefinition)
926 15
                                && array_key_exists($grantTypeDefinition, static::DEFAULT_GRANT_TYPE_FACTORIES)
927
                            ) {
928 15
                                $grantTypeDefinition = static::DEFAULT_GRANT_TYPE_FACTORIES[$grantTypeDefinition];
929
                            }
930
931
                            /** @var Oauth2GrantTypeFactoryInterface $factory */
932 15
                            $factory = Yii::createObject([
933 15
                                'class' => $grantTypeDefinition,
934 15
                                'module' => $this,
935 15
                            ]);
936 15
                            $accessTokenTTL = $factory->getDefaultAccessTokenTTL()
937 15
                                ?? $this->getDefaultAccessTokenTTL();
938 15
                            $this->_authorizationServer->enableGrantType($factory->getGrantType(), $accessTokenTTL);
939
                        } else {
940 1
                            throw new InvalidConfigException(
941 1
                                'Unknown grantType '
942 1
                                . (
943 1
                                    is_scalar($grantTypeDefinition)
944 1
                                        ? '"' . $grantTypeDefinition . '".'
945 1
                                        : 'with data type ' . gettype($grantTypeDefinition)
946 1
                                )
947 1
                            );
948
                        }
949
                    }
950
                }
951
            }
952
        }
953
954 17
        return $this->_authorizationServer;
955
    }
956
957
    /**
958
     * @inheritDoc
959
     * @throws InvalidConfigException
960
     */
961 6
    public function getOidcScopeCollection()
962
    {
963 6
        if ($this->_oidcScopeCollection === null) {
964 6
            $openIdConnectScopes = $this->getOpenIdConnectScopes();
965 6
            if ($openIdConnectScopes instanceof Oauth2OidcScopeCollectionInterface) {
966 1
                $this->_oidcScopeCollection = $openIdConnectScopes;
967 5
            } elseif (is_callable($openIdConnectScopes)) {
968 1
                $this->_oidcScopeCollection = call_user_func($openIdConnectScopes, $this);
969 1
                if (!($this->_oidcScopeCollection instanceof Oauth2OidcScopeCollectionInterface)) {
970 1
                    throw new InvalidConfigException(
971 1
                        '$openIdConnectScopes must return an instance of '
972 1
                            . Oauth2OidcScopeCollectionInterface::class
973 1
                    );
974
                }
975 4
            } elseif (is_array($openIdConnectScopes) || is_string($openIdConnectScopes)) {
976 3
                $this->_oidcScopeCollection = Yii::createObject([
977 3
                    'class' => Oauth2OidcScopeCollectionInterface::class,
978 3
                    'oidcScopes' => (array)$openIdConnectScopes,
979 3
                ]);
980
            } else {
981 1
                throw new InvalidConfigException(
982 1
                    '$openIdConnectScopes must be a callable, array, string or '
983 1
                        . Oauth2OidcScopeCollectionInterface::class
984 1
                );
985
            }
986
        }
987
988 5
        return $this->_oidcScopeCollection;
989
    }
990
991
    /**
992
     * @return Oauth2ResourceServerInterface The resource server.
993
     * @throws InvalidConfigException
994
     * @since 1.0.0
995
     */
996 7
    public function getResourceServer()
997
    {
998 7
        if (!($this->serverRole & static::SERVER_ROLE_RESOURCE_SERVER)) {
999 1
            throw new InvalidCallException('Oauth2 server role does not include resource server.');
1000
        }
1001
1002 6
        if (!$this->_resourceServer) {
1003 6
            $this->ensureProperties(static::REQUIRED_SETTINGS_RESOURCE_SERVER);
1004
1005 5
            $accessTokenRepository = $this->getAccessTokenRepository()
1006 5
                ->setRevocationValidation($this->resourceServerAccessTokenRevocationValidation);
1007
1008 5
            $this->_resourceServer = Yii::createObject(Oauth2ResourceServerInterface::class, [
1009 5
                $accessTokenRepository,
1010 5
                $this->getPublicKey(),
1011 5
            ]);
1012
        }
1013
1014 5
        return $this->_resourceServer;
1015
    }
1016
1017
    /**
1018
     * @return Oauth2CryptographerInterface The data cryptographer for the module.
1019
     * @throws InvalidConfigException
1020
     * @since 1.0.0
1021
     */
1022 27
    public function getCryptographer()
1023
    {
1024 27
        if (!$this->_cryptographer) {
1025 27
            $this->_cryptographer = Yii::createObject([
1026 27
                'class' => Oauth2CryptographerInterface::class,
1027 27
                'keys' => $this->storageEncryptionKeys,
1028 27
                'defaultKeyName' => $this->defaultStorageEncryptionKey,
1029 27
            ]);
1030
        }
1031
1032 26
        return $this->_cryptographer;
1033
    }
1034
1035
    /**
1036
     * @param string|null $newKeyName
1037
     * @return array
1038
     * @throws InvalidConfigException
1039
     */
1040 1
    public function rotateStorageEncryptionKeys($newKeyName = null)
1041
    {
1042 1
        $cryptographer = $this->getCryptographer();
1043
1044 1
        $result = [];
1045 1
        foreach (static::ENCRYPTED_MODELS as $modelInterface) {
1046 1
            $modelClass = DiHelper::getValidatedClassName($modelInterface);
1047 1
            if (!is_a($modelClass, Oauth2EncryptedStorageInterface::class, true)) {
1048
                throw new InvalidConfigException($modelInterface . ' must implement '
1049
                    . Oauth2EncryptedStorageInterface::class);
1050
            }
1051 1
            $result[$modelClass] = $modelClass::rotateStorageEncryptionKeys($cryptographer, $newKeyName);
1052
        }
1053
1054 1
        return $result;
1055
    }
1056
1057
    /**
1058
     * Checks if the connection is using TLS or if the remote IP address is allowed to connect without TLS.
1059
     * @return bool
1060
     */
1061 12
    public function validateTlsConnection()
1062
    {
1063 12
        if (Yii::$app->request->getIsSecureConnection()) {
1064 1
            return true;
1065
        }
1066
1067
        if (
1068 11
            !empty($this->nonTlsAllowedRanges)
1069 11
            && (new IpValidator(['ranges' => $this->nonTlsAllowedRanges]))->validate(Yii::$app->request->getRemoteIP())
1070
        ) {
1071 7
            return true;
1072
        }
1073
1074 4
        return false;
1075
    }
1076
1077
    /**
1078
     * @return array
1079
     * @throws InvalidConfigException
1080
     */
1081
    public function getStorageEncryptionKeyUsage()
1082
    {
1083
        $cryptographer = $this->getCryptographer();
1084
1085
        $result = [];
1086
        foreach (static::ENCRYPTED_MODELS as $modelInterface) {
1087
            $modelClass = DiHelper::getValidatedClassName($modelInterface);
1088
            if (!is_a($modelClass, Oauth2EncryptedStorageInterface::class, true)) {
1089
                throw new InvalidConfigException($modelInterface . ' must implement '
1090
                    . Oauth2EncryptedStorageInterface::class);
1091
            }
1092
1093
            $result[$modelClass] = $modelClass::getUsedStorageEncryptionKeys($cryptographer);
1094
        }
1095
1096
        return $result;
1097
    }
1098
1099
    /**
1100
     * @param Oauth2ClientInterface $client
1101
     * @param string[] $requestedScopeIdentifiers
1102
     * @throws Oauth2ServerException
1103
     */
1104 6
    public function validateAuthRequestScopes($client, $requestedScopeIdentifiers, $redirectUri = null)
1105
    {
1106 6
        if (!$client->validateAuthRequestScopes($requestedScopeIdentifiers, $unknownScopes, $unauthorizedScopes)) {
1107
            Yii::info('Invalid scope for client "' . $client->getIdentifier() . '": '
1108
                . VarDumper::export(['unauthorizedScopes' => $unauthorizedScopes, 'unknownScopes' => $unknownScopes]));
1109
1110
            if (
1111
                $client->getExceptionOnInvalidScope() === true
1112
                || (
1113
                    $client->getExceptionOnInvalidScope() === null
1114
                    && $this->exceptionOnInvalidScope === true
1115
                )
1116
            ) {
1117
                if ($unknownScopes) {
1118
                    throw Oauth2ServerException::unknownScope(array_shift($unknownScopes), $redirectUri);
1119
                }
1120
                throw Oauth2ServerException::scopeNotAllowedForClient(array_shift($unauthorizedScopes), $redirectUri);
1121
            }
1122
        }
1123
    }
1124
1125
    /**
1126
     * Generates a redirect Response to the Client Authorization page where the user is prompted to authorize the
1127
     * Client and requested Scope.
1128
     * @param Oauth2ClientAuthorizationRequestInterface $clientAuthorizationRequest
1129
     * @return Response
1130
     * @since 1.0.0
1131
     */
1132 5
    public function generateClientAuthReqRedirectResponse($clientAuthorizationRequest)
1133
    {
1134 5
        $this->setClientAuthReqSession($clientAuthorizationRequest);
1135 5
        if (!empty($this->clientAuthorizationUrl)) {
1136 1
            $url = $this->clientAuthorizationUrl;
1137
        } else {
1138 4
            $url = $this->uniqueId
1139 4
                . '/' . Oauth2ConsentControllerInterface::CONTROLLER_NAME
1140 4
                . '/' . Oauth2ConsentControllerInterface::ACTION_NAME_AUTHORIZE_CLIENT;
1141
        }
1142 5
        return Yii::$app->response->redirect([
1143 5
            $url,
1144 5
            'clientAuthorizationRequestId' => $clientAuthorizationRequest->getRequestId(),
1145 5
        ]);
1146
    }
1147
1148
    /**
1149
     * Generates a redirect Response to the End Session Authorization page where the user is prompted to authorize the
1150
     * logout.
1151
     * @param Oauth2EndSessionAuthorizationRequestInterface $endSessionAuthorizationRequest
1152
     * @return Response
1153
     * @since 1.0.0
1154
     */
1155
    public function generateEndSessionAuthReqRedirectResponse($endSessionAuthorizationRequest)
1156
    {
1157
        $this->setEndSessionAuthReqSession($endSessionAuthorizationRequest);
1158
        if (!empty($this->openIdConnectLogoutConfirmationUrl)) {
1159
            $url = $this->openIdConnectLogoutConfirmationUrl;
1160
        } else {
1161
            $url = $this->uniqueId
1162
                . '/' . Oauth2ConsentControllerInterface::CONTROLLER_NAME
1163
                . '/' . Oauth2ConsentControllerInterface::ACTION_NAME_AUTHORIZE_END_SESSION;
1164
        }
1165
        return Yii::$app->response->redirect([
1166
            $url,
1167
            'endSessionAuthorizationRequestId' => $endSessionAuthorizationRequest->getRequestId(),
1168
        ]);
1169
    }
1170
1171
    /**
1172
     * Get a previously stored Client Authorization Request from the session.
1173
     * @param string $requestId
1174
     * @return Oauth2ClientAuthorizationRequestInterface|null
1175
     * @since 1.0.0
1176
     */
1177 5
    public function getClientAuthReqSession($requestId)
1178
    {
1179 5
        return $this->getAuthReqSession(
1180 5
            $requestId,
1181 5
            static::CLIENT_AUTHORIZATION_REQUEST_SESSION_PREFIX,
1182 5
            Oauth2ClientAuthorizationRequestInterface::class,
1183 5
        );
1184
    }
1185
1186
    /**
1187
     * Get a previously stored OIDC End Session Authorization Request from the session.
1188
     * @param string $requestId
1189
     * @return Oauth2EndSessionAuthorizationRequestInterface|null
1190
     * @since 1.0.0
1191
     */
1192
    public function getEndSessionAuthReqSession($requestId)
1193
    {
1194
        return $this->getAuthReqSession(
1195
            $requestId,
1196
            static::END_SESSION_AUTHORIZATION_REQUEST_SESSION_PREFIX,
1197
            Oauth2EndSessionAuthorizationRequestInterface::class,
1198
        );
1199
    }
1200
1201
    /**
1202
     * Get a previously stored Authorization Request from the session.
1203
     * @template T of Oauth2BaseAuthorizationRequestInterface
1204
     * @param string $requestId
1205
     * @param string $cachePrefix
1206
     * @param class-string<T> $expectedInterface
1207
     * @return T|null
1208
     * @since 1.0.0
1209
     */
1210 5
    protected function getAuthReqSession($requestId, $cachePrefix, $expectedInterface)
1211
    {
1212 5
        if (empty($requestId)) {
1213
            return null;
1214
        }
1215 5
        $key = $cachePrefix . $requestId;
1216 5
        $authorizationRequest = Yii::$app->session->get($key);
1217 5
        if (!($authorizationRequest instanceof $expectedInterface)) {
1218 2
            if (!empty($authorizationRequest)) {
1219 1
                Yii::warning(
1220 1
                    'Found a Authorization Request Session with key "' . $key
1221 1
                    . '", but it\'s not a ' . $expectedInterface
1222 1
                );
1223
            }
1224 2
            return null;
1225
        }
1226 5
        if ($authorizationRequest->getRequestId() !== $requestId) {
1227 1
            Yii::warning(
1228 1
                'Found a Authorization Request Session with key "' . $key
1229 1
                . '", but its request id does not match "' . $requestId . '".'
1230 1
            );
1231 1
            return null;
1232
        }
1233 5
        $authorizationRequest->setModule($this);
1234
1235 5
        return $authorizationRequest;
1236
    }
1237
1238
    /**
1239
     * Stores the Client Authorization Request in the session.
1240
     * @param Oauth2ClientAuthorizationRequestInterface $clientAuthorizationRequest
1241
     * @since 1.0.0
1242
     */
1243 8
    public function setClientAuthReqSession($clientAuthorizationRequest)
1244
    {
1245 8
        $this->setAuthReqSession(
1246 8
            $clientAuthorizationRequest,
1247 8
            static::CLIENT_AUTHORIZATION_REQUEST_SESSION_PREFIX
1248 8
        );
1249
    }
1250
1251
    /**
1252
     * Stores the OIDC End Session Authorization Request in the session.
1253
     * @param Oauth2EndSessionAuthorizationRequestInterface $endSessionAuthorizationRequest
1254
     * @since 1.0.0
1255
     */
1256
    public function setEndSessionAuthReqSession($endSessionAuthorizationRequest)
1257
    {
1258
        $this->setAuthReqSession(
1259
            $endSessionAuthorizationRequest,
1260
            static::END_SESSION_AUTHORIZATION_REQUEST_SESSION_PREFIX
1261
        );
1262
    }
1263
1264
    /**
1265
     * Stores the Authorization Request in the session.
1266
     * @param Oauth2BaseAuthorizationRequestInterface $authorizationRequest
1267
     * @param string $cachePrefix
1268
     * @since 1.0.0
1269
     */
1270 8
    protected function setAuthReqSession($authorizationRequest, $cachePrefix)
1271
    {
1272 8
        $requestId = $authorizationRequest->getRequestId();
1273 8
        if (empty($requestId)) {
1274 1
            throw new InvalidArgumentException('$authorizationRequest must return a request id.');
1275
        }
1276 7
        $key = $cachePrefix . $requestId;
1277 7
        Yii::$app->session->set($key, $authorizationRequest);
1278
    }
1279
1280
    /**
1281
     * Clears a Client Authorization Request from the session storage.
1282
     * @param string $requestId
1283
     * @since 1.0.0
1284
     */
1285 2
    public function removeClientAuthReqSession($requestId)
1286
    {
1287 2
        $this->removeAuthReqSession($requestId, static::CLIENT_AUTHORIZATION_REQUEST_SESSION_PREFIX);
1288
    }
1289
1290
    /**
1291
     * Clears an End Session Authorization Request from the session storage.
1292
     * @param string $requestId
1293
     * @since 1.0.0
1294
     */
1295
    public function removeEndSessionAuthReqSession($requestId)
1296
    {
1297
        $this->removeAuthReqSession($requestId, static::END_SESSION_AUTHORIZATION_REQUEST_SESSION_PREFIX);
1298
    }
1299
1300
    /**
1301
     * Clears an Authorization Request from the session storage.
1302
     * @param string $requestId
1303
     * @param string $cachePrefix
1304
     * @since 1.0.0
1305
     */
1306 2
    public function removeAuthReqSession($requestId, $cachePrefix)
1307
    {
1308 2
        if (empty($requestId)) {
1309 1
            throw new InvalidArgumentException('$requestId can not be empty.');
1310
        }
1311 1
        $key = $cachePrefix . $requestId;
1312 1
        Yii::$app->session->remove($key);
1313
    }
1314
1315
    /**
1316
     * Stores whether the user was authenticated during the completion of the Client Authorization Request.
1317
     * @param string $clientAuthorizationRequestId
1318
     * @param bool $authenticatedDuringRequest
1319
     * @since 1.0.0
1320
     */
1321
    public function setUserAuthenticatedDuringClientAuthRequest(
1322
        $clientAuthorizationRequestId,
1323
        $authenticatedDuringRequest
1324
    ) {
1325
        $clientAuthorizationRequest = $this->getClientAuthReqSession($clientAuthorizationRequestId);
1326
        if ($clientAuthorizationRequest) {
1327
            $clientAuthorizationRequest->setUserAuthenticatedDuringRequest($authenticatedDuringRequest);
1328
            $this->setClientAuthReqSession($clientAuthorizationRequest);
1329
        }
1330
    }
1331
1332
    /**
1333
     * Stores the user identity selected during the completion of the Client Authorization Request.
1334
     * @param string $clientAuthorizationRequestId
1335
     * @param Oauth2UserInterface $userIdentity
1336
     * @since 1.0.0
1337
     */
1338
    public function setClientAuthRequestUserIdentity($clientAuthorizationRequestId, $userIdentity)
1339
    {
1340
        $clientAuthorizationRequest = $this->getClientAuthReqSession($clientAuthorizationRequestId);
1341
        if ($clientAuthorizationRequest) {
1342
            $clientAuthorizationRequest->setUserIdentity($userIdentity);
1343
            $this->setClientAuthReqSession($clientAuthorizationRequest);
1344
        }
1345
    }
1346
1347
    /**
1348
     * Generates a redirect Response when the Client Authorization Request is completed.
1349
     * @param Oauth2ClientAuthorizationRequestInterface $clientAuthorizationRequest
1350
     * @return Response
1351
     * @since 1.0.0
1352
     */
1353 1
    public function generateClientAuthReqCompledRedirectResponse($clientAuthorizationRequest)
1354
    {
1355 1
        $clientAuthorizationRequest->processAuthorization();
1356 1
        $this->setClientAuthReqSession($clientAuthorizationRequest);
1357 1
        return Yii::$app->response->redirect($clientAuthorizationRequest->getAuthorizationRequestUrl());
1358
    }
1359
1360
    /**
1361
     * Generates a redirect Response when the End Session Authorization Request is completed.
1362
     * @param Oauth2EndSessionAuthorizationRequestInterface $endSessionAuthorizationRequest
1363
     * @return Response
1364
     * @since 1.0.0
1365
     */
1366
    public function generateEndSessionAuthReqCompledRedirectResponse($endSessionAuthorizationRequest)
1367
    {
1368
        $endSessionAuthorizationRequest->processAuthorization();
1369
        $this->setEndSessionAuthReqSession($endSessionAuthorizationRequest);
1370
        return Yii::$app->response->redirect($endSessionAuthorizationRequest->getEndSessionRequestUrl());
1371
    }
1372
1373
    /**
1374
     * @return IdentityInterface|Oauth2UserInterface|Oauth2OidcUserInterface|null
1375
     * @throws InvalidConfigException
1376
     * @since 1.0.0
1377
     */
1378 5
    public function getUserIdentity()
1379
    {
1380 5
        $user = Yii::$app->user->identity;
1381 5
        if (!empty($user) && !($user instanceof Oauth2UserInterface)) {
1382 1
            throw new InvalidConfigException(
1383 1
                'Yii::$app->user->identity (currently ' . get_class($user)
1384 1
                    . ') must implement ' . Oauth2UserInterface::class
1385 1
            );
1386
        }
1387 4
        return $user;
1388
    }
1389
1390
    /**
1391
     * Validates a bearer token authenticated request. Note: this method does not return a result but will throw
1392
     * an exception when the authentication fails.
1393
     * @throws InvalidConfigException
1394
     * @throws Oauth2ServerException
1395
     * @since 1.0.0
1396
     */
1397 3
    public function validateAuthenticatedRequest()
1398
    {
1399 3
        $psr7Request = Psr7Helper::yiiToPsr7Request(Yii::$app->request);
1400
1401 3
        $psr7Request = $this->getResourceServer()->validateAuthenticatedRequest($psr7Request);
1402
1403 3
        $token = substr(Yii::$app->request->headers->get('Authorization'), self::BEARER_TOKEN_OFFSET);
0 ignored issues
show
It seems like Yii::app->request->headers->get('Authorization') can also be of type array and null; however, parameter $string of substr() 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

1403
        $token = substr(/** @scrutinizer ignore-type */ Yii::$app->request->headers->get('Authorization'), self::BEARER_TOKEN_OFFSET);
Loading history...
1404
1405 3
        if ($token) {
1406 3
            $claims = $this->getAccessToken($token)->claims();
1407
1408 3
            foreach ($claims->all() as $claimKey => $claimValue) {
1409 3
                if (!$this->isDefaultClaimKey($claimKey)) {
1410
                    $psr7Request = $psr7Request->withAttribute($claimKey, $claimValue);
1411
                }
1412
            }
1413
        }
1414
1415 3
        $this->_oauthClaims = $psr7Request->getAttributes();
1416 3
        $this->_oauthClaimsAuthorizationHeader = Yii::$app->request->getHeaders()->get('Authorization');
1417
    }
1418
1419
    /**
1420
     * Find a user identity bases on an access token.
1421
     * Note: validateAuthenticatedRequest() must be called before this method is called.
1422
     * @param string $token
1423
     * @param string $type
1424
     * @return Oauth2UserInterface|null
1425
     * @throws InvalidConfigException
1426
     * @throws Oauth2ServerException
1427
     * @see validateAuthenticatedRequest()
1428
     * @since 1.0.0
1429
     */
1430 4
    public function findIdentityByAccessToken($token, $type)
1431
    {
1432 4
        if (!is_a($type, Oauth2HttpBearerAuthInterface::class, true)) {
1433 1
            throw new InvalidCallException($type . ' must implement ' . Oauth2HttpBearerAuthInterface::class);
1434
        }
1435
1436
        if (
1437 3
            !preg_match('/^Bearer\s+(.*?)$/', $this->_oauthClaimsAuthorizationHeader, $matches)
1438 3
            || !Yii::$app->security->compareString($matches[1], $token)
1439
        ) {
1440 1
            throw new InvalidCallException(
1441 1
                'validateAuthenticatedRequest() must be called before findIdentityByAccessToken().'
1442 1
            );
1443
        }
1444
1445 2
        $userId = $this->getRequestOauthUserId();
1446 2
        if (empty($userId)) {
1447 1
            return null;
1448
        }
1449
1450 1
        return $this->identityClass::findIdentity($userId);
1451
    }
1452
1453
    /**
1454
     * Generate a "Personal Access Token" (PAT) which can be used as an alternative to using passwords
1455
     * for authentication (e.g. when using an API or command line).
1456
     *
1457
     * Note: Personal Access Tokens are intended to access resources on behalf users themselves.
1458
     *       To grant access to resources on behalf of an organization, or for long-lived integrations,
1459
     *       you most likely want to define an Oauth2 Client with the "Client Credentials" grant
1460
     *       (https://oauth.net/2/grant-types/client-credentials).
1461
     *
1462
     * @param string $clientIdentifier The Oauth2 client identifier for which the PAT should be generated.
1463
     * @param int|string $userIdentifier The identifier (primary key) of the user for which the PAT should be generated.
1464
     * @param Oauth2ScopeInterface[]|string[]|string|null $scope The Access Token scope.
1465
     * @param string|true|null $clientSecret If the client is a "confidential" client the secret is required.
1466
     *        If the boolean value `true` is passed, the client secret is automatically injected.
1467
     * @return Oauth2AccessTokenData
1468
     */
1469 3
    public function generatePersonalAccessToken($clientIdentifier, $userIdentifier, $scope = null, $clientSecret = null)
1470
    {
1471 3
        if (is_array($scope)) {
1472 2
            $scopeIdentifiers = [];
1473 2
            foreach ($scope as $scopeItem) {
1474 2
                if (is_string($scopeItem)) {
1475 1
                    $scopeIdentifiers[] = $scopeItem;
1476 1
                } elseif ($scopeItem instanceof Oauth2ScopeInterface) {
1477 1
                    $scopeIdentifiers[] = $scopeItem->getIdentifier();
1478
                } else {
1479
                    throw new InvalidArgumentException('If $scope is an array its elements must be either'
1480
                        . ' a string or an instance of ' . Oauth2ScopeInterface::class);
1481
                }
1482
            }
1483 2
            $scope = implode(' ', $scopeIdentifiers);
1484
        }
1485
1486 3
        if ($clientSecret === true) {
1487
            /** @var Oauth2ClientInterface $client */
1488 3
            $client = $this->getClientRepository()->findModelByIdentifier($clientIdentifier);
1489 3
            if ($client && $client->isConfidential()) {
1490 3
                $clientSecret = $client->getDecryptedSecret($this->getCryptographer());
1491
            } else {
1492
                $clientSecret = null;
1493
            }
1494
        }
1495
1496 3
        $request = (new Psr7ServerRequest('POST', ''))->withParsedBody([
1497 3
            'grant_type' => static::GRANT_TYPE_IDENTIFIER_PERSONAL_ACCESS_TOKEN,
1498 3
            'client_id' => $clientIdentifier,
1499 3
            'client_secret' => $clientSecret,
1500 3
            'user_id' => $userIdentifier,
1501 3
            'scope' => $scope,
1502 3
        ]);
1503
1504 3
        return new Oauth2AccessTokenData(Json::decode(
1505 3
            $this->getAuthorizationServer()
1506 3
                ->respondToAccessTokenRequest(
1507 3
                    $request,
1508 3
                    new Psr7Response()
1509 3
                )
1510 3
                ->getBody()
1511 3
                ->__toString()
1512 3
        ));
1513
    }
1514
1515
    /**
1516
     * @inheritDoc
1517
     */
1518 5
    public function getRequestOauthClaim($attribute, $default = null)
1519
    {
1520 5
        if (empty($this->_oauthClaimsAuthorizationHeader)) {
1521
            // User authorization was not processed by Oauth2Module.
1522 1
            return $default;
1523
        }
1524 4
        if (Yii::$app->request->getHeaders()->get('Authorization') !== $this->_oauthClaimsAuthorizationHeader) {
1525 1
            throw new InvalidCallException(
1526 1
                'App Request Authorization header does not match the processed Oauth header.'
1527 1
            );
1528
        }
1529 3
        return $this->_oauthClaims[$attribute] ?? $default;
1530
    }
1531
1532
    /**
1533
     * Helper function to ensure the required properties are configured for the module.
1534
     * @param string[] $properties
1535
     * @throws InvalidConfigException
1536
     * @since 1.0.0
1537
     */
1538 32
    protected function ensureProperties($properties)
1539
    {
1540 32
        foreach ($properties as $property) {
1541 32
            if (empty($this->$property)) {
1542 6
                throw new InvalidConfigException(__CLASS__ . '::$' . $property . ' must be set.');
1543
            }
1544
        }
1545
    }
1546
1547
    /**
1548
     * @throws InvalidConfigException
1549
     */
1550
    public function logoutUser($revokeTokens = true)
1551
    {
1552
        $identity = $this->getUserIdentity();
1553
1554
        if ($identity) {
1555
            if ($revokeTokens) {
1556
                $this->revokeTokensByUserId($identity->getId());
1557
            }
1558
1559
            Yii::$app->user->logout();
1560
        }
1561
    }
1562
1563
    public function revokeTokensByUserId($userId)
1564
    {
1565
        $accessTokens = $this->getAccessTokenRepository()->revokeAccessTokensByUserId($userId);
1566
        $accessTokenIds = array_map(fn($accessToken) => $accessToken->getPrimaryKey(), $accessTokens);
1567
        $this->getRefreshTokenRepository()->revokeRefreshTokensByAccessTokenIds($accessTokenIds);
1568
    }
1569
1570 4
    public function getSupportedPromptValues()
1571
    {
1572 4
        $supportedPromptValues = [
1573 4
            Oauth2OidcAuthenticationRequestInterface::REQUEST_PARAMETER_PROMPT_NONE,
1574 4
            Oauth2OidcAuthenticationRequestInterface::REQUEST_PARAMETER_PROMPT_LOGIN,
1575 4
            Oauth2OidcAuthenticationRequestInterface::REQUEST_PARAMETER_PROMPT_CONSENT,
1576 4
            Oauth2OidcAuthenticationRequestInterface::REQUEST_PARAMETER_PROMPT_SELECT_ACCOUNT,
1577 4
        ];
1578
1579 4
        if (!empty($this->userAccountCreationUrl)) {
1580 4
            $supportedPromptValues[] = Oauth2OidcAuthenticationRequestInterface::REQUEST_PARAMETER_PROMPT_CREATE;
1581
        }
1582
1583 4
        return $supportedPromptValues;
1584
    }
1585
1586
    /**
1587
     * @return int
1588
     */
1589 2
    public function getElaboratedHttpClientErrorsLogLevel()
1590
    {
1591 2
        if ($this->httpClientErrorsLogLevel === null) {
1592 1
            return YII_DEBUG ? Logger::LEVEL_ERROR : Logger::LEVEL_INFO;
1593
        }
1594
1595 1
        return $this->httpClientErrorsLogLevel;
1596
    }
1597
1598
1599 3
    public function getJwtConfiguration(): Configuration
1600
    {
1601
        // Based on \League\OAuth2\Server\AuthorizationValidators\BearerTokenValidator::initJwtConfiguration().
1602 3
        $jwtConfiguration = Configuration::forSymmetricSigner(
1603 3
            new Sha256(),
1604 3
            InMemory::plainText('empty', 'empty')
1605 3
        );
1606
1607 3
        $publicKey = $this->getPublicKey();
1608 3
        $jwtConfiguration->setValidationConstraints(
1609 3
            new SignedWith(
1610 3
                new Sha256(),
1611 3
                InMemory::plainText($publicKey->getKeyContents(), $publicKey->getPassPhrase() ?? '')
1612 3
            )
1613 3
        );
1614
1615 3
        return $jwtConfiguration;
1616
    }
1617
1618 3
    public function getAccessToken(string $token): Token
1619
    {
1620 3
        $jwtConfiguration = $this->getJwtConfiguration();
1621 3
        $accessToken = $jwtConfiguration->parser()->parse($token);
1622 3
        $jwtConfiguration->validator()->assert($accessToken, ...$jwtConfiguration->validationConstraints());
1623 3
        Yii::debug('Found access token: ' . $token, __METHOD__);
1624
1625 3
        return $accessToken;
1626
    }
1627
1628 3
    protected function isDefaultClaimKey(string $claimKey): bool
1629
    {
1630 3
        return in_array(
1631 3
            $claimKey,
1632 3
            [
1633 3
                'aud',
1634 3
                'jti',
1635 3
                'iat',
1636 3
                'nbf',
1637 3
                'exp',
1638 3
                'sub',
1639 3
                'scopes',
1640 3
                'client_id',
1641 3
            ]
1642 3
        );
1643
    }
1644
}
1645