Passed
Branch master (c0ae7d)
by Rutger
02:34
created

Oauth2Module::getEncryptor()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 6
c 1
b 0
f 0
dl 0
loc 11
ccs 6
cts 6
cp 1
rs 10
cc 2
nc 2
nop 0
crap 2
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 League\OAuth2\Server\CryptKey;
14
use League\OAuth2\Server\Exception\OAuthServerException;
15
use League\OAuth2\Server\Grant\GrantTypeInterface;
16
use rhertogh\Yii2Oauth2Server\base\Oauth2BaseModule;
17
use rhertogh\Yii2Oauth2Server\controllers\console\Oauth2ClientController;
18
use rhertogh\Yii2Oauth2Server\controllers\console\Oauth2DebugController;
19
use rhertogh\Yii2Oauth2Server\controllers\console\Oauth2MigrationsController;
20
use rhertogh\Yii2Oauth2Server\helpers\Psr7Helper;
21
use rhertogh\Yii2Oauth2Server\interfaces\components\authorization\Oauth2ClientAuthorizationRequestInterface;
22
use rhertogh\Yii2Oauth2Server\interfaces\components\encryption\Oauth2EncryptorInterface;
23
use rhertogh\Yii2Oauth2Server\interfaces\components\factories\encryption\Oauth2EncryptionKeyFactoryInterface;
24
use rhertogh\Yii2Oauth2Server\interfaces\components\factories\grants\base\Oauth2GrantTypeFactoryInterface;
25
use rhertogh\Yii2Oauth2Server\interfaces\components\openidconnect\scope\Oauth2OidcScopeCollectionInterface;
26
use rhertogh\Yii2Oauth2Server\interfaces\components\openidconnect\server\Oauth2OidcBearerTokenResponseInterface;
27
use rhertogh\Yii2Oauth2Server\interfaces\components\server\Oauth2AuthorizationServerInterface;
28
use rhertogh\Yii2Oauth2Server\interfaces\components\server\Oauth2ResourceServerInterface;
29
use rhertogh\Yii2Oauth2Server\interfaces\controllers\web\Oauth2CertificatesControllerInterface;
30
use rhertogh\Yii2Oauth2Server\interfaces\controllers\web\Oauth2ConsentControllerInterface;
31
use rhertogh\Yii2Oauth2Server\interfaces\controllers\web\Oauth2OidcControllerInterface;
32
use rhertogh\Yii2Oauth2Server\interfaces\controllers\web\Oauth2ServerControllerInterface;
33
use rhertogh\Yii2Oauth2Server\interfaces\controllers\web\Oauth2WellKnownControllerInterface;
34
use rhertogh\Yii2Oauth2Server\interfaces\filters\auth\Oauth2HttpBearerAuthInterface;
35
use rhertogh\Yii2Oauth2Server\interfaces\models\Oauth2OidcUserInterface;
36
use rhertogh\Yii2Oauth2Server\interfaces\models\Oauth2UserInterface;
37
use Yii;
38
use yii\base\BootstrapInterface;
39
use yii\base\InvalidArgumentException;
40
use yii\base\InvalidCallException;
41
use yii\base\InvalidConfigException;
42
use yii\console\Application as ConsoleApplication;
43
use yii\helpers\ArrayHelper;
44
use yii\helpers\StringHelper;
45
use yii\i18n\PhpMessageSource;
46
use yii\web\Application as WebApplication;
47
use yii\web\GroupUrlRule;
48
use yii\web\IdentityInterface;
49
use yii\web\Response;
50
use yii\web\UrlRule;
51
52
/**
53
 * This is the main module class for the Yii2 Oauth2 Server module.
54
 * To use it, include it as a module in the application configuration like the following:
55
 *
56
 * ~~~
57
 * return [
58
 *     'bootstrap' => ['oauth2'],
59
 *     'modules' => [
60
 *         'oauth2' => [
61
 *             'class' => 'rhertogh\Yii2Oauth2Server\Oauth2Module',
62
 *             // ... Please check docs/guide/start-installation.md further details
63
 *          ],
64
 *     ],
65
 * ]
66
 * ~~~
67
 *
68
 * @since 1.0.0
69
 */
70
class Oauth2Module extends Oauth2BaseModule implements BootstrapInterface
71
{
72
    /**
73
     * Application type "web": http response.
74
     * @since 1.0.0
75
     */
76
    public const APPLICATION_TYPE_WEB = 'web';
77
    /**
78
     * Application type "console": cli response.
79
     * @since 1.0.0
80
     */
81
    public const APPLICATION_TYPE_CONSOLE = 'console';
82
    /**
83
     * Supported Application types.
84
     * @since 1.0.0
85
     */
86
    public const APPLICATION_TYPES = [
87
        self::APPLICATION_TYPE_WEB,
88
        self::APPLICATION_TYPE_CONSOLE,
89
    ];
90
91
    /**
92
     * "Authorization Server" Role, please see guide for details.
93
     * @since 1.0.0
94
     */
95
    public const SERVER_ROLE_AUTHORIZATION_SERVER = 1;
96
    /**
97
     * "Resource Server" Role, please see guide for details.
98
     * @since 1.0.0
99
     */
100
    public const SERVER_ROLE_RESOURCE_SERVER = 2;
101
102
    /**
103
     * Required settings when the server role includes Authorization Server
104
     * @since 1.0.0
105
     */
106
    protected const REQUIRED_SETTINGS_AUTHORIZATION_SERVER = [
107
        'codesEncryptionKey',
108
        'storageEncryptionKeys',
109
        'defaultStorageEncryptionKey',
110
        'privateKey',
111
        'publicKey',
112
    ];
113
    /**
114
     * Required settings when the server role includes Resource Server
115
     * @since 1.0.0
116
     */
117
    protected const REQUIRED_SETTINGS_RESOURCE_SERVER = [
118
        'publicKey',
119
    ];
120
121
    /**
122
     * Prefix used in session storage of Client Authorization Requests
123
     * @since 1.0.0
124
     */
125
    protected const CLIENT_AUTHORIZATION_REQUEST_SESSION_PREFIX = 'OATH2_CLIENT_AUTHORIZATION_REQUEST_';
126
127
    /**
128
     * Controller mapping for the module. Will be parsed on `init()`.
129
     * @since 1.0.0
130
     */
131
    protected const CONTROLLER_MAP = [
132
        self::APPLICATION_TYPE_WEB => [
133
            Oauth2ServerControllerInterface::CONTROLLER_NAME => [
134
                'controller' => Oauth2ServerControllerInterface::class,
135
                'serverRole' => self::SERVER_ROLE_AUTHORIZATION_SERVER,
136
            ],
137
            Oauth2ConsentControllerInterface::CONTROLLER_NAME => [
138
                'controller' => Oauth2ConsentControllerInterface::class,
139
                'serverRole' => self::SERVER_ROLE_AUTHORIZATION_SERVER,
140
            ],
141
            Oauth2WellKnownControllerInterface::CONTROLLER_NAME => [
142
                'controller' => Oauth2WellKnownControllerInterface::class,
143
                'serverRole' => self::SERVER_ROLE_AUTHORIZATION_SERVER,
144
            ],
145
            Oauth2CertificatesControllerInterface::CONTROLLER_NAME => [
146
                'controller' => Oauth2CertificatesControllerInterface::class,
147
                'serverRole' => self::SERVER_ROLE_AUTHORIZATION_SERVER,
148
            ],
149
            Oauth2OidcControllerInterface::CONTROLLER_NAME => [
150
                'controller' => Oauth2OidcControllerInterface::class,
151
                'serverRole' => self::SERVER_ROLE_AUTHORIZATION_SERVER,
152
            ],
153
        ],
154
        self::APPLICATION_TYPE_CONSOLE => [
155
            'migrations' => [
156
                'controller' => Oauth2MigrationsController::class,
157
                'serverRole' => self::SERVER_ROLE_AUTHORIZATION_SERVER | self::SERVER_ROLE_RESOURCE_SERVER,
158
            ],
159
            'client' => [
160
                'controller' => Oauth2ClientController::class,
161
                'serverRole' => self::SERVER_ROLE_AUTHORIZATION_SERVER,
162
            ],
163
            'debug' => [
164
                'controller' => Oauth2DebugController::class,
165
                'serverRole' => self::SERVER_ROLE_AUTHORIZATION_SERVER | self::SERVER_ROLE_RESOURCE_SERVER,
166
            ],
167
        ]
168
    ];
169
170
    /**
171
     * @inheritdoc
172
     */
173
    public $controllerNamespace = __NAMESPACE__ . '\-'; // Set explicitly via $controllerMap in `init()`.
174
175
    /**
176
     * @var string|null The application type. If `null` the type will be automatically detected.
177
     * @see APPLICATION_TYPES
178
     */
179
    public $appType = null;
180
181
    /**
182
     * @var int The Oauth 2.0 Server Roles the module will perform.
183
     * @since 1.0.0
184
     */
185
    public $serverRole = self::SERVER_ROLE_AUTHORIZATION_SERVER | self::SERVER_ROLE_RESOURCE_SERVER;
186
187
    /**
188
     * @var string|null The private key for the server. Can be a string containing the key itself or point to a file.
189
     * When pointing to a file it's recommended to use an absolute path prefixed with 'file://' or start with
190
     * '@' to use a Yii path alias.
191
     * @see $privateKeyPassphrase For setting a passphrase for the private key.
192
     * @since 1.0.0
193
     */
194
    public $privateKey = null;
195
196
    /**
197
     * @var string|null The passphrase for the private key.
198
     * @since 1.0.0
199
     */
200
    public $privateKeyPassphrase = null;
201
    /**
202
     * @var string|null The public key for the server. Can be a string containing the key itself or point to a file.
203
     * When pointing to a file it's recommended to use an absolute path prefixed with 'file://' or start with
204
     * '@' to use a Yii path alias.
205
     * @since 1.0.0
206
     */
207
    public $publicKey = null;
208
209
    /**
210
     * @var string|null The encryption key for authorization and refresh codes.
211
     * @since 1.0.0
212
     */
213
    public $codesEncryptionKey = null;
214
215
    /**
216
     * @var string[]|null The encryption keys for storage like client secrets.
217
     * Where the array key is the name of the key, and the value the key itself. E.g.
218
     * `['myKey' => 'def00000cb36fd6ed6641e0ad70805b28d....']`
219
     * @since 1.0.0
220
     */
221
    public $storageEncryptionKeys = null;
222
223
    /**
224
     * @var string|null The index of the default key in storageEncryptionKeys. E.g. 'myKey'.
225
     * @since 1.0.0
226
     */
227
    public $defaultStorageEncryptionKey = null;
228
229
    /**
230
     * @var Oauth2UserInterface|string|null The Identity Class of your application,
231
     * most likely the same as the 'identityClass' of your application's User Component.
232
     * @since 1.0.0
233
     */
234
    public $identityClass = null;
235
236
    /**
237
     * @var null|string Prefix used for url rules. When `null` the module's uniqueId will be used.
238
     * @since 1.0.0
239
     */
240
    public $urlRulesPrefix = null;
241
242
    /**
243
     * @var string URL path for the access token endpoint (will be prefixed with $urlRulesPrefix).
244
     * @since 1.0.0
245
     */
246
    public $authorizePath = 'authorize';
247
248
    /**
249
     * @var string URL path for the access token endpoint (will be prefixed with $urlRulesPrefix).
250
     * @since 1.0.0
251
     */
252
    public $accessTokenPath = 'access-token';
253
254
    /**
255
     * @var string URL path for the certificates jwks endpoint (will be prefixed with $urlRulesPrefix).
256
     * @since 1.0.0
257
     */
258
    public $jwksPath = 'certs';
259
260
    /**
261
     * The URL to the page where the user can perform the client/scope authorization
262
     * (if `null` the build in page will be used).
263
     * @return string
264
     * @since 1.0.0
265
     */
266
    public $clientAuthorizationUrl = null;
267
268
    /**
269
     * @var string The URL path to the build in page where the user can authorize the client for the requested scopes
270
     * (will be prefixed with $urlRulesPrefix).
271
     * Note: This setting will only be used if $clientAuthorizationUrl is `null`.
272
     * @since 1.0.0
273
     */
274
    public $clientAuthorizationPath = 'authorize-client';
275
276
    /**
277
     * @var string The view to use in the "client authorization action" for the page where the user can
278
     * authorize the client for the requested scopes.
279
     * Note: This setting will only be used if $clientAuthorizationUrl is `null`.
280
     * @since 1.0.0
281
     */
282
    public $clientAuthorizationView = 'authorize-client';
283
284
    /**
285
     * @var string The URL path to the OpenID Connect Userinfo Action (will be prefixed with $urlRulesPrefix).
286
     * Note: This setting will only be used if $enableOpenIdConnect and $openIdConnectUserinfoEndpoint are `true`.
287
     * @since 1.0.0
288
     */
289
    public $openIdConnectUserinfoPath = 'oidc/userinfo';
290
291
    /**
292
     * @var Oauth2GrantTypeFactoryInterface[]|GrantTypeInterface[]|string[]|Oauth2GrantTypeFactoryInterface|GrantTypeInterface|string|callable
293
     * The Oauth 2.0 Grant Types that the module will serve.
294
     * @since 1.0.0
295
     */
296
    public $grantTypes = [];
297
298
    /**
299
     * @var string|null Default Time To Live for the access token, used when the Grant Type does not specify it.
300
     * When `null` default value of 1 hour is used.
301
     * The format should be a DateInterval duration (https://www.php.net/manual/en/dateinterval.construct.php).
302
     * @since 1.0.0
303
     */
304
    public $defaultAccessTokenTTL = null;
305
306
    /**
307
     * @var bool Should the resource server check for revocation of the access token.
308
     * @since 1.0.0
309
     */
310
    public $resourceServerAccessTokenRevocationValidation = true;
311
312
    /**
313
     * @var bool Enable support for OpenIdvConnect.
314
     * @since 1.0.0
315
     */
316
    public $enableOpenIdConnect = false;
317
318
    /**
319
     * @var bool Enable the .well-known/openid-configuration discovery endpoint.
320
     * @since 1.0.0
321
     */
322
    public $enableOpenIdConnectDiscovery = true;
323
324
    /**
325
     * @var bool include `grant_types_supported` in the OpenIdConnect Discovery.
326
     * Note: Since grant types can be specified per client not all clients might support all enabled grant types.
327
     * @since 1.0.0
328
     */
329
    public $openIdConnectDiscoveryIncludeSupportedGrantTypes = true;
330
331
    /**
332
     * @var string URL to include in the OpenID Connect Discovery Service of a page containing
333
     * human-readable information that developers might want or need to know when using the OpenID Provider.
334
     * @see 'service_documentation' in https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3
335
     * @since 1.0.0
336
     */
337
    public $openIdConnectDiscoveryServiceDocumentationUrl = null;
338
339
    /**
340
     * @var string|bool A string to a custom userinfo endpoint or `true` to enable the build in endpoint.
341
     * @since 1.0.0
342
     */
343
    public $openIdConnectUserinfoEndpoint = true;
344
345
    /**
346
     * Warning! Enabling this setting might introduce privacy concerns since the client could poll for the
347
     * online status of a user.
348
     *
349
     * @var bool If this setting is disabled in case of OpenID Connect Context the Access Token won't include a
350
     * Refresh Token when the 'offline_access' scope is not included in the authorization request.
351
     * In some cases it might be needed to always include a Refresh Token, in that case enable this setting and
352
     * implement the `Oauth2OidcUserSessionStatusInterface` on the User Identity model.
353
     * @since 1.0.0
354
     */
355
    public $openIdConnectIssueRefreshTokenWithoutOfflineAccessScope = false;
356
357
    /**
358
     * @var bool The default option for "User Account Selection' when not specified for a client.
359
     * @since 1.0.0
360
     */
361
    public $defaultUserAccountSelection = self::USER_ACCOUNT_SELECTION_DISABLED;
362
363
    /**
364
     * @var bool|null Display exception messages that might leak server details. This could be useful for debugging.
365
     * In case of `null` (default) the YII_DEBUG constant will be used.
366
     * Warning: Should NOT be enabled in production!
367
     * @since 1.0.0
368
     */
369
    public $displayConfidentialExceptionMessages = null;
370
371
    /**
372
     * @var string|null The namespace with which migrations will be created (and by which they will be located).
373
     * Note: The specified namespace must be defined as a Yii alias (e.g. '@app').
374
     * @since 1.0.0
375
     */
376
    public $migrationsNamespace = null;
377
    /**
378
     * @var string|null Optional prefix used in the name of generated migrations
379
     * @since 1.0.0
380
     */
381
    public $migrationsPrefix = null;
382
    /**
383
     * @var string|array|int|null Sets the file ownership of generated migrations
384
     * @see \yii\helpers\BaseFileHelper::changeOwnership()
385
     * @since 1.0.0
386
     */
387
    public $migrationsFileOwnership = null;
388
    /**
389
     * @var int|null Sets the file mode of generated migrations
390
     * @see \yii\helpers\BaseFileHelper::changeOwnership()
391
     * @since 1.0.0
392
     */
393
    public $migrationsFileMode = null;
394
395
    /**
396
     * @var Oauth2AuthorizationServerInterface|null Cache for the authorization server
397
     * @since 1.0.0
398
     */
399
    protected $_authorizationServer = null;
400
401
    /**
402
     * @var Oauth2ResourceServerInterface|null Cache for the resource server
403
     * @since 1.0.0
404
     */
405
    protected $_resourceServer = null;
406
407
    /**
408
     * @var Oauth2EncryptorInterface|null Cache for the Oauth2Encryptor
409
     * @since 1.0.0
410
     */
411
    protected $_encryptor = null;
412
413
    /**
414
     * @var string|null The authorization header used when the authorization request was validated.
415
     * @since 1.0.0
416
     */
417
    protected $_oauthClaimsAuthorizationHeader = null;
418
419
    /**
420
     * @inheritDoc
421
     * @throws InvalidConfigException
422
     */
423 109
    public function init()
424
    {
425 109
        parent::init();
426
427 109
        $app = Yii::$app;
428
429 109
        if ($app instanceof WebApplication || $this->appType == static::APPLICATION_TYPE_WEB) {
430 18
            $controllerMap = static::CONTROLLER_MAP[static::APPLICATION_TYPE_WEB];
431 109
        } elseif ($app instanceof ConsoleApplication || $this->appType == static::APPLICATION_TYPE_CONSOLE) {
432 109
            $controllerMap = static::CONTROLLER_MAP[static::APPLICATION_TYPE_CONSOLE];
433
        } else {
434
            throw new InvalidConfigException(
435
                'Unable to detect application type, configure it manually by setting `$appType`.'
436
            );
437
        }
438 109
        $controllerMap = array_filter(
439
            $controllerMap,
440 109
            fn($controllerSettings) => $controllerSettings['serverRole'] & $this->serverRole
441
        );
442 109
        $this->controllerMap = ArrayHelper::getColumn($controllerMap, 'controller');
443
444 109
        if (empty($this->identityClass)) {
445 1
            throw new InvalidConfigException('$identityClass must be set.');
446 109
        } elseif (!is_a($this->identityClass, Oauth2UserInterface::class, true)) {
447 1
            throw new InvalidConfigException(
448 1
                $this->identityClass . ' must implement ' . Oauth2UserInterface::class
0 ignored issues
show
Bug introduced by
Are you sure $this->identityClass of type rhertogh\Yii2Oauth2Serve...th2UserInterface|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

448
                /** @scrutinizer ignore-type */ $this->identityClass . ' must implement ' . Oauth2UserInterface::class
Loading history...
449
            );
450
        }
451
452 109
        foreach (static::DEFAULT_INTERFACE_IMPLEMENTATIONS as $interface => $implementation) {
453 109
            if (!Yii::$container->has($interface)) {
454 109
                Yii::$container->set($interface, $implementation);
455
            }
456
        }
457
458 109
        if (empty($this->urlRulesPrefix)) {
459 109
            $this->urlRulesPrefix = $this->uniqueId;
460
        }
461
462 109
        $this->registerTranslations();
463
    }
464
465
    /**
466
     * @inheritdoc
467
     * @throws InvalidConfigException
468
     */
469 109
    public function bootstrap($app)
470
    {
471
        if (
472 109
            $app instanceof WebApplication
473 109
            && $this->serverRole & static::SERVER_ROLE_AUTHORIZATION_SERVER
474
        ) {
475
            $rules = [
476 18
                $this->accessTokenPath => Oauth2ServerControllerInterface::CONTROLLER_NAME
477
                    . '/' . Oauth2ServerControllerInterface::ACTION_NAME_ACCESS_TOKEN,
478 18
                $this->authorizePath => Oauth2ServerControllerInterface::CONTROLLER_NAME
479
                    . '/' . Oauth2ServerControllerInterface::ACTION_NAME_AUTHORIZE,
480 18
                $this->jwksPath => Oauth2CertificatesControllerInterface::CONTROLLER_NAME
481
                    . '/' . Oauth2CertificatesControllerInterface::ACTION_NAME_JWKS,
482
            ];
483
484 18
            if (empty($this->clientAuthorizationUrl)) {
485 17
                $rules[$this->clientAuthorizationPath] = Oauth2ConsentControllerInterface::CONTROLLER_NAME
486
                    . '/' . Oauth2ConsentControllerInterface::ACTION_NAME_AUTHORIZE_CLIENT;
487
            }
488
489 18
            if ($this->enableOpenIdConnect && $this->openIdConnectUserinfoEndpoint === true) {
490 18
                $rules[$this->openIdConnectUserinfoPath] =
491 18
                    Oauth2OidcControllerInterface::CONTROLLER_NAME
492
                    . '/' . Oauth2OidcControllerInterface::ACTION_NAME_USERINFO;
493
            }
494
495 18
            $urlManager = $app->getUrlManager();
496 18
            $urlManager->addRules([
497 18
                Yii::createObject([
498
                    'class' => GroupUrlRule::class,
499 18
                    'prefix' => $this->urlRulesPrefix,
500 18
                    'routePrefix' => $this->id,
501
                    'rules' => $rules,
502
                ]),
503
            ]);
504
505 18
            if ($this->enableOpenIdConnect && $this->enableOpenIdConnectDiscovery) {
506 18
                $urlManager->addRules([
507 18
                    Yii::createObject([
508
                        'class' => UrlRule::class,
509
                        'pattern' => '.well-known/openid-configuration',
510 18
                        'route' => $this->id
511
                            . '/' . Oauth2WellKnownControllerInterface::CONTROLLER_NAME
512
                            . '/' . Oauth2WellKnownControllerInterface::ACTION_NAME_OPENID_CONFIGURATION,
513
                    ]),
514
                ]);
515
            }
516
        }
517
    }
518
519
    /**
520
     * Registers the translations for the module
521
     * @param bool $force Force the setting of the translations (even if they are already defined).
522
     * @since 1.0.0
523
     */
524 109
    public function registerTranslations($force = false)
525
    {
526 109
        if ($force || !array_key_exists('oauth2', Yii::$app->i18n->translations)) {
527 109
            Yii::$app->i18n->translations['oauth2'] = [
528
                'class' => PhpMessageSource::class,
529
                'sourceLanguage' => 'en-US',
530 109
                'basePath' => __DIR__ . DIRECTORY_SEPARATOR . 'messages',
531
                'fileMap' => [
532
                    'oauth2' => 'oauth2.php',
533
                ],
534
            ];
535
        }
536
    }
537
538
    /**
539
     * @return CryptKey The private key of the server.
540
     * @throws InvalidConfigException
541
     * @since 1.0.0
542
     */
543 17
    public function getPrivateKey()
544
    {
545 17
        $privateKey = $this->privateKey;
546 17
        if (StringHelper::startsWith($privateKey, '@')) {
547 14
            $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

547
            $privateKey = 'file://' . /** @scrutinizer ignore-type */ Yii::getAlias($privateKey);
Loading history...
548
        }
549 17
        return Yii::createObject(CryptKey::class, [$privateKey, $this->privateKeyPassphrase]);
550
    }
551
552
    /**
553
     * @return CryptKey The public key of the server.
554
     * @throws InvalidConfigException
555
     * @since 1.0.0
556
     */
557 9
    public function getPublicKey()
558
    {
559 9
        $publicKey = $this->publicKey;
560 9
        if (StringHelper::startsWith($publicKey, '@')) {
561 6
            $publicKey = 'file://' . Yii::getAlias($publicKey);
0 ignored issues
show
Bug introduced by
Are you sure Yii::getAlias($publicKey) of type false|string can be used in concatenation? ( Ignorable by Annotation )

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

561
            $publicKey = 'file://' . /** @scrutinizer ignore-type */ Yii::getAlias($publicKey);
Loading history...
562
        }
563 9
        return Yii::createObject(CryptKey::class, [$publicKey]);
564
    }
565
566
    /**
567
     * @return Oauth2AuthorizationServerInterface The authorization server.
568
     * @throws InvalidConfigException
569
     * @since 1.0.0
570
     */
571 22
    public function getAuthorizationServer()
572
    {
573 22
        if (!($this->serverRole & static::SERVER_ROLE_AUTHORIZATION_SERVER)) {
574 1
            throw new InvalidCallException('Oauth2 server role does not include authorization server.');
575
        }
576
577 21
        if (!$this->_authorizationServer) {
578 21
            $this->ensureProperties(static::REQUIRED_SETTINGS_AUTHORIZATION_SERVER);
579
580 16
            if (empty($this->storageEncryptionKeys[$this->defaultStorageEncryptionKey])) {
581 1
                throw new InvalidConfigException(
582 1
                    'Key "' . $this->defaultStorageEncryptionKey . '" is not set in $storageEncryptionKeys'
583
                );
584
            }
585
586
            /** @var Oauth2EncryptionKeyFactoryInterface $keyFactory */
587 15
            $keyFactory = Yii::createObject(Oauth2EncryptionKeyFactoryInterface::class);
588
            try {
589 15
                $codesEncryptionKey = $keyFactory->createFromAsciiSafeString($this->codesEncryptionKey);
590 2
            } catch (BadFormatException $e) {
591 1
                throw new InvalidConfigException(
592 1
                    '$codesEncryptionKey is malformed: ' . $e->getMessage(),
593
                    0,
594
                    $e
595
                );
596 1
            } catch (EnvironmentIsBrokenException $e) {
597 1
                throw new InvalidConfigException(
598 1
                    'Could not instantiate $codesEncryptionKey: ' . $e->getMessage(),
599
                    0,
600
                    $e
601
                );
602
            }
603
604 13
            $responseType = null;
605 13
            if ($this->enableOpenIdConnect) {
606 13
                $responseType = Yii::createObject(Oauth2OidcBearerTokenResponseInterface::class, [
607
                    $this,
608
                ]);
609
            }
610
611 13
            $this->_authorizationServer = Yii::createObject(Oauth2AuthorizationServerInterface::class, [
612 13
                $this->getClientRepository(),
613 13
                $this->getAccessTokenRepository(),
614 13
                $this->getScopeRepository(),
615 13
                $this->getPrivateKey(),
616
                $codesEncryptionKey,
617
                $responseType
618
            ]);
619
620 13
            if (!empty($this->grantTypes)) {
621 13
                $grantTypes = $this->grantTypes;
622
623 13
                if (is_callable($grantTypes)) {
624 1
                    call_user_func($grantTypes, $this->_authorizationServer, $this);
0 ignored issues
show
Bug introduced by
It seems like $grantTypes can also be of type League\OAuth2\Server\Grant\GrantTypeInterface and rhertogh\Yii2Oauth2Serve...antTypeFactoryInterface; however, parameter $callback of call_user_func() does only seem to accept callable, maybe add an additional type check? ( Ignorable by Annotation )

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

624
                    call_user_func(/** @scrutinizer ignore-type */ $grantTypes, $this->_authorizationServer, $this);
Loading history...
625
                } else {
626 12
                    if (!is_array($grantTypes)) {
627 2
                        $grantTypes = [$grantTypes];
628
                    }
629
630 12
                    foreach ($grantTypes as $grantTypeDefinition) {
631 12
                        if ($grantTypeDefinition instanceof GrantTypeInterface) {
632 1
                            $accessTokenTTL = $this->defaultAccessTokenTTL
633
                                ? new \DateInterval($this->defaultAccessTokenTTL)
634 1
                                : null;
635 1
                            $this->_authorizationServer->enableGrantType($grantTypeDefinition, $accessTokenTTL);
636
                        } elseif (
637
                            (
638 11
                                is_numeric($grantTypeDefinition)
639 11
                                && array_key_exists($grantTypeDefinition, static::DEFAULT_GRANT_TYPE_FACTORIES)
640
                            )
641 11
                            || is_a($grantTypeDefinition, Oauth2GrantTypeFactoryInterface::class, true)
642
                        ) {
643
                            if (
644 10
                                is_numeric($grantTypeDefinition)
645 10
                                && array_key_exists($grantTypeDefinition, static::DEFAULT_GRANT_TYPE_FACTORIES)
646
                            ) {
647 10
                                $grantTypeDefinition = static::DEFAULT_GRANT_TYPE_FACTORIES[$grantTypeDefinition];
648
                            }
649
650
                            /** @var Oauth2GrantTypeFactoryInterface $factory */
651 10
                            $factory = Yii::createObject([
652
                                'class' => $grantTypeDefinition,
653
                                'module' => $this,
654
                            ]);
655 10
                            $accessTokenTTL = $factory->accessTokenTTL ?? $this->defaultAccessTokenTTL ?? null;
0 ignored issues
show
Bug introduced by
Accessing accessTokenTTL on the interface rhertogh\Yii2Oauth2Serve...antTypeFactoryInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
656 10
                            $this->_authorizationServer->enableGrantType(
657 10
                                $factory->getGrantType(),
658 10
                                $accessTokenTTL ? new \DateInterval($accessTokenTTL) : null
659
                            );
660
                        } else {
661 1
                            throw new InvalidConfigException(
662
                                'Unknown grantType '
663 1
                                . (is_scalar($grantTypeDefinition)
664 1
                                    ? '"' . $grantTypeDefinition . '".'
665 1
                                    : 'with data type ' . gettype($grantTypeDefinition)
666
                                )
667
                            );
668
                        }
669
                    }
670
                }
671
            }
672
        }
673
674 12
        return $this->_authorizationServer;
675
    }
676
677
    /**
678
     * @inheritDoc
679
     * @throws InvalidConfigException
680
     */
681 5
    public function getOidcScopeCollection()
682
    {
683 5
        if ($this->_oidcScopeCollection === null) {
684 5
            $openIdConnectScopes = $this->getOpenIdConnectScopes();
685 5
            if ($openIdConnectScopes instanceof Oauth2OidcScopeCollectionInterface) {
686 1
                $this->_oidcScopeCollection = $openIdConnectScopes;
687 4
            } elseif (is_callable($openIdConnectScopes)) {
688 1
                $this->_oidcScopeCollection = call_user_func($openIdConnectScopes, $this);
689 1
                if (!($this->_oidcScopeCollection instanceof Oauth2OidcScopeCollectionInterface)) {
690
                    throw new InvalidConfigException(
691
                        '$openIdConnectScopes must return an instance of '
692
                            . Oauth2OidcScopeCollectionInterface::class
693
                    );
694
                }
695 3
            } elseif (is_array($openIdConnectScopes) || is_string($openIdConnectScopes)) {
696 2
                $this->_oidcScopeCollection = Yii::createObject([
697
                    'class' => Oauth2OidcScopeCollectionInterface::class,
698 2
                    'oidcScopes' => (array)$openIdConnectScopes,
699
                ]);
700
            } else {
701 1
                throw new InvalidConfigException(
702
                    '$openIdConnectScopes must be a callable, array, string or '
703
                        . Oauth2OidcScopeCollectionInterface::class
704
                );
705
            }
706
        }
707
708 4
        return $this->_oidcScopeCollection;
709
    }
710
711
    /**
712
     * @return Oauth2ResourceServerInterface The resource server.
713
     * @throws InvalidConfigException
714
     * @since 1.0.0
715
     */
716 7
    public function getResourceServer()
717
    {
718 7
        if (!($this->serverRole & static::SERVER_ROLE_RESOURCE_SERVER)) {
719 1
            throw new InvalidCallException('Oauth2 server role does not include resource server.');
720
        }
721
722 6
        if (!$this->_resourceServer) {
723 6
            $this->ensureProperties(static::REQUIRED_SETTINGS_RESOURCE_SERVER);
724
725 5
            $accessTokenRepository = $this->getAccessTokenRepository()
726 5
                ->setRevocationValidation($this->resourceServerAccessTokenRevocationValidation);
727
728 5
            $this->_resourceServer = Yii::createObject(Oauth2ResourceServerInterface::class, [
729
                $accessTokenRepository,
730 5
                $this->getPublicKey(),
731
            ]);
732
        }
733
734 5
        return $this->_resourceServer;
735
    }
736
737
    /**
738
     * @return Oauth2EncryptorInterface The data encryptor for the module.
739
     * @throws InvalidConfigException
740
     * @since 1.0.0
741
     */
742 1
    public function getEncryptor()
743
    {
744 1
        if (!$this->_encryptor) {
745 1
            $this->_encryptor = Yii::createObject([
746
                'class' => Oauth2EncryptorInterface::class,
747 1
                'keys' => $this->storageEncryptionKeys,
748 1
                'defaultKeyName' => $this->defaultStorageEncryptionKey,
749
            ]);
750
        }
751
752 1
        return $this->_encryptor;
753
    }
754
755
    /**
756
     * Generates a redirect Response to the client authorization page where the user is prompted to authorize the
757
     * client and requested scope.
758
     * @param Oauth2ClientAuthorizationRequestInterface $clientAuthorizationRequest
759
     * @return Response
760
     * @since 1.0.0
761
     */
762 5
    public function generateClientAuthReqRedirectResponse($clientAuthorizationRequest)
763
    {
764 5
        $this->setClientAuthReqSession($clientAuthorizationRequest);
765 5
        if (!empty($this->clientAuthorizationUrl)) {
766 1
            $url = $this->clientAuthorizationUrl;
767
        } else {
768 4
            $url = $this->uniqueId
769
                . '/' . Oauth2ConsentControllerInterface::CONTROLLER_NAME
770
                . '/' . Oauth2ConsentControllerInterface::ACTION_NAME_AUTHORIZE_CLIENT;
771
        }
772 5
        return Yii::$app->response->redirect([
773
            $url,
774 5
            'clientAuthorizationRequestId' => $clientAuthorizationRequest->getRequestId(),
775
        ]);
776
    }
777
778
    /**
779
     * Get a previously stored Client Authorization Request from the session.
780
     * @param $requestId
781
     * @return Oauth2ClientAuthorizationRequestInterface|null
782
     * @since 1.0.0
783
     */
784 5
    public function getClientAuthReqSession($requestId)
785
    {
786 5
        if (empty($requestId)) {
787
            return null;
788
        }
789 5
        $key = static::CLIENT_AUTHORIZATION_REQUEST_SESSION_PREFIX . $requestId;
790 5
        $clientAuthorizationRequest = Yii::$app->session->get($key);
0 ignored issues
show
Bug introduced by
The method get() does not exist on null. ( Ignorable by Annotation )

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

790
        /** @scrutinizer ignore-call */ 
791
        $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...
791 5
        if (!($clientAuthorizationRequest instanceof Oauth2ClientAuthorizationRequestInterface)) {
792 2
            if (!empty($clientAuthorizationRequest)) {
793 1
                Yii::warning(
794 1
                    'Found a ClientAuthorizationRequestSession with key "' . $key
795
                        . '", but it\'s not a ' . Oauth2ClientAuthorizationRequestInterface::class
796
                );
797
            }
798 2
            return null;
799
        }
800 5
        if ($clientAuthorizationRequest->getRequestId() !== $requestId) {
801 1
            Yii::warning(
802 1
                'Found a ClientAuthorizationRequestSession with key "' . $key
803
                    . '", but it\'s request id does not match "' . $requestId . '".'
804
            );
805 1
            return null;
806
        }
807 5
        $clientAuthorizationRequest->setModule($this);
808
809 5
        return $clientAuthorizationRequest;
810
    }
811
812
    /**
813
     * Stores the Client Authorization Request in the session.
814
     * @param Oauth2ClientAuthorizationRequestInterface $clientAuthorizationRequest
815
     * @since 1.0.0
816
     */
817 8
    public function setClientAuthReqSession($clientAuthorizationRequest)
818
    {
819 8
        $requestId = $clientAuthorizationRequest->getRequestId();
820 8
        if (empty($requestId)) {
821 1
            throw new InvalidArgumentException('$scopeAuthorization must return a request id.');
822
        }
823 7
        $key = static::CLIENT_AUTHORIZATION_REQUEST_SESSION_PREFIX . $requestId;
824 7
        Yii::$app->session->set($key, $clientAuthorizationRequest);
825
    }
826
827
    /**
828
     * Stores whether the user was authenticated during the completion of the Client Authorization Request.
829
     * @param Oauth2ClientAuthorizationRequestInterface $clientAuthorizationRequest
830
     * @since 1.0.0
831
     */
832
    public function setUserAuthenticatedDuringClientAuthRequest(
833
        $clientAuthorizationRequestId,
834
        $authenticatedDuringRequest
835
    ) {
836
        $clientAuthorizationRequest = $this->getClientAuthReqSession($clientAuthorizationRequestId);
837
        if ($clientAuthorizationRequest) {
838
            $clientAuthorizationRequest->setUserAuthenticatedDuringRequest($authenticatedDuringRequest);
839
            $this->setClientAuthReqSession($clientAuthorizationRequest);
840
        }
841
    }
842
843
    /**
844
     * Stores the user identity selected during the completion of the Client Authorization Request.
845
     * @param string $clientAuthorizationRequestId
846
     * @param Oauth2UserInterface $userIdentity
847
     * @since 1.0.0
848
     */
849
    public function setClientAuthRequestUserIdentity($clientAuthorizationRequestId, $userIdentity)
850
    {
851
        $clientAuthorizationRequest = $this->getClientAuthReqSession($clientAuthorizationRequestId);
852
        if ($clientAuthorizationRequest) {
853
            $clientAuthorizationRequest->setUserIdentity($userIdentity);
854
            $this->setClientAuthReqSession($clientAuthorizationRequest);
855
        }
856
    }
857
858
    /**
859
     * Clears a Client Authorization Request from the session storage.
860
     * @param string $requestId
861
     * @since 1.0.0
862
     */
863 2
    public function removeClientAuthReqSession($requestId)
864
    {
865 2
        if (empty($requestId)) {
866 1
            throw new InvalidArgumentException('$requestId can not be empty.');
867
        }
868 1
        $key = static::CLIENT_AUTHORIZATION_REQUEST_SESSION_PREFIX . $requestId;
869 1
        Yii::$app->session->remove($key);
870
    }
871
872
    /**
873
     * Generates a redirect Response when the Client Authorization Request is completed.
874
     * @param Oauth2ClientAuthorizationRequestInterface $clientAuthorizationRequest
875
     * @return Response
876
     * @throws InvalidConfigException
877
     * @since 1.0.0
878
     */
879 1
    public function generateClientAuthReqCompledRedirectResponse($clientAuthorizationRequest)
880
    {
881 1
        $clientAuthorizationRequest->processAuthorization();
882 1
        $this->setClientAuthReqSession($clientAuthorizationRequest);
883 1
        return Yii::$app->response->redirect($clientAuthorizationRequest->getAuthorizationRequestUrl());
884
    }
885
886
    /**
887
     * @return IdentityInterface|Oauth2UserInterface|Oauth2OidcUserInterface|null
888
     * @throws InvalidConfigException
889
     * @since 1.0.0
890
     */
891 5
    public function getUserIdentity()
892
    {
893 5
        $user = Yii::$app->user->identity;
894 5
        if (!empty($user) && !($user instanceof Oauth2UserInterface)) {
895 1
            throw new InvalidConfigException(
896 1
                'Yii::$app->user->identity (currently ' . get_class($user)
897
                    . ') must implement ' . Oauth2UserInterface::class
898
            );
899
        }
900 4
        return $user;
901
    }
902
903
    /**
904
     * Validates a bearer token authenticated request. Note: this method does not return a result but will throw
905
     * an exception when the authentication fails.
906
     * @throws InvalidConfigException
907
     * @throws OAuthServerException
908
     * @since 1.0.0
909
     */
910 3
    public function validateAuthenticatedRequest()
911
    {
912 3
        $psr7Request = Psr7Helper::yiiToPsr7Request(Yii::$app->request);
0 ignored issues
show
Bug introduced by
It seems like Yii::app->request can also be of type yii\console\Request; however, parameter $request of rhertogh\Yii2Oauth2Serve...per::yiiToPsr7Request() does only seem to accept yii\web\Request, maybe add an additional type check? ( Ignorable by Annotation )

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

912
        $psr7Request = Psr7Helper::yiiToPsr7Request(/** @scrutinizer ignore-type */ Yii::$app->request);
Loading history...
913
914 3
        $psr7Request = $this->getResourceServer()->validateAuthenticatedRequest($psr7Request);
915
916 3
        $this->_oauthClaims = $psr7Request->getAttributes();
917 3
        $this->_oauthClaimsAuthorizationHeader = Yii::$app->request->getHeaders()->get('Authorization');
0 ignored issues
show
Documentation Bug introduced by
It seems like Yii::app->request->getHe...)->get('Authorization') can also be of type array. However, the property $_oauthClaimsAuthorizationHeader is declared as type null|string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
918
    }
919
920
    /**
921
     * Find a user identity bases on an access token.
922
     * Note: validateAuthenticatedRequest() must be called before this method is called.
923
     * @param string $token
924
     * @param string $type
925
     * @return Oauth2UserInterface|null
926
     * @throws InvalidConfigException
927
     * @throws OAuthServerException
928
     * @see validateAuthenticatedRequest()
929
     * @since 1.0.0
930
     */
931 4
    public function findIdentityByAccessToken($token, $type)
932
    {
933 4
        if (!is_a($type, Oauth2HttpBearerAuthInterface::class, true)) {
934 1
            throw new InvalidCallException($type . ' must implement ' . Oauth2HttpBearerAuthInterface::class);
935
        }
936
937
        if (
938 3
            !preg_match('/^Bearer\s+(.*?)$/', $this->_oauthClaimsAuthorizationHeader, $matches)
0 ignored issues
show
Bug introduced by
It seems like $this->_oauthClaimsAuthorizationHeader can also be of type null; however, parameter $subject of preg_match() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

938
            !preg_match('/^Bearer\s+(.*?)$/', /** @scrutinizer ignore-type */ $this->_oauthClaimsAuthorizationHeader, $matches)
Loading history...
939 3
            || !Yii::$app->security->compareString($matches[1], $token)
940
        ) {
941 1
            throw new InvalidCallException(
942
                'validateAuthenticatedRequest() must be called before findIdentityByAccessToken().'
943
            );
944
        }
945
946 2
        $userId = $this->getRequestOauthUserId();
947 2
        if (empty($userId)) {
948 1
            return null;
949
        }
950
951 1
        return $this->identityClass::findIdentity($userId);
0 ignored issues
show
Bug introduced by
The method findIdentity() 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

951
        return $this->identityClass::/** @scrutinizer ignore-call */ findIdentity($userId);

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...
952
    }
953
954
    /**
955
     * @inheritDoc
956
     */
957 5
    protected function getRequestOauthClaim($attribute, $default = null)
958
    {
959 5
        if (empty($this->_oauthClaimsAuthorizationHeader)) {
960
            // User authorization was not processed by Oauth2Module.
961 1
            return $default;
962
        }
963 4
        if (Yii::$app->request->getHeaders()->get('Authorization') !== $this->_oauthClaimsAuthorizationHeader) {
964 1
            throw new InvalidCallException(
965
                'App Request Authorization header does not match the processed Oauth header.'
966
            );
967
        }
968 3
        return $this->_oauthClaims[$attribute] ?? $default;
969
    }
970
971
    /**
972
     * Helper function to ensure the required properties are configured for the module.
973
     * @param $properties
974
     * @throws InvalidConfigException
975
     * @since 1.0.0
976
     */
977 27
    protected function ensureProperties($properties)
978
    {
979 27
        foreach ($properties as $property) {
980 27
            if (empty($this->$property)) {
981 6
                throw new InvalidConfigException('$' . $property . ' must be set.');
982
            }
983
        }
984
    }
985
}
986