Passed
Push — master ( 09be9f...ff655c )
by Rutger
05:15 queued 01:19
created

Oauth2Client::setAllowAuthCodeWithoutPkce()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 2
c 0
b 0
f 0
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
cc 1
nc 1
nop 1
crap 1
1
<?php
2
3
namespace rhertogh\Yii2Oauth2Server\models;
4
5
use rhertogh\Yii2Oauth2Server\helpers\DiHelper;
6
use rhertogh\Yii2Oauth2Server\helpers\EnvironmentHelper;
7
use rhertogh\Yii2Oauth2Server\helpers\exceptions\EnvironmentVariableNotAllowedException;
8
use rhertogh\Yii2Oauth2Server\helpers\exceptions\EnvironmentVariableNotSetException;
9
use rhertogh\Yii2Oauth2Server\interfaces\models\Oauth2ClientInterface;
10
use rhertogh\Yii2Oauth2Server\interfaces\models\Oauth2ClientScopeInterface;
11
use rhertogh\Yii2Oauth2Server\interfaces\models\Oauth2ScopeInterface;
12
use rhertogh\Yii2Oauth2Server\models\behaviors\DateTimeBehavior;
13
use rhertogh\Yii2Oauth2Server\models\queries\Oauth2ClientScopeQuery;
14
use rhertogh\Yii2Oauth2Server\models\traits\Oauth2EnabledTrait;
15
use rhertogh\Yii2Oauth2Server\models\traits\Oauth2EntityIdentifierTrait;
16
use rhertogh\Yii2Oauth2Server\Oauth2Module;
17
use Yii;
18
use yii\base\Exception;
19
use yii\base\InvalidArgumentException;
20
use yii\base\InvalidConfigException;
21
use yii\base\UnknownPropertyException;
22
use yii\helpers\ArrayHelper;
23
use yii\helpers\Json;
24
25
class Oauth2Client extends base\Oauth2Client implements Oauth2ClientInterface
26
{
27
    use Oauth2EntityIdentifierTrait;
28
    use Oauth2EnabledTrait;
29
30
    protected const ENCRYPTED_ATTRIBUTES = ['secret', 'old_secret'];
31
32
    /**
33
     * @var Oauth2Module
34
     */
35
    protected $module;
36
37
    /**
38
     * Minimum length for new client secrets.
39
     * @var int
40
     */
41
    protected $minimumSecretLength = 10;
42
43
    /////////////////////////////
44
    /// ActiveRecord Settings ///
45
    /////////////////////////////
46
47
    /**
48
     * @inheritDoc
49
     */
50 65
    public function behaviors()
51
    {
52 65
        return ArrayHelper::merge(parent::behaviors(), [
53 65
            'dateTimeBehavior' => DateTimeBehavior::class
54 65
        ]);
55
    }
56
57
    /**
58
     * @inheritDoc
59
     */
60 1
    public function rules()
61
    {
62 1
        return ArrayHelper::merge(parent::rules(), [
63 1
            [
64 1
                ['secret'],
65 1
                'required',
66 1
                'when' => fn(self $model) => $model->isConfidential(),
67 1
            ],
68 1
        ]);
69
    }
70
71
    /////////////////////////
72
    /// Getters & Setters ///
73
    /////////////////////////
74
75
    /**
76
     * @inheritdoc
77
     */
78 66
    public function __set($name, $value)
79
    {
80 66
        if ($name === 'secret') { // Don't allow setting the secret via magic method.
81 1
            throw new UnknownPropertyException('For security the "secret" property must be set via setSecret()');
82
        } else {
83 65
            parent::__set($name, $value);
84
        }
85
    }
86
87
    /**
88
     * @inheritDoc
89
     */
90 8
    public function getModule(): Oauth2Module
91
    {
92 8
        return $this->module;
93
    }
94
95
    /**
96
     * @inheritDoc
97
     */
98 52
    public function setModule($module)
99
    {
100 52
        $this->module = $module;
101 52
        return $this;
102
    }
103
104
    /**
105
     * @inheritdoc
106
     */
107 4
    public function getName()
108
    {
109 4
        return $this->name;
110
    }
111
112
    /**
113
     * @inheritdoc
114
     */
115 1
    public function setName($name)
116
    {
117 1
        $this->name = $name;
118 1
        return $this;
119
    }
120
121
    /**
122
     * @inheritdoc
123
     */
124 1
    public function getType()
125
    {
126 1
        return $this->type;
127
    }
128
129
    /**
130
     * @inheritdoc
131
     */
132 2
    public function setType($type)
133
    {
134 2
        if (!in_array($type, Oauth2ClientInterface::TYPES)) {
135 1
            throw new InvalidArgumentException('Unknown type "' . $type . '".');
136
        }
137
138 1
        $this->type = $type;
139 1
        return $this;
140
    }
141
142
    /**
143
     * @inheritdoc
144
     * @throws InvalidConfigException
145
     * @throws EnvironmentVariableNotSetException
146
     * @throws EnvironmentVariableNotAllowedException
147
     */
148 15
    public function getRedirectUri()
149
    {
150 15
        return $this->getUrisAttribute('redirect_uris');
151
    }
152
153
    /**
154
     * @inheritdoc
155
     * @throws InvalidConfigException
156
     * @throws EnvironmentVariableNotSetException
157
     * @throws EnvironmentVariableNotAllowedException
158
     */
159
    public function getPostLogoutRedirectUris()
160
    {
161
        return $this->getUrisAttribute('post_logout_redirect_uris');
162
    }
163
164
    /**
165
     * @param string $attribute
166
     * @return string[]
167
     * @throws InvalidConfigException
168
     * @throws EnvironmentVariableNotSetException
169
     * @throws EnvironmentVariableNotAllowedException
170
     */
171 15
    protected function getUrisAttribute($attribute)
172
    {
173 15
        $uris = $this->$attribute;
174 15
        if (empty($uris)) {
175 1
            return [];
176
        }
177
178
        // Compatibility with DBMSs that don't support JSON data type.
179 14
        if (is_string($uris)) {
180
            try {
181 11
                $uris = Json::decode($uris);
182 1
            } catch (InvalidArgumentException $e) {
183 1
                throw new InvalidConfigException('Invalid json in `' . $attribute . '` for client ' . $this->id, 0, $e);
184
            }
185
        }
186
187 13
        $redirectUrisEnvVarConfig = $this->getRedirectUrisEnvVarConfig();
188 13
        if ($redirectUrisEnvVarConfig && version_compare(PHP_VERSION, '8.1.0', '<')) {
189
            // PHP < 8.1 can only handle indexed array when unpacking.
190 6
            $redirectUrisEnvVarConfig = array_values($redirectUrisEnvVarConfig);
191
        }
192
193 13
        if (is_string($uris)) {
194 4
            if ($redirectUrisEnvVarConfig && preg_match('/^\${[a-zA-Z0-9_]+}$/', $uris)) {
195 3
                $uris = EnvironmentHelper::parseEnvVars($uris, ...$redirectUrisEnvVarConfig);
0 ignored issues
show
Bug introduced by
$redirectUrisEnvVarConfig is expanded, but the parameter $allowList of rhertogh\Yii2Oauth2Serve...tHelper::parseEnvVars() does not expect variable arguments. ( Ignorable by Annotation )

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

195
                $uris = EnvironmentHelper::parseEnvVars($uris, /** @scrutinizer ignore-type */ ...$redirectUrisEnvVarConfig);
Loading history...
196
                try {
197
                    // Try to parse the content of the environment variable(s) as JSON.
198 3
                    $uris = Json::decode($uris);
199 1
                } catch (InvalidArgumentException $e) {
200
                    // Use as plain text if it failed.
201
                }
202
            }
203
        }
204
205 13
        if (is_string($uris)) {
206 3
            $uris = [$uris];
207 10
        } elseif (is_array($uris)) {
208 9
            $uris = array_values($uris);
209
        } else {
210 1
            throw new InvalidConfigException('`' . $attribute . '` must be a JSON encoded string or array of strings.');
211
        }
212
213 12
        foreach ($uris as $key => $uri) {
214 12
            if (!is_string($uri)) {
215 1
                throw new InvalidConfigException('`' . $attribute . '` must be a JSON encoded string or array of strings.');
216
            }
217 11
            if ($redirectUrisEnvVarConfig) {
218 6
                $uris[$key] = EnvironmentHelper::parseEnvVars($uri, ...$redirectUrisEnvVarConfig);
219 5
                if (!$uris[$key]) {
220 1
                    unset($uris[$key]);
221
                }
222
            }
223
        }
224 10
        return array_values($uris); // Re-index array in case elements were removed.
225
    }
226
227
    /**
228
     * @inheritDoc
229
     */
230 4
    public function setRedirectUri($uri)
231
    {
232 4
        return $this->setUrisAttribute('redirect_uris', $uri);
233
    }
234
235
    /**
236
     * @inheritDoc
237
     */
238
    public function setPostLogoutRedirectUris($uri)
239
    {
240
        return $this->setUrisAttribute('post_logout_redirect_uris', $uri);
241
    }
242
243
    /**
244
     * @param string $attribute
245
     * @param string|string[] $uri
246
     * @return $this
247
     */
248 4
    protected function setUrisAttribute($attribute, $uri)
249
    {
250 4
        if (is_array($uri)) {
251 2
            foreach ($uri as $value) {
252 2
                if (!is_string($value)) {
253 1
                    throw new InvalidArgumentException('When $uri is an array, its values must be strings.');
254
                }
255
            }
256 1
            $uri = Json::encode($uri);
257 2
        } elseif (is_string($uri)) {
0 ignored issues
show
introduced by
The condition is_string($uri) is always true.
Loading history...
258 1
            $uri = Json::encode([$uri]);
259
        } else {
260 1
            throw new InvalidArgumentException('$uri must be a string or an array, got: ' . gettype($uri));
261
        }
262
263 2
        $this->$attribute = $uri;
264
265 2
        return $this;
266
    }
267
268
    /**
269
     * @inheritdoc
270
     */
271 18
    public function getEnvVarConfig()
272
    {
273 18
        return $this->env_var_config;
274
    }
275
276
    /**
277
     * @inheritdoc
278
     */
279 9
    public function setEnvVarConfig($config)
280
    {
281 9
        $this->env_var_config = $config;
282 9
        return $this;
283
    }
284
285
    /**
286
     * @inheritdoc
287
     */
288 14
    public function getRedirectUrisEnvVarConfig()
289
    {
290 14
        $envVarConfig = $this->getEnvVarConfig();
291 14
        return is_array($envVarConfig) && array_key_exists('redirectUris', $envVarConfig)
292 7
            ? $this->getEnvVarConfig()['redirectUris']
293 14
            : $this->getModule()->clientRedirectUrisEnvVarConfig;
294
    }
295
296
    /**
297
     * @inheritdoc
298
     */
299 3
    public function getSecretsEnvVarConfig()
300
    {
301 3
        $envVarConfig = $this->getEnvVarConfig();
302 3
        return is_array($envVarConfig) && array_key_exists('secrets', $envVarConfig)
303 1
            ? $envVarConfig['secrets']
304 3
            : null;
305
    }
306
307 2
    public function isVariableRedirectUriQueryAllowed()
308
    {
309 2
        return (bool)$this->allow_variable_redirect_uri_query;
310
    }
311 1
    public function setAllowVariableRedirectUriQuery($allowVariableRedirectUriQuery)
312
    {
313 1
        $this->allow_variable_redirect_uri_query = $allowVariableRedirectUriQuery;
314 1
        return $this;
315
    }
316
317
    /**
318
     * @inheritDoc
319
     */
320 4
    public function getUserAccountSelection()
321
    {
322 4
        return $this->user_account_selection;
323
    }
324
325 4
    public function endUsersMayAuthorizeClient()
326
    {
327 4
        return $this->end_users_may_authorize_client;
328
    }
329
330 1
    public function setEndUsersMayAuthorizeClient($endUsersMayAuthorizeClient)
331
    {
332 1
        $this->end_users_may_authorize_client = $endUsersMayAuthorizeClient;
333 1
        return $this;
334
    }
335
336
    /**
337
     * @inheritDoc
338
     */
339 1
    public function setUserAccountSelection($userAccountSelectionConfig)
340
    {
341 1
        $this->user_account_selection = $userAccountSelectionConfig;
342 1
        return $this;
343
    }
344
345
    /**
346
     * @inheritDoc
347
     */
348 4
    public function isAuthCodeWithoutPkceAllowed()
349
    {
350 4
        return (bool)$this->allow_auth_code_without_pkce;
351
    }
352
353
    /**
354
     * @inheritDoc
355
     */
356 1
    public function setAllowAuthCodeWithoutPkce($allowAuthCodeWithoutPkce)
357
    {
358 1
        $this->allow_auth_code_without_pkce = $allowAuthCodeWithoutPkce;
359 1
        return $this;
360
    }
361
362
    /**
363
     * @inheritDoc
364
     */
365 2
    public function skipAuthorizationIfScopeIsAllowed()
366
    {
367 2
        return (bool)$this->skip_authorization_if_scope_is_allowed;
368
    }
369
370
    /**
371
     * @inheritDoc
372
     */
373 1
    public function setSkipAuthorizationIfScopeIsAllowed($skipAuthIfScopeIsAllowed)
374
    {
375 1
        $this->skip_authorization_if_scope_is_allowed = $skipAuthIfScopeIsAllowed;
376 1
        return $this;
377
    }
378
379
    /**
380
     * @inheritDoc
381
     */
382 1
    public function getClientCredentialsGrantUserId()
383
    {
384 1
        return $this->client_credentials_grant_user_id;
385
    }
386
387
    /**
388
     * @inheritDoc
389
     */
390 1
    public function setClientCredentialsGrantUserId($userId)
391
    {
392 1
        $this->client_credentials_grant_user_id = $userId;
393 1
        return $this;
394
    }
395
396
    /**
397
     * @inheritDoc
398
     */
399 1
    public function getOpenIdConnectAllowOfflineAccessWithoutConsent()
400
    {
401 1
        return (bool)$this->oidc_allow_offline_access_without_consent;
402
    }
403
404
    /**
405
     * @inheritDoc
406
     */
407 1
    public function setOpenIdConnectAllowOfflineAccessWithoutConsent($allowOfflineAccessWithoutConsent)
408
    {
409 1
        $this->oidc_allow_offline_access_without_consent = $allowOfflineAccessWithoutConsent;
410 1
        return $this;
411
    }
412
413
    /**
414
     * @inheritDoc
415
     */
416
    public function getOpenIdConnectRpInitiatedLogout()
417
    {
418
        return $this->oidc_rp_initiated_logout;
419
    }
420
421
    /**
422
     * @inheritDoc
423
     */
424
    public function setOpenIdConnectRpInitiatedLogout($skipLogoutValidation)
425
    {
426
        $this->oidc_rp_initiated_logout = $skipLogoutValidation;
427
        return $this;
428
    }
429
430
    /**
431
     * @inheritDoc
432
     */
433 1
    public function getOpenIdConnectUserinfoEncryptedResponseAlg()
434
    {
435 1
        return $this->oidc_userinfo_encrypted_response_alg;
436
    }
437
438
    /**
439
     * @inheritDoc
440
     */
441 1
    public function setOpenIdConnectUserinfoEncryptedResponseAlg($algorithm)
442
    {
443 1
        $this->oidc_userinfo_encrypted_response_alg = $algorithm;
444 1
        return $this;
445
    }
446
447
    /**
448
     * @inheritdoc
449
     */
450 9
    public function isConfidential()
451
    {
452 9
        return (int)$this->type !== static::TYPE_PUBLIC;
453
    }
454
455
    /**
456
     * @inheritDoc
457
     */
458 17
    public function getAllowGenericScopes()
459
    {
460 17
        return (bool)$this->allow_generic_scopes;
461
    }
462
463
    /**
464
     * @inheritDoc
465
     */
466 1
    public function setAllowGenericScopes($allowGenericScopes)
467
    {
468 1
        $this->allow_generic_scopes = $allowGenericScopes;
469 1
        return $this;
470
    }
471
472
    /**
473
     * @inheritDoc
474
     */
475 1
    public function getExceptionOnInvalidScope()
476
    {
477 1
        return $this->exception_on_invalid_scope;
478
    }
479
480
    /**
481
     * @inheritDoc
482
     */
483 1
    public function setExceptionOnInvalidScope($exceptionOnInvalidScope)
484
    {
485 1
        $this->exception_on_invalid_scope = $exceptionOnInvalidScope;
486 1
        return $this;
487
    }
488
489
    /**
490
     * @inheritDoc
491
     */
492 3
    public static function getEncryptedAttributes()
493
    {
494 3
        return static::ENCRYPTED_ATTRIBUTES;
495
    }
496
497
    /**
498
     * @inheritDoc
499
     */
500 2
    public static function rotateStorageEncryptionKeys($cryptographer, $newKeyName = null)
501
    {
502 2
        $numUpdated = 0;
503 2
        $encryptedAttributes = static::getEncryptedAttributes();
504 2
        $query = static::find()->andWhere(['NOT', array_fill_keys($encryptedAttributes, null)]);
505
506 2
        $transaction = static::getDb()->beginTransaction();
507
        try {
508
            /** @var static $client */
509 2
            foreach ($query->each() as $client) {
510 2
                $client->rotateStorageEncryptionKey($cryptographer, $newKeyName);
511 2
                if ($client->getDirtyAttributes($encryptedAttributes)) {
512 2
                    $client->persist();
513 1
                    $numUpdated++;
514
                }
515
            }
516 1
            $transaction->commit();
517 1
        } catch (\Exception $e) {
518 1
            $transaction->rollBack();
519 1
            throw $e;
520
        }
521
522 1
        return $numUpdated;
523
    }
524
525
    /**
526
     * @inheritDoc
527
     */
528
    public static function getUsedStorageEncryptionKeys($cryptographer)
529
    {
530
        $encryptedAttributes = static::getEncryptedAttributes();
531
        $query = static::find()->andWhere(['NOT', array_fill_keys($encryptedAttributes, null)]);
532
533
        $keyUsage = [];
534
        foreach ($query->each() as $client) {
535
            foreach ($encryptedAttributes as $encryptedAttribute) {
536
                $data = $client->$encryptedAttribute;
537
                if (!empty($data)) {
538
                    ['keyName' => $keyName] = $cryptographer->parseData($data);
539
                    if (array_key_exists($keyName, $keyUsage)) {
540
                        $keyUsage[$keyName][] = $client->getPrimaryKey();
541
                    } else {
542
                        $keyUsage[$keyName] = [$client->getPrimaryKey()];
543
                    }
544
                }
545
            }
546
        }
547
548
        return $keyUsage;
549
    }
550
551
    /**
552
     * @inheritDoc
553
     */
554 3
    public function rotateStorageEncryptionKey($cryptographer, $newKeyName = null)
555
    {
556 3
        foreach (static::getEncryptedAttributes() as $attribute) {
557 3
            $data = $this->getAttribute($attribute);
558 3
            if ($data) {
559
                try {
560 3
                    $this->setAttribute($attribute, $cryptographer->rotateKey($data, $newKeyName));
561
                } catch (\Exception $e) {
562
                    throw new Exception('Unable to rotate key for client "' . $this->identifier
563
                        . '", attribute "' . $attribute . '": ' . $e->getMessage(), 0, $e);
564
                }
565
            }
566
        }
567
    }
568
569
    /**
570
     * @inheritDoc
571
     */
572 4
    public function setSecret($secret, $cryptographer, $oldSecretValidUntil = null, $keyName = null)
573
    {
574 4
        if ($this->isConfidential()) {
575 4
            if (!$this->validateNewSecret($secret, $error)) {
576 1
                throw new InvalidArgumentException($error);
577
            }
578
579
            // Ensure we clear out any old secret.
580 3
            $this->setAttribute('old_secret', null);
581 3
            $this->setAttribute('old_secret_valid_until', null);
582
583 3
            if ($oldSecretValidUntil) {
584 1
                $oldSecretData = $this->getAttribute('secret') ?? null;
585 1
                if ($oldSecretData) {
586
                    // Ensure correct encryption key.
587 1
                    $oldSecretData = $cryptographer->encryp($cryptographer->decrypt($oldSecretData), $keyName);
588 1
                    $this->setAttribute('old_secret', $oldSecretData);
589
590 1
                    if ($oldSecretValidUntil instanceof \DateInterval) {
591 1
                        $oldSecretValidUntil = (new \DateTimeImmutable())->add($oldSecretValidUntil);
592
                    }
593 1
                    $this->setAttribute('old_secret_valid_until', $oldSecretValidUntil);
594
                }
595
            }
596
597 3
            $this->setAttribute('secret', $cryptographer->encryp($secret, $keyName));
598
        } else {
599 1
            if ($secret !== null) {
600 1
                throw new InvalidArgumentException(
601 1
                    'The secret for a non-confidential client can only be set to `null`.'
602 1
                );
603
            }
604
605 1
            $this->setAttribute('secret', null);
606
        }
607
608 3
        return $this;
609
    }
610
611
    /**
612
     * @inheritDoc
613
     */
614 1
    public function setSecretsAsEnvVars($secretEnvVarName, $oldSecretEnvVarName = null, $oldSecretValidUntil = null)
615
    {
616 1
        if (empty($secretEnvVarName)) {
617
            throw new InvalidArgumentException(
618
                'Parameter $secretEnvVarName can not be empty.'
619
            );
620
        }
621
622 1
        if ($oldSecretEnvVarName) {
623 1
            if (!$oldSecretValidUntil) {
624
                throw new InvalidArgumentException(
625
                    'Parameter $oldSecretValidUntil must be set when $oldSecretEnvVar is set.'
626
                );
627
            }
628 1
            $this->setAttribute('old_secret', '${' . $oldSecretEnvVarName . '}');
629
630 1
            if ($oldSecretValidUntil instanceof \DateInterval) {
631 1
                $oldSecretValidUntil = (new \DateTimeImmutable())->add($oldSecretValidUntil);
632
            }
633 1
            $this->setAttribute('old_secret_valid_until', $oldSecretValidUntil);
634
        } else {
635
            $this->setAttribute('old_secret', null);
636
            $this->setAttribute('old_secret_valid_until', null);
637
        }
638
639 1
        $this->setAttribute('secret', '${' . $secretEnvVarName . '}');
640
641 1
        return $this;
642
    }
643
644
    /**
645
     * @inheritDoc
646
     */
647 4
    public function validateNewSecret($secret, &$error)
648
    {
649 4
        $error = null;
650 4
        if (mb_strlen($secret) < $this->getMinimumSecretLength()) {
651 1
            $error = 'Secret should be at least ' . $this->getMinimumSecretLength() . ' characters.';
652
        }
653
654 4
        return $error === null;
655
    }
656
657
    /**
658
     * @inheritDoc
659
     */
660 5
    public function getMinimumSecretLength()
661
    {
662 5
        return $this->minimumSecretLength;
663
    }
664
665
    /**
666
     * @inheritDoc
667
     */
668 1
    public function setMinimumSecretLength($minimumSecretLength)
669
    {
670 1
        if (!(int)$minimumSecretLength) {
671
            throw new InvalidArgumentException('$minimumSecretLength can not be empty.');
672
        }
673 1
        $this->minimumSecretLength = (int)$minimumSecretLength;
674 1
        return $this;
675
    }
676
677
    /**
678
     * @inheritDoc
679
     */
680 3
    public function getDecryptedSecret($cryptographer)
681
    {
682 3
        return $cryptographer->decrypt($this->envVarParseSecret($this->secret));
683
    }
684
685
    /**
686
     * @inheritDoc
687
     */
688 2
    public function getDecryptedOldSecret($cryptographer)
689
    {
690 2
        return $cryptographer->decrypt($this->envVarParseSecret($this->old_secret));
691
    }
692
693
    /**
694
     * Replaces environment variables with their values in the secret
695
     *
696
     * @param string $secret
697
     * @return string
698
     * @throws EnvironmentVariableNotAllowedException
699
     * @throws EnvironmentVariableNotSetException
700
     * @throws InvalidConfigException
701
     */
702 3
    protected function envVarParseSecret($secret)
703
    {
704 3
        $secretsEnvVarConfig = $this->getSecretsEnvVarConfig();
705 3
        if ($secretsEnvVarConfig) {
706 1
            if (version_compare(PHP_VERSION, '8.1.0', '<')) {
707
                // PHP < 8.1 can only handle indexed array when unpacking.
708 1
                $secretsEnvVarConfig = array_values($secretsEnvVarConfig);
709
            }
710 1
            $secret = EnvironmentHelper::parseEnvVars($secret, ...$secretsEnvVarConfig);
0 ignored issues
show
Bug introduced by
$secretsEnvVarConfig is expanded, but the parameter $allowList of rhertogh\Yii2Oauth2Serve...tHelper::parseEnvVars() does not expect variable arguments. ( Ignorable by Annotation )

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

710
            $secret = EnvironmentHelper::parseEnvVars($secret, /** @scrutinizer ignore-type */ ...$secretsEnvVarConfig);
Loading history...
711 2
        } elseif (preg_match(EnvironmentHelper::ENV_VAR_REGEX, $secret)) {
712
            throw new InvalidConfigException('Environment variable used without env_var_config being set.');
713
        }
714
715 3
        return $secret;
716
    }
717
718
    /**
719
     * @inheritDoc
720
     */
721
    public function getOldSecretValidUntil()
722
    {
723
        return $this->old_secret_valid_until;
724
    }
725
726
    /**
727
     * @inheritdoc
728
     */
729 2
    public function validateSecret($secret, $cryptographer)
730
    {
731 2
        return is_string($secret)
732 2
            && strlen($secret)
733 2
            && (
734 2
                Yii::$app->security->compareString($this->getDecryptedSecret($cryptographer), $secret)
735 2
                || (
736 2
                    !empty($this->old_secret)
737 2
                    && !empty($this->old_secret_valid_until)
738 2
                    && $this->old_secret_valid_until > (new \DateTime())
739 2
                    && Yii::$app->security->compareString($this->getDecryptedOldSecret($cryptographer), $secret)
740 2
                )
741 2
            );
742
    }
743
744
    /**
745
     * @inheritdoc
746
     */
747 1
    public function getLogoUri()
748
    {
749 1
        return $this->logo_uri;
750
    }
751
752
    /**
753
     * @inheritdoc
754
     */
755 1
    public function setLogoUri($logoUri)
756
    {
757 1
        $this->logo_uri = $logoUri;
758 1
        return $this;
759
    }
760
761
    /**
762
     * @inheritdoc
763
     */
764 1
    public function getTermsOfServiceUri()
765
    {
766 1
        return $this->tos_uri;
767
    }
768
769
    /**
770
     * @inheritdoc
771
     */
772 1
    public function setTermsOfServiceUri($tosUri)
773
    {
774 1
        $this->tos_uri = $tosUri;
775 1
        return $this;
776
    }
777
778
    /**
779
     * @inheritdoc
780
     */
781 1
    public function getContacts()
782
    {
783 1
        return $this->contacts;
784
    }
785
786
    /**
787
     * @inheritdoc
788
     */
789 1
    public function setContacts($contacts)
790
    {
791 1
        $this->contacts = $contacts;
792 1
        return $this;
793
    }
794
795
    /**
796
     * @inheritDoc
797
     */
798 1
    public function getGrantTypes()
799
    {
800 1
        return (int)$this->grant_types;
801
    }
802
803
    /**
804
     * @inheritDoc
805
     */
806 2
    public function setGrantTypes($grantTypes)
807
    {
808 2
        $grantTypeIds = array_flip(Oauth2Module::GRANT_TYPE_MAPPING);
809 2
        for ($i = (int)log(PHP_INT_MAX, 2); $i >= 0; $i--) {
810 2
            $grantTypeId = (int)pow(2, $i);
811 2
            if ($grantTypes & $grantTypeId) {
812 2
                if (!array_key_exists($grantTypeId, $grantTypeIds)) {
813 1
                    throw new InvalidArgumentException('Unknown Grant Type ID: ' . $grantTypeId);
814
                }
815
            }
816
        }
817
818 1
        $this->grant_types = $grantTypes;
819
820 1
        return $this;
821
    }
822
823
    /**
824
     * @inheritDoc
825
     */
826 2
    public function validateGrantType($grantTypeIdentifier)
827
    {
828 2
        $grantTypeId = Oauth2Module::getGrantTypeId($grantTypeIdentifier);
829 2
        if (empty($grantTypeId)) {
830 1
            throw new InvalidArgumentException('Unknown grant type "' . $grantTypeIdentifier . '".');
831
        }
832
833 1
        return (bool)($this->getGrantTypes() & $grantTypeId);
834
    }
835
836
    /**
837
     * @inheritDoc
838
     * @throws InvalidConfigException
839
     */
840 10
    public function validateAuthRequestScopes($scopeIdentifiers, &$unknownScopes = [], &$unauthorizedScopes = [])
841
    {
842 10
        if (empty($scopeIdentifiers)) {
843 1
            $unknownScopes = [];
844 1
            $unauthorizedScopes = [];
845 1
            return true;
846
        }
847
848
        /** @var Oauth2ScopeInterface $scopeClass */
849 9
        $scopeClass = DiHelper::getValidatedClassName(Oauth2ScopeInterface::class);
850 9
        $knownScopeIdentifiers = $scopeClass::find()
851 9
            ->andWhere(['identifier' => $scopeIdentifiers])
852 9
            ->select('identifier')
853 9
            ->column();
854
855 9
        $unknownScopes = array_diff($scopeIdentifiers, $knownScopeIdentifiers);
856
857 9
        $allowedScopeIdentifiers = array_map(
858 9
            fn($scope) => $scope->getIdentifier(),
859 9
            $this->getAllowedScopes($knownScopeIdentifiers)
860 9
        );
861
862 9
        $unauthorizedScopes = array_values(array_diff($knownScopeIdentifiers, $allowedScopeIdentifiers));
863
864 9
        return empty($unknownScopes) && empty($unauthorizedScopes);
865
    }
866
867
    /**
868
     * @inheritDoc
869
     * @throws InvalidConfigException
870
     */
871 16
    public function getAllowedScopes($requestedScopeIdentifiers = [])
872
    {
873
        /** @var Oauth2ClientScopeInterface $clientScopeClass */
874 16
        $clientScopeClass = DiHelper::getValidatedClassName(Oauth2ClientScopeInterface::class);
875 16
        $clientScopeTableName = $clientScopeClass::tableName();
876
        /** @var Oauth2ScopeInterface $scopeClass */
877 16
        $scopeClass = DiHelper::getValidatedClassName(Oauth2ScopeInterface::class);
878 16
        $scopeTableName = $scopeClass::tableName();
879
880 16
        if (is_array($requestedScopeIdentifiers)) {
881 14
            $possibleScopesConditions = [
882
                // Requested and default scopes defined for this client.
883 14
                ['AND',
884 14
                    [$clientScopeTableName . '.client_id' => $this->getPrimaryKey()],
885 14
                    [$clientScopeTableName . '.enabled' => 1],
886 14
                    ['OR',
887 14
                        ...(
888 14
                        !empty($requestedScopeIdentifiers)
889 9
                            ? [[$scopeTableName . '.identifier' => $requestedScopeIdentifiers]]
890
                            : []
891 14
                        ),
892 14
                        ['NOT', [
893 14
                            $clientScopeTableName . '.applied_by_default' => Oauth2ScopeInterface::APPLIED_BY_DEFAULT_NO
894 14
                        ]],
895 14
                        ['AND',
896 14
                            [$clientScopeTableName . '.applied_by_default' => null],
897 14
                            ['NOT', [
898 14
                                $scopeTableName . '.applied_by_default' => Oauth2ScopeInterface::APPLIED_BY_DEFAULT_NO
899 14
                            ]],
900 14
                        ],
901 14
                    ],
902 14
                ],
903 14
            ];
904
        } else {
905 2
            if ($requestedScopeIdentifiers === true) {
906 2
                $possibleScopesConditions = [
907
                    // All scopes defined for this client.
908 2
                    [$clientScopeTableName . '.enabled' => 1],
909 2
                ];
910
            } else {
911
                throw new InvalidArgumentException('`$possibleScopesConditions` must be either an array of strings or `true`.');
912
            }
913
        }
914
915 16
        $allowGenericScopes = $this->getAllowGenericScopes();
916 16
        if ($allowGenericScopes) {
917 5
            if (is_array($requestedScopeIdentifiers)) {
918
                // Requested and default scopes defined by scope for all clients.
919 4
                $possibleScopesConditions[] = ['AND',
920 4
                    [$clientScopeTableName . '.client_id' => null],
921 4
                    ['OR',
922 4
                        ...(
923 4
                        !empty($requestedScopeIdentifiers)
924 2
                            ? [[$scopeTableName . '.identifier' => $requestedScopeIdentifiers]]
925
                            : []
926 4
                        ),
927 4
                        ['NOT', [$scopeTableName . '.applied_by_default' => Oauth2ScopeInterface::APPLIED_BY_DEFAULT_NO]],
928 4
                    ],
929 4
                ];
930 1
            } elseif ($requestedScopeIdentifiers === true) {
931
                // All scopes defined by scope for all clients.
932 1
                $possibleScopesConditions[] = [$clientScopeTableName . '.client_id' => null];
933
            }
934
        }
935
936 16
        return $scopeClass::find()
937 16
            ->joinWith(
938 16
                ['clientScopes' => function(Oauth2ClientScopeQuery $query) use ($clientScopeTableName) {
0 ignored issues
show
Coding Style introduced by
Expected 1 space after FUNCTION keyword; 0 found
Loading history...
939 16
                    $query->andOnCondition([$clientScopeTableName . '.client_id' => $this->getPrimaryKey()]);
940 16
                }],
941 16
                true
942 16
            )
943 16
            ->enabled()
944 16
            ->andWhere(['OR', ...$possibleScopesConditions])
945 16
            ->orderBy('id')
946 16
            ->all();
947
    }
948
949
    /**
950
     * @inheritdoc
951
     * @return array{
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{ at position 2 could not be parsed: the token is null at position 2.
Loading history...
952
     *     'unaffected': Oauth2ClientScopeInterface[],
953
     *     'new': Oauth2ClientScopeInterface[],
954
     *     'updated': Oauth2ClientScopeInterface[],
955
     *     'deleted': Oauth2ClientScopeInterface[],
956
     * }
957
     */
958 11
    public function syncClientScopes($scopes, $scopeRepository)
959
    {
960 11
        if (is_string($scopes)) {
961 2
            $scopes = array_filter(array_map('trim', explode(' ', $scopes)));
962 9
        } elseif ($scopes === null) {
963 1
            $scopes = [];
964 8
        } elseif (!is_array($scopes)) {
965 1
            throw new InvalidArgumentException('$scopes must be a string, an array or null.');
966
        }
967
968
        /** @var class-string<Oauth2ClientScopeInterface> $clientScopeClass */
969 10
        $clientScopeClass = DiHelper::getValidatedClassName(Oauth2ClientScopeInterface::class);
970
971
        /** @var Oauth2ClientScopeInterface[] $origClientScopes */
972 10
        $origClientScopes = $clientScopeClass::findAll([
973 10
            'client_id' => $this->getPrimaryKey(),
974 10
        ]);
975
976 10
        $origClientScopes = array_combine(
977 10
            array_map(
978 10
                fn(Oauth2ClientScopeInterface $clientScope) => implode('-', $clientScope->getPrimaryKey(true)),
979 10
                $origClientScopes
980 10
            ),
981 10
            $origClientScopes
982 10
        );
983
984
        /** @var Oauth2ClientScopeInterface[] $clientScopes */
985 10
        $clientScopes = [];
986
987 10
        foreach ($scopes as $key => $value) {
988 9
            if ($value instanceof Oauth2ClientScopeInterface) {
989 2
                $clientScope = $value;
990 2
                $clientScope->client_id = $this->getPrimaryKey(); // Ensure PK is set.
0 ignored issues
show
Bug introduced by
Accessing client_id on the interface rhertogh\Yii2Oauth2Serve...th2ClientScopeInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
991 2
                $pkIndex = implode('-', $clientScope->getPrimaryKey(true));
992 2
                if (array_key_exists($pkIndex, $origClientScopes)) {
993
                    // Overwrite orig (might still be considered "unchanged" when new ClientScope is not "dirty").
994 2
                    $origClientScopes[$pkIndex] = $clientScope;
995
                }
996
            } else {
997
998 7
                $scopeIdentifier = null;
999 7
                $clientScopeConfig = [
1000 7
                    'client_id' => $this->getPrimaryKey(),
1001 7
                ];
1002
1003 7
                if (is_string($value)) {
1004 3
                    $scopeIdentifier = $value;
1005 4
                } elseif ($value instanceof Oauth2ScopeInterface) {
1006 2
                    $scopePk = $value->getPrimaryKey();
1007 2
                    if ($scopePk) {
1008 1
                        $clientScopeConfig = ArrayHelper::merge(
1009 1
                            $clientScopeConfig,
1010 1
                            ['scope_id' => $scopePk]
1011 1
                        );
1012
                    } else {
1013
                        // New model, using identifier.
1014 2
                        $scopeIdentifier = $value->getIdentifier();
1015
                    }
1016 2
                } elseif (is_array($value)) {
1017 1
                    $clientScopeConfig = ArrayHelper::merge(
1018 1
                        $clientScopeConfig,
1019 1
                        $value,
1020 1
                    );
1021 1
                    if (empty($clientScopeConfig['scope_id'])) {
1022 1
                        $scopeIdentifier = $key;
1023
                    }
1024
                } else {
1025 1
                    throw new InvalidArgumentException(
1026 1
                        'If $scopes is an array, its values must be a string, array or an instance of '
1027 1
                        . Oauth2ClientScopeInterface::class . ' or ' . Oauth2ScopeInterface::class . '.'
1028 1
                    );
1029
                }
1030
1031 6
                if (isset($scopeIdentifier)) {
1032 4
                    $scope = $scopeRepository->getScopeEntityByIdentifier($scopeIdentifier);
1033 4
                    if (empty($scope)) {
1034 1
                        throw new InvalidArgumentException('No scope with identifier "'
1035 1
                            . $scopeIdentifier . '" found.');
1036
                    }
1037 3
                    if (!($scope instanceof Oauth2ScopeInterface)) {
1038
                        throw new InvalidConfigException(get_class($scope)
1039
                            . ' must implement ' . Oauth2ScopeInterface::class);
1040
                    }
1041 3
                    $clientScopeConfig['scope_id'] = $scope->getPrimaryKey();
1042
                } else {
1043 3
                    if (empty($clientScopeConfig['scope_id'])) {
1044 1
                        throw new InvalidArgumentException('Element ' . $key
1045 1
                            . ' in $scope should specify either the scope id or its identifier.');
1046
                    }
1047
                }
1048
1049 4
                $pkIndex = $clientScopeConfig['client_id'] . '-' . $clientScopeConfig['scope_id'];
1050 4
                if (array_key_exists($pkIndex, $origClientScopes)) {
1051 4
                    $clientScope = $origClientScopes[$pkIndex];
1052 4
                    $clientScope->setAttributes($clientScopeConfig, false);
1053
                } else {
1054
                    /** @var Oauth2ClientScopeInterface $clientScope */
1055 4
                    $clientScope = Yii::createObject(ArrayHelper::merge(
1056 4
                        ['class' => $clientScopeClass],
1057 4
                        $clientScopeConfig
1058 4
                    ));
1059
                }
1060
            }
1061
1062 6
            $pkIndex = implode('-', $clientScope->getPrimaryKey(true));
1063 6
            $clientScopes[$pkIndex] = $clientScope;
1064
        }
1065
1066 7
        $transaction = static::getDb()->beginTransaction();
1067
        try {
1068
            // Delete records no longer present in the provided data.
1069
            /** @var self[]|array[] $deleteClientScopes */
1070 7
            $deleteClientScopes = array_diff_key($origClientScopes, $clientScopes);
1071 7
            foreach ($deleteClientScopes as $deleteClientScope) {
1072 6
                $deleteClientScope->delete();
1073
            }
1074
1075
            // Create records not present in the provided data.
1076 7
            $createClientScopes = array_diff_key($clientScopes, $origClientScopes);
1077 7
            foreach ($createClientScopes as $createClientScope) {
1078 6
                $createClientScope->persist();
1079
            }
1080
1081
            // Update existing records if needed.
1082 6
            $unaffectedClientScopes = [];
1083 6
            $updatedClientScopes = [];
1084 6
            foreach (array_intersect_key($origClientScopes, $clientScopes) as $key => $existingClientScope) {
1085 5
                if ($existingClientScope->getDirtyAttributes()) {
1086 2
                    $existingClientScope->persist();
1087 2
                    $updatedClientScopes[$key] = $existingClientScope;
1088
                } else {
1089 5
                    $unaffectedClientScopes[$key] = $existingClientScope;
1090
                }
1091
            }
1092
1093 6
            $transaction->commit();
1094 1
        } catch (\Exception $e) {
1095 1
            $transaction->rollBack();
1096 1
            throw $e;
1097
        }
1098
1099 6
        return [
1100 6
            'unaffected' => $unaffectedClientScopes,
1101 6
            'new' => $createClientScopes,
1102 6
            'updated' => $updatedClientScopes,
1103 6
            'deleted' => $deleteClientScopes,
1104 6
        ];
1105
    }
1106
}
1107