Passed
Push — master ( 0f5341...8d5d80 )
by Rutger
03:20
created

Oauth2Client::getEnvVarConfig()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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

199
                $uris[$key] = EnvironmentHelper::parseEnvVars($uri, /** @scrutinizer ignore-type */ ...$redirectUrisEnvVarConfig);
Loading history...
200 5
                if (!$uris[$key]) {
201 1
                    unset($uris[$key]);
202
                }
203
            }
204
        }
205 10
        return array_values($uris); // Re-index array in case elements were removed.
206
    }
207
208
    /**
209
     * @inheritDoc
210
     */
211 4
    public function setRedirectUri($uri)
212
    {
213 4
        if (is_array($uri)) {
214 2
            foreach ($uri as $value) {
215 2
                if (!is_string($value)) {
216 1
                    throw new InvalidArgumentException('When $uri is an array, its values must be strings.');
217
                }
218
            }
219 1
            $uri = Json::encode($uri);
220 2
        } elseif (is_string($uri)) {
221 1
            $uri = Json::encode([$uri]);
222
        } else {
223 1
            throw new InvalidArgumentException('$uri must be a string or an array, got: ' . gettype($uri));
224
        }
225
226 2
        $this->redirect_uris = $uri;
0 ignored issues
show
Documentation Bug introduced by
It seems like $uri of type string is incompatible with the declared type array of property $redirect_uris.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
227
228 2
        return $this;
229
    }
230
231
    /**
232
     * @inheritdoc
233
     */
234 18
    public function getEnvVarConfig()
235
    {
236 18
        return $this->env_var_config;
237
    }
238
239
    /**
240
     * @inheritdoc
241
     */
242 9
    public function setEnvVarConfig($config)
243
    {
244 9
        $this->env_var_config = $config;
245 9
        return $this;
246
    }
247
248
    /**
249
     * @inheritdoc
250
     */
251 14
    public function getRedirectUrisEnvVarConfig()
252
    {
253 14
        $envVarConfig = $this->getEnvVarConfig();
254 14
        return is_array($envVarConfig) && array_key_exists('redirectUris', $envVarConfig)
255 7
            ? $this->getEnvVarConfig()['redirectUris']
256 14
            : $this->getModule()->clientRedirectUrisEnvVarConfig;
257
    }
258
259
    /**
260
     * @inheritdoc
261
     */
262 3
    public function getSecretsEnvVarConfig()
263
    {
264 3
        $envVarConfig = $this->getEnvVarConfig();
265 3
        return is_array($envVarConfig) && array_key_exists('secrets', $envVarConfig)
266 1
            ? $envVarConfig['secrets']
267 3
            : null;
268
    }
269
270 2
    public function isVariableRedirectUriQueryAllowed()
271
    {
272 2
        return (bool)$this->allow_variable_redirect_uri_query;
273
    }
274 1
    public function setAllowVariableRedirectUriQuery($allowVariableRedirectUriQuery)
275
    {
276 1
        $this->allow_variable_redirect_uri_query = $allowVariableRedirectUriQuery;
277 1
        return $this;
278
    }
279
280
    /**
281
     * @inheritDoc
282
     */
283 4
    public function getUserAccountSelection()
284
    {
285 4
        return $this->user_account_selection;
286
    }
287
288 4
    public function endUsersMayAuthorizeClient()
289
    {
290 4
        return $this->end_users_may_authorize_client;
291
    }
292
293 1
    public function setEndUsersMayAuthorizeClient($endUsersMayAuthorizeClient)
294
    {
295 1
        $this->end_users_may_authorize_client = $endUsersMayAuthorizeClient;
296 1
        return $this;
297
    }
298
299
    /**
300
     * @inheritDoc
301
     */
302 1
    public function setUserAccountSelection($userAccountSelectionConfig)
303
    {
304 1
        $this->user_account_selection = $userAccountSelectionConfig;
305 1
        return $this;
306
    }
307
308
    /**
309
     * @inheritDoc
310
     */
311 4
    public function isAuthCodeWithoutPkceAllowed()
312
    {
313 4
        return (bool)$this->allow_auth_code_without_pkce;
314
    }
315
316
    /**
317
     * @inheritDoc
318
     */
319 1
    public function setAllowAuthCodeWithoutPkce($allowAuthCodeWithoutPkce)
320
    {
321 1
        $this->allow_auth_code_without_pkce = $allowAuthCodeWithoutPkce;
322 1
        return $this;
323
    }
324
325
    /**
326
     * @inheritDoc
327
     */
328 2
    public function skipAuthorizationIfScopeIsAllowed()
329
    {
330 2
        return (bool)$this->skip_authorization_if_scope_is_allowed;
331
    }
332
333
    /**
334
     * @inheritDoc
335
     */
336 1
    public function setSkipAuthorizationIfScopeIsAllowed($skipAuthIfScopeIsAllowed)
337
    {
338 1
        $this->skip_authorization_if_scope_is_allowed = $skipAuthIfScopeIsAllowed;
339 1
        return $this;
340
    }
341
342
    /**
343
     * @inheritDoc
344
     */
345 1
    public function getClientCredentialsGrantUserId()
346
    {
347 1
        return $this->client_credentials_grant_user_id;
348
    }
349
350
    /**
351
     * @inheritDoc
352
     */
353 1
    public function setClientCredentialsGrantUserId($userId)
354
    {
355 1
        $this->client_credentials_grant_user_id = $userId;
356 1
        return $this;
357
    }
358
359
    /**
360
     * @inheritDoc
361
     */
362 1
    public function getOpenIdConnectAllowOfflineAccessWithoutConsent()
363
    {
364 1
        return (bool)$this->oidc_allow_offline_access_without_consent;
365
    }
366
367
    /**
368
     * @inheritDoc
369
     */
370 1
    public function setOpenIdConnectAllowOfflineAccessWithoutConsent($allowOfflineAccessWithoutConsent)
371
    {
372 1
        $this->oidc_allow_offline_access_without_consent = $allowOfflineAccessWithoutConsent;
373 1
        return $this;
374
    }
375
376
    /**
377
     * @inheritDoc
378
     */
379 1
    public function getOpenIdConnectUserinfoEncryptedResponseAlg()
380
    {
381 1
        return $this->oidc_userinfo_encrypted_response_alg;
382
    }
383
384
    /**
385
     * @inheritDoc
386
     */
387 1
    public function setOpenIdConnectUserinfoEncryptedResponseAlg($algorithm)
388
    {
389 1
        $this->oidc_userinfo_encrypted_response_alg = $algorithm;
390 1
        return $this;
391
    }
392
393
    /**
394
     * @inheritdoc
395
     */
396 9
    public function isConfidential()
397
    {
398 9
        return (int)$this->type !== static::TYPE_PUBLIC;
399
    }
400
401
    /**
402
     * @inheritDoc
403
     */
404 19
    public function getScopeAccess()
405
    {
406 19
        return (int)$this->scope_access;
407
    }
408
409
    /**
410
     * @inheritDoc
411
     */
412 2
    public function setScopeAccess($scopeAccess)
413
    {
414 2
        if (!in_array($scopeAccess, Oauth2ClientInterface::SCOPE_ACCESSES)) {
415 1
            throw new InvalidArgumentException('Unknown scope access "' . $scopeAccess . '".');
416
        }
417
418 1
        $this->scope_access = $scopeAccess;
419 1
        return $this;
420
    }
421
422
    /**
423
     * @inheritDoc
424
     */
425 3
    public static function getEncryptedAttributes()
426
    {
427 3
        return static::ENCRYPTED_ATTRIBUTES;
428
    }
429
430
    /**
431
     * @inheritDoc
432
     */
433 2
    public static function rotateStorageEncryptionKeys($cryptographer, $newKeyName = null)
434
    {
435 2
        $numUpdated = 0;
436 2
        $encryptedAttributes = static::getEncryptedAttributes();
437 2
        $query = static::find()->andWhere(['NOT', array_fill_keys($encryptedAttributes, null)]);
438
439 2
        $transaction = static::getDb()->beginTransaction();
440
        try {
441
            /** @var static $client */
442 2
            foreach ($query->each() as $client) {
443 2
                $client->rotateStorageEncryptionKey($cryptographer, $newKeyName);
444 2
                if ($client->getDirtyAttributes($encryptedAttributes)) {
445 2
                    $client->persist();
446 1
                    $numUpdated++;
447
                }
448
            }
449 1
            $transaction->commit();
450 1
        } catch (\Exception $e) {
451 1
            $transaction->rollBack();
452 1
            throw $e;
453
        }
454
455 1
        return $numUpdated;
456
    }
457
458
    /**
459
     * @inheritDoc
460
     */
461
    public static function getUsedStorageEncryptionKeys($cryptographer)
462
    {
463
        $encryptedAttributes = static::getEncryptedAttributes();
464
        $query = static::find()->andWhere(['NOT', array_fill_keys($encryptedAttributes, null)]);
465
466
        $keyUsage = [];
467
        foreach ($query->each() as $client) {
468
            foreach ($encryptedAttributes as $encryptedAttribute) {
469
                $data = $client->$encryptedAttribute;
470
                if (!empty($data)) {
471
                    ['keyName' => $keyName] = $cryptographer->parseData($data);
472
                    if (array_key_exists($keyName, $keyUsage)) {
473
                        $keyUsage[$keyName][] = $client->getPrimaryKey();
474
                    } else {
475
                        $keyUsage[$keyName] = [$client->getPrimaryKey()];
476
                    }
477
                }
478
            }
479
        }
480
481
        return $keyUsage;
482
    }
483
484
    /**
485
     * @inheritDoc
486
     */
487 3
    public function rotateStorageEncryptionKey($cryptographer, $newKeyName = null)
488
    {
489 3
        foreach (static::getEncryptedAttributes() as $attribute) {
490 3
            $data = $this->getAttribute($attribute);
491 3
            if ($data) {
492
                try {
493 3
                    $this->setAttribute($attribute, $cryptographer->rotateKey($data, $newKeyName));
494
                } catch (\Exception $e) {
495
                    throw new Exception('Unable to rotate key for client "' . $this->identifier
496
                        . '", attribute "' . $attribute . '": ' . $e->getMessage(), 0, $e);
497
                }
498
            }
499
        }
500
    }
501
502
    /**
503
     * @inheritDoc
504
     */
505 4
    public function setSecret($secret, $cryptographer, $oldSecretValidUntil = null, $keyName = null)
506
    {
507 4
        if ($this->isConfidential()) {
508 4
            if (!$this->validateNewSecret($secret, $error)) {
509 1
                throw new InvalidArgumentException($error);
510
            }
511
512
            // Ensure we clear out any old secret.
513 3
            $this->setAttribute('old_secret', null);
514 3
            $this->setAttribute('old_secret_valid_until', null);
515
516 3
            if ($oldSecretValidUntil) {
517 1
                $oldSecretData = $this->getAttribute('secret') ?? null;
518 1
                if ($oldSecretData) {
519
                    // Ensure correct encryption key.
520 1
                    $oldSecretData = $cryptographer->encryp($cryptographer->decrypt($oldSecretData), $keyName);
521 1
                    $this->setAttribute('old_secret', $oldSecretData);
522
523 1
                    if ($oldSecretValidUntil instanceof \DateInterval) {
524 1
                        $oldSecretValidUntil = (new \DateTimeImmutable())->add($oldSecretValidUntil);
525
                    }
526 1
                    $this->setAttribute('old_secret_valid_until', $oldSecretValidUntil);
527
                }
528
            }
529
530 3
            $this->setAttribute('secret', $cryptographer->encryp($secret, $keyName));
531
        } else {
532 1
            if ($secret !== null) {
533 1
                throw new InvalidArgumentException(
534 1
                    'The secret for a non-confidential client can only be set to `null`.'
535 1
                );
536
            }
537
538 1
            $this->setAttribute('secret', null);
539
        }
540
541 3
        return $this;
542
    }
543
544
    /**
545
     * @inheritDoc
546
     */
547 1
    public function setSecretsAsEnvVars($secretEnvVarName, $oldSecretEnvVarName = null, $oldSecretValidUntil = null)
548
    {
549 1
        if (empty($secretEnvVarName)) {
550
            throw new InvalidArgumentException(
551
                'Parameter $secretEnvVarName can not be empty.'
552
            );
553
        }
554
555 1
        if ($oldSecretEnvVarName) {
556 1
            if (!$oldSecretValidUntil) {
557
                throw new InvalidArgumentException(
558
                    'Parameter $oldSecretValidUntil must be set when $oldSecretEnvVar is set.'
559
                );
560
            }
561 1
            $this->setAttribute('old_secret', '${' . $oldSecretEnvVarName . '}');
562
563 1
            if ($oldSecretValidUntil instanceof \DateInterval) {
564 1
                $oldSecretValidUntil = (new \DateTimeImmutable())->add($oldSecretValidUntil);
565
            }
566 1
            $this->setAttribute('old_secret_valid_until', $oldSecretValidUntil);
567
        } else {
568
            $this->setAttribute('old_secret', null);
569
            $this->setAttribute('old_secret_valid_until', null);
570
        }
571
572 1
        $this->setAttribute('secret', '${' . $secretEnvVarName . '}');
573
574 1
        return $this;
575
    }
576
577
    /**
578
     * @inheritDoc
579
     */
580 4
    public function validateNewSecret($secret, &$error)
581
    {
582 4
        $error = null;
583 4
        if (mb_strlen($secret) < $this->getMinimumSecretLength()) {
584 1
            $error = 'Secret should be at least ' . $this->getMinimumSecretLength() . ' characters.';
585
        }
586
587 4
        return $error === null;
588
    }
589
590
    /**
591
     * @inheritDoc
592
     */
593 5
    public function getMinimumSecretLength()
594
    {
595 5
        return $this->minimumSecretLength;
596
    }
597
598
    /**
599
     * @inheritDoc
600
     */
601 1
    public function setMinimumSecretLength($minimumSecretLength)
602
    {
603 1
        if (!(int)$minimumSecretLength) {
604
            throw new InvalidArgumentException('$minimumSecretLength can not be empty.');
605
        }
606 1
        $this->minimumSecretLength = (int)$minimumSecretLength;
607 1
        return $this;
608
    }
609
610
    /**
611
     * @inheritDoc
612
     */
613 3
    public function getDecryptedSecret($cryptographer)
614
    {
615 3
        return $cryptographer->decrypt($this->envVarParseSecret($this->secret));
616
    }
617
618
    /**
619
     * @inheritDoc
620
     */
621 2
    public function getDecryptedOldSecret($cryptographer)
622
    {
623 2
        return $cryptographer->decrypt($this->envVarParseSecret($this->old_secret));
624
    }
625
626
    /**
627
     * Replaces environment variables with their values in the secret
628
     *
629
     * @param string $secret
630
     * @return string
631
     * @throws EnvironmentVariableNotAllowedException
632
     * @throws EnvironmentVariableNotSetException
633
     * @throws InvalidConfigException
634
     */
635 3
    protected function envVarParseSecret($secret)
636
    {
637 3
        $secretsEnvVarConfig = $this->getSecretsEnvVarConfig();
638 3
        if ($secretsEnvVarConfig) {
639 1
            if (version_compare(PHP_VERSION, '8.1.0', '<')) {
640
                // PHP < 8.1 can only handle indexed array when unpacking.
641 1
                $secretsEnvVarConfig = array_values($secretsEnvVarConfig);
642
            }
643 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

643
            $secret = EnvironmentHelper::parseEnvVars($secret, /** @scrutinizer ignore-type */ ...$secretsEnvVarConfig);
Loading history...
644 2
        } elseif (preg_match(EnvironmentHelper::ENV_VAR_REGEX, $secret)) {
645
            throw new InvalidConfigException('Environment variable used without env_var_config being set.');
646
        }
647
648 3
        return $secret;
649
    }
650
651
    /**
652
     * @inheritDoc
653
     */
654
    public function getOldSecretValidUntil()
655
    {
656
        return $this->old_secret_valid_until;
657
    }
658
659
    /**
660
     * @inheritdoc
661
     */
662 2
    public function validateSecret($secret, $cryptographer)
663
    {
664 2
        return is_string($secret)
665 2
            && strlen($secret)
666 2
            && (
667 2
                Yii::$app->security->compareString($this->getDecryptedSecret($cryptographer), $secret)
668 2
                || (
669 2
                    !empty($this->old_secret)
670 2
                    && !empty($this->old_secret_valid_until)
671 2
                    && $this->old_secret_valid_until > (new \DateTime())
672 2
                    && Yii::$app->security->compareString($this->getDecryptedOldSecret($cryptographer), $secret)
673 2
                )
674 2
            );
675
    }
676
677
    /**
678
     * @inheritdoc
679
     */
680 1
    public function getLogoUri()
681
    {
682 1
        return $this->logo_uri;
683
    }
684
685
    /**
686
     * @inheritdoc
687
     */
688 1
    public function setLogoUri($logoUri)
689
    {
690 1
        $this->logo_uri = $logoUri;
691 1
        return $this;
692
    }
693
694
    /**
695
     * @inheritdoc
696
     */
697 1
    public function getTermsOfServiceUri()
698
    {
699 1
        return $this->tos_uri;
700
    }
701
702
    /**
703
     * @inheritdoc
704
     */
705 1
    public function setTermsOfServiceUri($tosUri)
706
    {
707 1
        $this->tos_uri = $tosUri;
708 1
        return $this;
709
    }
710
711
    /**
712
     * @inheritdoc
713
     */
714 1
    public function getContacts()
715
    {
716 1
        return $this->contacts;
717
    }
718
719
    /**
720
     * @inheritdoc
721
     */
722 1
    public function setContacts($contacts)
723
    {
724 1
        $this->contacts = $contacts;
725 1
        return $this;
726
    }
727
728
    /**
729
     * @inheritDoc
730
     */
731 1
    public function getGrantTypes()
732
    {
733 1
        return (int)$this->grant_types;
734
    }
735
736
    /**
737
     * @inheritDoc
738
     */
739 2
    public function setGrantTypes($grantTypes)
740
    {
741 2
        $grantTypeIds = array_flip(Oauth2Module::GRANT_TYPE_MAPPING);
742 2
        for ($i = (int)log(PHP_INT_MAX, 2); $i >= 0; $i--) {
743 2
            $grantTypeId = (int)pow(2, $i);
744 2
            if ($grantTypes & $grantTypeId) {
745 2
                if (!array_key_exists($grantTypeId, $grantTypeIds)) {
746 1
                    throw new InvalidArgumentException('Unknown Grant Type ID: ' . $grantTypeId);
747
                }
748
            }
749
        }
750
751 1
        $this->grant_types = $grantTypes;
752
753 1
        return $this;
754
    }
755
756
    /**
757
     * @inheritDoc
758
     */
759 2
    public function validateGrantType($grantTypeIdentifier)
760
    {
761 2
        $grantTypeId = Oauth2Module::getGrantTypeId($grantTypeIdentifier);
762 2
        if (empty($grantTypeId)) {
763 1
            throw new InvalidArgumentException('Unknown grant type "' . $grantTypeIdentifier . '".');
764
        }
765
766 1
        return (bool)($this->getGrantTypes() & $grantTypeId);
767
    }
768
769
    /**
770
     * @inheritDoc
771
     */
772 14
    public function validateAuthRequestScopes($scopeIdentifiers, &$unauthorizedScopes = [])
773
    {
774
        if (
775 14
            empty($scopeIdentifiers)
776
            // Quiet mode will always allow the request (scopes will silently be limited to the defined ones).
777 14
            || $this->getScopeAccess() === static::SCOPE_ACCESS_STRICT_QUIET
778
        ) {
779 4
            $unauthorizedScopes = [];
780 4
            return true;
781
        }
782
783 10
        $allowedScopeIdentifiers = array_map(
784 10
            fn($scope) => $scope->getIdentifier(),
785 10
            $this->getAllowedScopes($scopeIdentifiers)
786 10
        );
787
788 9
        $unauthorizedScopes = array_values(array_diff($scopeIdentifiers, $allowedScopeIdentifiers));
789
790 9
        return empty($unauthorizedScopes);
791
    }
792
793
    /**
794
     * @inheritDoc
795
     * @throws InvalidConfigException
796
     */
797 15
    public function getAllowedScopes($requestedScopeIdentifiers = [])
798
    {
799
        /** @var Oauth2ClientScopeInterface $clientScopeClass */
800 15
        $clientScopeClass = DiHelper::getValidatedClassName(Oauth2ClientScopeInterface::class);
801 15
        $clientScopeTableName = $clientScopeClass::tableName();
802
        /** @var Oauth2ScopeInterface $scopeClass */
803 15
        $scopeClass = DiHelper::getValidatedClassName(Oauth2ScopeInterface::class);
804 15
        $scopeTableName = $scopeClass::tableName();
805
806 15
        $possibleScopesConditions = [
807
            // Default scopes defined for this client.
808 15
            ['AND',
809 15
                [$clientScopeTableName . '.client_id' => $this->getPrimaryKey()],
810 15
                [$clientScopeTableName . '.enabled' => 1],
811 15
                ['OR',
812 15
                    ...(
813 15
                        !empty($requestedScopeIdentifiers)
814 12
                            ? [[$scopeTableName . '.identifier' => $requestedScopeIdentifiers]]
815
                            : []
816 15
                    ),
817 15
                    ['NOT', [
818 15
                        $clientScopeTableName . '.applied_by_default' => Oauth2ScopeInterface::APPLIED_BY_DEFAULT_NO
819 15
                    ]],
820 15
                    ['AND',
821 15
                        [$clientScopeTableName . '.applied_by_default' => null],
822 15
                        ['NOT', [
823 15
                            $scopeTableName . '.applied_by_default' => Oauth2ScopeInterface::APPLIED_BY_DEFAULT_NO
824 15
                        ]],
825 15
                    ],
826 15
                ],
827 15
            ],
828 15
        ];
829
830 15
        $scopeAccess = $this->getScopeAccess();
831 15
        if ($scopeAccess === Oauth2Client::SCOPE_ACCESS_PERMISSIVE) {
832
            // Default scopes defined by scope for all client.
833 4
            $possibleScopesConditions[] = ['AND',
834 4
                [$clientScopeTableName . '.client_id' => null],
835 4
                ['OR',
836 4
                    ...(
837 4
                        !empty($requestedScopeIdentifiers)
838 3
                            ? [[$scopeTableName . '.identifier' => $requestedScopeIdentifiers]]
839
                            : []
840 4
                    ),
841 4
                    ['NOT', [$scopeTableName . '.applied_by_default' => Oauth2ScopeInterface::APPLIED_BY_DEFAULT_NO]],
842 4
                ],
843 4
            ];
844
        } elseif (
845 11
            ($scopeAccess !== Oauth2Client::SCOPE_ACCESS_STRICT)
846 11
            && ($scopeAccess !== Oauth2Client::SCOPE_ACCESS_STRICT_QUIET)
847
        ) {
848
            // safeguard against unknown types.
849 1
            throw new \LogicException('Unknown scope_access: "' . $scopeAccess . '".');
850
        }
851
852 14
        return $scopeClass::find()
853 14
            ->joinWith('clientScopes', true)
854 14
            ->enabled()
855 14
            ->andWhere(['OR', ...$possibleScopesConditions])
856 14
            ->orderBy('id')
857 14
            ->all();
858
    }
859
860
    /**
861
     * @inheritdoc
862
     * @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...
863
     *     'unaffected': Oauth2ClientScopeInterface[],
864
     *     'new': Oauth2ClientScopeInterface[],
865
     *     'updated': Oauth2ClientScopeInterface[],
866
     *     'deleted': Oauth2ClientScopeInterface[],
867
     * }
868
     */
869 11
    public function syncClientScopes($scopes, $scopeRepository)
870
    {
871 11
        if (is_string($scopes)) {
872 2
            $scopes = explode(' ', $scopes);
873 9
        } elseif ($scopes === null) {
874 1
            $scopes = [];
875 8
        } elseif (!is_array($scopes)) {
876 1
            throw new InvalidArgumentException('$scopes must be a string, an array or null.');
877
        }
878
879
        /** @var class-string<Oauth2ClientScopeInterface> $clientScopeClass */
880 10
        $clientScopeClass = DiHelper::getValidatedClassName(Oauth2ClientScopeInterface::class);
881
882
        /** @var Oauth2ClientScopeInterface[] $origClientScopes */
883 10
        $origClientScopes = $clientScopeClass::findAll([
884 10
            'client_id' => $this->getPrimaryKey(),
885 10
        ]);
886
887 10
        $origClientScopes = array_combine(
888 10
            array_map(
889 10
                fn(Oauth2ClientScopeInterface $clientScope) => implode('-', $clientScope->getPrimaryKey(true)),
890 10
                $origClientScopes
891 10
            ),
892 10
            $origClientScopes
893 10
        );
894
895
        /** @var Oauth2ClientScopeInterface[] $clientScopes */
896 10
        $clientScopes = [];
897
898 10
        foreach ($scopes as $key => $value) {
899 9
            if ($value instanceof Oauth2ClientScopeInterface) {
900 2
                $clientScope = $value;
901 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...
902 2
                $pkIndex = implode('-', $clientScope->getPrimaryKey(true));
903 2
                if (array_key_exists($pkIndex, $origClientScopes)) {
904
                    // Overwrite orig (might still be considered "unchanged" when new ClientScope is not "dirty").
905 2
                    $origClientScopes[$pkIndex] = $clientScope;
906
                }
907
            } else {
908
909 7
                $scopeIdentifier = null;
910 7
                $clientScopeConfig = [
911 7
                    'client_id' => $this->getPrimaryKey(),
912 7
                ];
913
914 7
                if (is_string($value)) {
915 3
                    $scopeIdentifier = $value;
916 4
                } elseif ($value instanceof Oauth2ScopeInterface) {
917 2
                    $scopePk = $value->getPrimaryKey();
918 2
                    if ($scopePk) {
919 1
                        $clientScopeConfig = ArrayHelper::merge(
920 1
                            $clientScopeConfig,
921 1
                            ['scope_id' => $scopePk]
922 1
                        );
923
                    } else {
924
                        // New model, using identifier.
925 2
                        $scopeIdentifier = $value->getIdentifier();
926
                    }
927 2
                } elseif (is_array($value)) {
928 1
                    $clientScopeConfig = ArrayHelper::merge(
929 1
                        $clientScopeConfig,
930 1
                        $value,
931 1
                    );
932 1
                    if (empty($clientScopeConfig['scope_id'])) {
933 1
                        $scopeIdentifier = $key;
934
                    }
935
                } else {
936 1
                    throw new InvalidArgumentException(
937 1
                        'If $scopes is an array, its values must be a string, array or an instance of '
938 1
                        . Oauth2ClientScopeInterface::class . ' or ' . Oauth2ScopeInterface::class . '.'
939 1
                    );
940
                }
941
942 6
                if (isset($scopeIdentifier)) {
943 4
                    $scope = $scopeRepository->getScopeEntityByIdentifier($scopeIdentifier);
944 4
                    if (empty($scope)) {
945 1
                        throw new InvalidArgumentException('No scope with identifier "'
946 1
                            . $scopeIdentifier . '" found.');
947
                    }
948 3
                    if (!($scope instanceof Oauth2ScopeInterface)) {
949
                        throw new InvalidConfigException(get_class($scope) . ' must implement ' . Oauth2ScopeInterface::class);
950
                    }
951 3
                    $clientScopeConfig['scope_id'] = $scope->getPrimaryKey();
952
                } else {
953 3
                    if (empty($clientScopeConfig['scope_id'])) {
954 1
                        throw new InvalidArgumentException('Element ' . $key
955 1
                            . ' in $scope should specify either the scope id or its identifier.');
956
                    }
957
                }
958
959 4
                $pkIndex = $clientScopeConfig['client_id'] . '-' . $clientScopeConfig['scope_id'];
960 4
                if (array_key_exists($pkIndex, $origClientScopes)) {
961 4
                    $clientScope = $origClientScopes[$pkIndex];
962 4
                    $clientScope->setAttributes($clientScopeConfig, false);
963
                } else {
964
                    /** @var Oauth2ClientScopeInterface $clientScope */
965 4
                    $clientScope = Yii::createObject(ArrayHelper::merge(
966 4
                        ['class' => $clientScopeClass],
967 4
                        $clientScopeConfig
968 4
                    ));
969
                }
970
            }
971
972 6
            $pkIndex = implode('-', $clientScope->getPrimaryKey(true));
973 6
            $clientScopes[$pkIndex] = $clientScope;
974
        }
975
976 7
        $transaction = static::getDb()->beginTransaction();
977
        try {
978
            // Delete records no longer present in the provided data.
979
            /** @var self[]|array[] $deleteClientScopes */
980 7
            $deleteClientScopes = array_diff_key($origClientScopes, $clientScopes);
981 7
            foreach ($deleteClientScopes as $deleteClientScope) {
982 6
                $deleteClientScope->delete();
983
            }
984
985
            // Create records not present in the provided data.
986 7
            $createClientScopes = array_diff_key($clientScopes, $origClientScopes);
987 7
            foreach ($createClientScopes as $createClientScope) {
988 6
                $createClientScope->persist();
989
            }
990
991
            // Update existing records if needed.
992 6
            $unaffectedClientScopes = [];
993 6
            $updatedClientScopes = [];
994 6
            foreach (array_intersect_key($origClientScopes, $clientScopes) as $key => $existingClientScope) {
995 5
                if ($existingClientScope->getDirtyAttributes()) {
996 2
                    $existingClientScope->persist();
997 2
                    $updatedClientScopes[$key] = $existingClientScope;
998
                } else {
999 5
                    $unaffectedClientScopes[$key] = $existingClientScope;
1000
                }
1001
            }
1002
1003 6
            $transaction->commit();
1004 1
        } catch (\Exception $e) {
1005 1
            $transaction->rollBack();
1006 1
            throw $e;
1007
        }
1008
1009 6
        return [
1010 6
            'unaffected' => $unaffectedClientScopes,
1011 6
            'new' => $createClientScopes,
1012 6
            'updated' => $updatedClientScopes,
1013 6
            'deleted' => $deleteClientScopes,
1014 6
        ];
1015
    }
1016
}
1017