Passed
Push — master ( b8bdbf...0c1875 )
by Rutger
15:13
created

Oauth2Client::endUsersMayAuthorizeClient()   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\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
        $uris = $this->redirect_uris;
151 15
        if (empty($uris)) {
152 1
            return [];
153
        }
154
155
        // Compatibility with DBMSs that don't support JSON data type.
156 14
        if (is_string($uris)) {
0 ignored issues
show
introduced by
The condition is_string($uris) is always false.
Loading history...
157
            try {
158 11
                $uris = Json::decode($uris);
159 1
            } catch (InvalidArgumentException $e) {
160 1
                throw new InvalidConfigException('Invalid json in redirect_uris for client ' . $this->id, 0, $e);
161
            }
162
        }
163
164 13
        $redirectUrisEnvVarConfig = $this->getRedirectUrisEnvVarConfig();
165 13
        if ($redirectUrisEnvVarConfig && version_compare(PHP_VERSION, '8.1.0', '<')) {
166
            // PHP < 8.1 can only handle indexed array when unpacking.
167 6
            $redirectUrisEnvVarConfig = array_values($redirectUrisEnvVarConfig);
168
        }
169
170 13
        if (is_string($uris)) {
0 ignored issues
show
introduced by
The condition is_string($uris) is always false.
Loading history...
171 4
            if ($redirectUrisEnvVarConfig && preg_match('/^\${[a-zA-Z0-9_]+}$/', $uris)) {
172 3
                $uris = EnvironmentHelper::parseEnvVars($uris, ...$redirectUrisEnvVarConfig);
173
                try {
174
                    // Try to parse the content of the environment variable(s) as JSON.
175 3
                    $uris = Json::decode($uris);
176 1
                } catch (InvalidArgumentException $e) {
177
                    // Use as plain text if it failed.
178
                }
179
            }
180
        }
181
182 13
        if (is_string($uris)) {
0 ignored issues
show
introduced by
The condition is_string($uris) is always false.
Loading history...
183 3
            $uris = [$uris];
184 10
        } elseif (is_array($uris)) {
0 ignored issues
show
introduced by
The condition is_array($uris) is always true.
Loading history...
185 9
            $uris = array_values($uris);
186
        } else {
187 1
            throw new InvalidConfigException('`redirect_uris` must be a JSON encoded string or array of strings.');
188
        }
189
190 12
        foreach ($uris as $key => $uri) {
191 12
            if (!is_string($uri)) {
192 1
                throw new InvalidConfigException('`redirect_uris` must be a JSON encoded string or array of strings.');
193
            }
194 11
            if ($redirectUrisEnvVarConfig) {
195 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

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

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