Identity   F
last analyzed

Complexity

Total Complexity 168

Size/Duplication

Total Lines 1491
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 670
dl 0
loc 1491
rs 1.93
c 1
b 0
f 0
wmc 168

88 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 2 1
A create() 0 12 1
A changeEmail() 0 10 2
A rename() 0 10 2
A applyPhonePossessionProvenAndVerifiedEvent() 0 12 1
A assertSelfAssertedTokenRegistrationAllowed() 0 9 3
A retractRegistrationAuthority() 0 18 2
A applyIdentityCreatedEvent() 0 15 1
A applyGssfPossessionProvenEvent() 0 12 1
A applyIdentityAccreditedAsRaEvent() 0 9 1
A complyWithVettingOfSecondFactor() 0 30 5
B revokeSecondFactor() 0 31 7
A applyIdentityAccreditedAsRaForInstitutionEvent() 0 9 1
A getId() 0 3 1
A getPreferredLocale() 0 3 1
A restore() 0 9 2
A applyRegistrationAuthorityInformationAmendedForInstitutionEvent() 0 6 1
A applySafeStoreSecretRecoveryTokenPossessionPromisedEvent() 0 6 1
A registerSelfAssertedSecondFactor() 0 33 5
A provePossessionOfPhone() 0 40 2
A assertAllVettedTokensAreSelfAsserted() 0 7 3
A applyIdentityRenamedEvent() 0 3 1
A applyPhoneRecoveryTokenPossessionProvenEvent() 0 5 1
A getInstitution() 0 3 1
A verifyEmail() 0 24 5
A applyIdentityAccreditedAsRaaForInstitutionEvent() 0 9 1
A provePossessionOfPhoneRecoveryToken() 0 15 1
B selfVetSecondFactor() 0 51 8
A applyGssfPossessionProvenAndVerifiedEvent() 0 12 1
A applyCompliedWithVettedSecondFactorRevocationEvent() 0 4 1
A provePossessionOfGssf() 0 43 2
A applyIdentityEmailChangedEvent() 0 3 1
A applyRegistrationAuthorityRetractedEvent() 0 3 1
A getChildEntities() 0 7 1
A applyEmailVerifiedEvent() 0 10 1
A applyUnverifiedSecondFactorRevokedEvent() 0 3 1
A applyYubikeyPossessionProvenAndVerifiedEvent() 0 12 1
A forget() 0 7 2
A appointAs() 0 31 5
A applySecondFactorVettedWithoutTokenProofOfPossession() 0 11 1
A allVettedSecondFactorsRemoved() 0 6 1
A amendRegistrationAuthorityInformation() 0 21 2
A applyVettedSecondFactorRevokedEvent() 0 3 1
A getVerifiedSecondFactor() 0 3 1
A getEmail() 0 3 1
A applyVerifiedSecondFactorRevokedEvent() 0 3 1
A applyIdentityAccreditedAsRaaEvent() 0 9 1
B assertTokenNotAlreadyRegistered() 0 15 7
B accreditWith() 0 51 6
A applyCompliedWithRecoveryCodeRevocationEvent() 0 3 1
A applySecondFactorMigratedEvent() 0 11 1
A migrateVettedSecondFactor() 0 44 3
A getAggregateRootId() 0 3 1
A applyAppointedAsRaForInstitutionEvent() 0 4 1
A provePossessionOfYubikey() 0 40 2
A applyU2fDevicePossessionProvenEvent() 0 12 1
A applyRecoveryTokenRevokedEvent() 0 3 1
A applyAppointedAsRaaEvent() 0 4 1
B vetSecondFactor() 0 71 7
A bootstrapYubikeySecondFactor() 0 19 1
A complyWithRecoveryTokenRevocation() 0 9 2
A getVettedSecondFactorById() 0 3 1
B assertTokenTypeNotAlreadyRegistered() 0 15 7
A applyAppointedAsRaaForInstitutionEvent() 0 4 1
A applyIdentityRestoredEvent() 0 11 1
A applyYubikeyPossessionProvenEvent() 0 12 1
A applyAppointedAsRaEvent() 0 4 1
A applyRegistrationAuthorityRetractedForInstitutionEvent() 0 4 1
A applyIdentityForgottenEvent() 0 5 1
A revokeRecoveryToken() 0 9 2
A applyCompliedWithVerifiedSecondFactorRevocationEvent() 0 4 1
A assertUserMayAddRecoveryToken() 0 6 2
A saveVettingTypeHints() 0 9 1
A expressPreferredLocale() 0 9 2
A applySecondFactorVettedEvent() 0 7 1
A assertUserMayAddSecondFactor() 0 7 2
A applyRegistrationAuthorityInformationAmendedEvent() 0 6 1
A applyU2fDevicePossessionProvenAndVerifiedEvent() 0 12 1
A applyCompliedWithUnverifiedSecondFactorRevocationEvent() 0 4 1
B complyWithSecondFactorRevocation() 0 31 7
A getCommonName() 0 3 1
A assertNotForgotten() 0 4 2
A promisePossessionOfSafeStoreSecretRecoveryToken() 0 13 1
A applyLocalePreferenceExpressedEvent() 0 3 1
A getNameId() 0 3 1
A applyPhonePossessionProvenEvent() 0 12 1
A provePossessionOfU2fDevice() 0 40 2
A applyYubikeySecondFactorBootstrappedEvent() 0 11 1

How to fix   Complexity   

Complex Class

Complex classes like Identity often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Identity, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * Copyright 2014 SURFnet bv
5
 *
6
 * Licensed under the Apache License, Version 2.0 (the "License");
7
 * you may not use this file except in compliance with the License.
8
 * You may obtain a copy of the License at
9
 *
10
 *     http://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing, software
13
 * distributed under the License is distributed on an "AS IS" BASIS,
14
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
 * See the License for the specific language governing permissions and
16
 * limitations under the License.
17
 */
0 ignored issues
show
Coding Style introduced by
Missing @link tag in file comment
Loading history...
18
19
namespace Surfnet\Stepup\Identity;
20
21
use Broadway\EventSourcing\EventSourcedAggregateRoot;
22
use Surfnet\Stepup\Configuration\InstitutionConfiguration;
23
use Surfnet\Stepup\Configuration\Value\Institution as ConfigurationInstitution;
24
use Surfnet\Stepup\DateTime\DateTime;
25
use Surfnet\Stepup\Exception\DomainException;
26
use Surfnet\Stepup\Helper\SecondFactorProvePossessionHelper;
27
use Surfnet\Stepup\Identity\Api\Identity as IdentityApi;
28
use Surfnet\Stepup\Identity\Collection\VettingTypeHintCollection;
29
use Surfnet\Stepup\Identity\Entity\RecoveryToken as RecoveryTokenEntity;
30
use Surfnet\Stepup\Identity\Entity\RecoveryTokenCollection;
31
use Surfnet\Stepup\Identity\Entity\RegistrationAuthority;
32
use Surfnet\Stepup\Identity\Entity\RegistrationAuthorityCollection;
33
use Surfnet\Stepup\Identity\Entity\SecondFactorCollection;
34
use Surfnet\Stepup\Identity\Entity\UnverifiedSecondFactor;
35
use Surfnet\Stepup\Identity\Entity\VerifiedSecondFactor;
36
use Surfnet\Stepup\Identity\Entity\VettedSecondFactor;
37
use Surfnet\Stepup\Identity\Event\AppointedAsRaaEvent;
38
use Surfnet\Stepup\Identity\Event\AppointedAsRaaForInstitutionEvent;
39
use Surfnet\Stepup\Identity\Event\AppointedAsRaEvent;
40
use Surfnet\Stepup\Identity\Event\AppointedAsRaForInstitutionEvent;
41
use Surfnet\Stepup\Identity\Event\CompliedWithRecoveryCodeRevocationEvent;
42
use Surfnet\Stepup\Identity\Event\CompliedWithUnverifiedSecondFactorRevocationEvent;
43
use Surfnet\Stepup\Identity\Event\CompliedWithVerifiedSecondFactorRevocationEvent;
44
use Surfnet\Stepup\Identity\Event\CompliedWithVettedSecondFactorRevocationEvent;
45
use Surfnet\Stepup\Identity\Event\EmailVerifiedEvent;
46
use Surfnet\Stepup\Identity\Event\GssfPossessionProvenAndVerifiedEvent;
47
use Surfnet\Stepup\Identity\Event\GssfPossessionProvenEvent;
48
use Surfnet\Stepup\Identity\Event\IdentityAccreditedAsRaaEvent;
49
use Surfnet\Stepup\Identity\Event\IdentityAccreditedAsRaaForInstitutionEvent;
50
use Surfnet\Stepup\Identity\Event\IdentityAccreditedAsRaEvent;
51
use Surfnet\Stepup\Identity\Event\IdentityAccreditedAsRaForInstitutionEvent;
52
use Surfnet\Stepup\Identity\Event\IdentityCreatedEvent;
53
use Surfnet\Stepup\Identity\Event\IdentityEmailChangedEvent;
54
use Surfnet\Stepup\Identity\Event\IdentityForgottenEvent;
55
use Surfnet\Stepup\Identity\Event\IdentityRestoredEvent;
56
use Surfnet\Stepup\Identity\Event\IdentityRenamedEvent;
57
use Surfnet\Stepup\Identity\Event\LocalePreferenceExpressedEvent;
58
use Surfnet\Stepup\Identity\Event\PhonePossessionProvenAndVerifiedEvent;
59
use Surfnet\Stepup\Identity\Event\PhonePossessionProvenEvent;
60
use Surfnet\Stepup\Identity\Event\PhoneRecoveryTokenPossessionProvenEvent;
61
use Surfnet\Stepup\Identity\Event\RecoveryTokenRevokedEvent;
62
use Surfnet\Stepup\Identity\Event\RegistrationAuthorityInformationAmendedEvent;
63
use Surfnet\Stepup\Identity\Event\RegistrationAuthorityInformationAmendedForInstitutionEvent;
64
use Surfnet\Stepup\Identity\Event\RegistrationAuthorityRetractedEvent;
65
use Surfnet\Stepup\Identity\Event\RegistrationAuthorityRetractedForInstitutionEvent;
66
use Surfnet\Stepup\Identity\Event\SafeStoreSecretRecoveryTokenPossessionPromisedEvent;
67
use Surfnet\Stepup\Identity\Event\SecondFactorMigratedEvent;
68
use Surfnet\Stepup\Identity\Event\SecondFactorMigratedToEvent;
69
use Surfnet\Stepup\Identity\Event\SecondFactorVettedEvent;
70
use Surfnet\Stepup\Identity\Event\SecondFactorVettedWithoutTokenProofOfPossession;
71
use Surfnet\Stepup\Identity\Event\U2fDevicePossessionProvenAndVerifiedEvent;
72
use Surfnet\Stepup\Identity\Event\U2fDevicePossessionProvenEvent;
73
use Surfnet\Stepup\Identity\Event\UnverifiedSecondFactorRevokedEvent;
74
use Surfnet\Stepup\Identity\Event\VerifiedSecondFactorRevokedEvent;
75
use Surfnet\Stepup\Identity\Event\VettedSecondFactorRevokedEvent;
76
use Surfnet\Stepup\Identity\Event\VettedSecondFactorsAllRevokedEvent;
77
use Surfnet\Stepup\Identity\Event\VettingTypeHintsSavedEvent;
78
use Surfnet\Stepup\Identity\Event\YubikeyPossessionProvenAndVerifiedEvent;
79
use Surfnet\Stepup\Identity\Event\YubikeyPossessionProvenEvent;
80
use Surfnet\Stepup\Identity\Event\YubikeySecondFactorBootstrappedEvent;
81
use Surfnet\Stepup\Identity\Value\CommonName;
82
use Surfnet\Stepup\Identity\Value\ContactInformation;
83
use Surfnet\Stepup\Identity\Value\DocumentNumber;
84
use Surfnet\Stepup\Identity\Value\Email;
85
use Surfnet\Stepup\Identity\Value\EmailVerificationWindow;
0 ignored issues
show
Bug introduced by
The type Surfnet\Stepup\Identity\...EmailVerificationWindow was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
86
use Surfnet\Stepup\Identity\Value\GssfId;
87
use Surfnet\Stepup\Identity\Value\IdentityId;
88
use Surfnet\Stepup\Identity\Value\Institution;
89
use Surfnet\Stepup\Identity\Value\Locale;
0 ignored issues
show
Bug introduced by
The type Surfnet\Stepup\Identity\Value\Locale was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
90
use Surfnet\Stepup\Identity\Value\Location;
91
use Surfnet\Stepup\Identity\Value\NameId;
92
use Surfnet\Stepup\Identity\Value\OnPremiseVettingType;
93
use Surfnet\Stepup\Identity\Value\PhoneNumber;
94
use Surfnet\Stepup\Identity\Value\RecoveryTokenId;
95
use Surfnet\Stepup\Identity\Value\RecoveryTokenType;
96
use Surfnet\Stepup\Identity\Value\RegistrationAuthorityRole;
0 ignored issues
show
Bug introduced by
The type Surfnet\Stepup\Identity\...gistrationAuthorityRole was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
97
use Surfnet\Stepup\Identity\Value\SafeStore;
98
use Surfnet\Stepup\Identity\Value\SecondFactorId;
99
use Surfnet\Stepup\Identity\Value\SecondFactorIdentifier;
100
use Surfnet\Stepup\Identity\Value\SelfAssertedRegistrationVettingType;
101
use Surfnet\Stepup\Identity\Value\SelfVetVettingType;
102
use Surfnet\Stepup\Identity\Value\StepupProvider;
103
use Surfnet\Stepup\Identity\Value\U2fKeyHandle;
104
use Surfnet\Stepup\Identity\Value\UnknownVettingType;
105
use Surfnet\Stepup\Identity\Value\VettingType;
106
use Surfnet\Stepup\Identity\Value\YubikeyPublicId;
107
use Surfnet\Stepup\Token\TokenGenerator;
108
use Surfnet\StepupBundle\Security\OtpGenerator;
109
use Surfnet\StepupBundle\Service\SecondFactorTypeService;
110
use Surfnet\StepupBundle\Value\Loa;
111
use Surfnet\StepupBundle\Value\SecondFactorType;
112
113
/**
114
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
115
 * @SuppressWarnings(PHPMD.TooManyMethods)
116
 * @SuppressWarnings(PHPMD.TooManyPublicMethods)
117
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
118
 * @SuppressWarnings(PHPMD.ExcessiveClassLength)
119
 */
0 ignored issues
show
Coding Style introduced by
Missing @category tag in class comment
Loading history...
Coding Style introduced by
Missing @package tag in class comment
Loading history...
Coding Style introduced by
Missing @author tag in class comment
Loading history...
Coding Style introduced by
Missing @license tag in class comment
Loading history...
Coding Style introduced by
Missing @link tag in class comment
Loading history...
120
class Identity extends EventSourcedAggregateRoot implements IdentityApi
121
{
122
    /**
123
     * @var IdentityId
124
     */
125
    private IdentityId $id;
126
127
    /**
128
     * @var Institution
129
     */
130
    private Institution $institution;
131
132
    /**
133
     * @var NameId
134
     */
135
    private NameId $nameId;
136
137
    /**
138
     * @var CommonName
139
     */
140
    private CommonName $commonName;
141
142
    /**
143
     * @var Email
144
     */
145
    private Email $email;
146
147
    private ?SecondFactorCollection $unverifiedSecondFactors = null;
148
149
    private ?SecondFactorCollection $verifiedSecondFactors = null;
150
151
    private ?SecondFactorCollection $vettedSecondFactors = null;
152
153
    private ?RegistrationAuthorityCollection $registrationAuthorities = null;
154
155
    /**
156
     * @var Locale
157
     */
158
    private Locale $preferredLocale;
159
160
    private ?bool $forgotten = null;
161
162
    private ?RecoveryTokenCollection $recoveryTokens = null;
163
164
    public static function create(
165
        IdentityId $id,
166
        Institution $institution,
167
        NameId $nameId,
168
        CommonName $commonName,
169
        Email $email,
170
        Locale $preferredLocale,
171
    ): self {
172
        $identity = new self();
173
        $identity->apply(new IdentityCreatedEvent($id, $institution, $nameId, $commonName, $email, $preferredLocale));
174
175
        return $identity;
176
    }
177
178
    final public function __construct()
179
    {
180
    }
181
182
    public function rename(CommonName $commonName): void
183
    {
184
        $this->assertNotForgotten();
185
186
        if ($this->commonName->equals($commonName)) {
187
            return;
188
        }
189
190
        $this->commonName = $commonName;
191
        $this->apply(new IdentityRenamedEvent($this->id, $this->institution, $commonName));
192
    }
193
194
    public function changeEmail(Email $email): void
195
    {
196
        $this->assertNotForgotten();
197
198
        if ($this->email->equals($email)) {
199
            return;
200
        }
201
202
        $this->email = $email;
203
        $this->apply(new IdentityEmailChangedEvent($this->id, $this->institution, $email));
204
    }
205
206
    public function bootstrapYubikeySecondFactor(
207
        SecondFactorId  $secondFactorId,
208
        YubikeyPublicId $yubikeyPublicId,
209
        int             $maxNumberOfTokens,
210
    ): void {
211
        $this->assertNotForgotten();
212
        $this->assertUserMayAddSecondFactor($maxNumberOfTokens);
213
        $this->assertTokenTypeNotAlreadyRegistered(new SecondFactorType('yubikey'));
214
215
        $this->apply(
216
            new YubikeySecondFactorBootstrappedEvent(
217
                $this->id,
218
                $this->nameId,
219
                $this->institution,
220
                $this->commonName,
221
                $this->email,
222
                $this->preferredLocale,
223
                $secondFactorId,
224
                $yubikeyPublicId,
225
            ),
226
        );
227
    }
228
229
    public function provePossessionOfYubikey(
230
        SecondFactorId          $secondFactorId,
231
        YubikeyPublicId         $yubikeyPublicId,
232
        bool                    $emailVerificationRequired,
233
        EmailVerificationWindow $emailVerificationWindow,
234
        int                     $maxNumberOfTokens,
235
    ): void {
236
        $this->assertNotForgotten();
237
        $this->assertUserMayAddSecondFactor($maxNumberOfTokens);
238
        $this->assertTokenTypeNotAlreadyRegistered(new SecondFactorType('yubikey'));
239
240
        if ($emailVerificationRequired) {
241
            $emailVerificationNonce = TokenGenerator::generateNonce();
242
243
            $this->apply(
244
                new YubikeyPossessionProvenEvent(
245
                    $this->id,
246
                    $this->institution,
247
                    $secondFactorId,
248
                    $yubikeyPublicId,
249
                    $emailVerificationRequired,
250
                    $emailVerificationWindow,
251
                    $emailVerificationNonce,
252
                    $this->commonName,
253
                    $this->email,
254
                    $this->preferredLocale,
255
                ),
256
            );
257
        } else {
258
            $this->apply(
259
                new YubikeyPossessionProvenAndVerifiedEvent(
260
                    $this->id,
261
                    $this->institution,
262
                    $secondFactorId,
263
                    $yubikeyPublicId,
264
                    $this->commonName,
265
                    $this->email,
266
                    $this->preferredLocale,
267
                    DateTime::now(),
268
                    OtpGenerator::generate(8),
269
                ),
270
            );
271
        }
272
    }
273
274
    public function provePossessionOfPhone(
275
        SecondFactorId          $secondFactorId,
276
        PhoneNumber             $phoneNumber,
277
        bool                    $emailVerificationRequired,
278
        EmailVerificationWindow $emailVerificationWindow,
279
        int                     $maxNumberOfTokens,
280
    ): void {
281
        $this->assertNotForgotten();
282
        $this->assertUserMayAddSecondFactor($maxNumberOfTokens);
283
        $this->assertTokenTypeNotAlreadyRegistered(new SecondFactorType('sms'));
284
285
        if ($emailVerificationRequired) {
286
            $emailVerificationNonce = TokenGenerator::generateNonce();
287
288
            $this->apply(
289
                new PhonePossessionProvenEvent(
290
                    $this->id,
291
                    $this->institution,
292
                    $secondFactorId,
293
                    $phoneNumber,
294
                    $emailVerificationRequired,
295
                    $emailVerificationWindow,
296
                    $emailVerificationNonce,
297
                    $this->commonName,
298
                    $this->email,
299
                    $this->preferredLocale,
300
                ),
301
            );
302
        } else {
303
            $this->apply(
304
                new PhonePossessionProvenAndVerifiedEvent(
305
                    $this->id,
306
                    $this->institution,
307
                    $secondFactorId,
308
                    $phoneNumber,
309
                    $this->commonName,
310
                    $this->email,
311
                    $this->preferredLocale,
312
                    DateTime::now(),
313
                    OtpGenerator::generate(8),
314
                ),
315
            );
316
        }
317
    }
318
319
    public function provePossessionOfPhoneRecoveryToken(
320
        RecoveryTokenId $recoveryTokenId,
321
        PhoneNumber $phoneNumber,
322
    ): void {
323
        $this->assertNotForgotten();
324
        $this->assertUserMayAddRecoveryToken(RecoveryTokenType::sms());
325
        $this->apply(
326
            new PhoneRecoveryTokenPossessionProvenEvent(
327
                $this->id,
328
                $this->institution,
329
                $recoveryTokenId,
330
                $phoneNumber,
331
                $this->commonName,
332
                $this->email,
333
                $this->preferredLocale,
334
            ),
335
        );
336
    }
337
338
339
    public function promisePossessionOfSafeStoreSecretRecoveryToken(RecoveryTokenId $tokenId, SafeStore $secret): void
340
    {
341
        $this->assertNotForgotten();
342
        $this->assertUserMayAddRecoveryToken(RecoveryTokenType::safeStore());
343
        $this->apply(
344
            new SafeStoreSecretRecoveryTokenPossessionPromisedEvent(
345
                $this->id,
346
                $this->institution,
347
                $tokenId,
348
                $secret,
349
                $this->commonName,
350
                $this->email,
351
                $this->preferredLocale,
352
            ),
353
        );
354
    }
355
356
    public function saveVettingTypeHints(Institution $institution, VettingTypeHintCollection $hints): void
357
    {
358
        $this->assertNotForgotten();
359
        $this->apply(
360
            new VettingTypeHintsSavedEvent(
361
                $this->id,
362
                $this->institution,
363
                $hints,
364
                $institution,
365
            ),
366
        );
367
    }
368
369
    public function provePossessionOfGssf(
370
        SecondFactorId          $secondFactorId,
371
        StepupProvider          $provider,
372
        GssfId                  $gssfId,
373
        bool                    $emailVerificationRequired,
374
        EmailVerificationWindow $emailVerificationWindow,
375
        int                     $maxNumberOfTokens,
376
    ): void {
377
        $this->assertNotForgotten();
378
        $this->assertUserMayAddSecondFactor($maxNumberOfTokens);
379
        $this->assertTokenTypeNotAlreadyRegistered(new SecondFactorType($provider->getStepupProvider()));
380
381
        if ($emailVerificationRequired) {
382
            $emailVerificationNonce = TokenGenerator::generateNonce();
383
384
            $this->apply(
385
                new GssfPossessionProvenEvent(
386
                    $this->id,
387
                    $this->institution,
388
                    $secondFactorId,
389
                    $provider,
390
                    $gssfId,
391
                    $emailVerificationRequired,
392
                    $emailVerificationWindow,
393
                    $emailVerificationNonce,
394
                    $this->commonName,
395
                    $this->email,
396
                    $this->preferredLocale,
397
                ),
398
            );
399
        } else {
400
            $this->apply(
401
                new GssfPossessionProvenAndVerifiedEvent(
402
                    $this->id,
403
                    $this->institution,
404
                    $secondFactorId,
405
                    $provider,
406
                    $gssfId,
407
                    $this->commonName,
408
                    $this->email,
409
                    $this->preferredLocale,
410
                    DateTime::now(),
411
                    OtpGenerator::generate(8),
412
                ),
413
            );
414
        }
415
    }
416
417
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $secondFactorId should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $keyHandle should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $emailVerificationRequired should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $emailVerificationWindow should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $maxNumberOfTokens should have a doc-comment as per coding-style.
Loading history...
418
     * @deprecated Built in U2F support is dropped from StepUp, this was not removed to support event replay
419
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
420
    public function provePossessionOfU2fDevice(
421
        SecondFactorId          $secondFactorId,
422
        U2fKeyHandle            $keyHandle,
423
        bool                    $emailVerificationRequired,
424
        EmailVerificationWindow $emailVerificationWindow,
425
        int                     $maxNumberOfTokens,
426
    ): void {
427
        $this->assertNotForgotten();
428
        $this->assertUserMayAddSecondFactor($maxNumberOfTokens);
429
        $this->assertTokenTypeNotAlreadyRegistered(new SecondFactorType('u2f'));
430
431
        if ($emailVerificationRequired) {
432
            $emailVerificationNonce = TokenGenerator::generateNonce();
433
434
            $this->apply(
435
                new U2fDevicePossessionProvenEvent(
0 ignored issues
show
Deprecated Code introduced by
The class Surfnet\Stepup\Identity\...cePossessionProvenEvent has been deprecated: Built in U2F support is dropped from StepUp, this Event was not removed to support event replay ( Ignorable by Annotation )

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

435
                /** @scrutinizer ignore-deprecated */ new U2fDevicePossessionProvenEvent(
Loading history...
436
                    $this->id,
437
                    $this->institution,
438
                    $secondFactorId,
439
                    $keyHandle,
440
                    $emailVerificationRequired,
441
                    $emailVerificationWindow,
442
                    $emailVerificationNonce,
443
                    $this->commonName,
444
                    $this->email,
445
                    $this->preferredLocale,
446
                ),
447
            );
448
        } else {
449
            $this->apply(
450
                new U2fDevicePossessionProvenAndVerifiedEvent(
0 ignored issues
show
Deprecated Code introduced by
The class Surfnet\Stepup\Identity\...nProvenAndVerifiedEvent has been deprecated: Built in U2F support is dropped from StepUp, this Event was not removed to support event replay ( Ignorable by Annotation )

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

450
                /** @scrutinizer ignore-deprecated */ new U2fDevicePossessionProvenAndVerifiedEvent(
Loading history...
451
                    $this->id,
452
                    $this->institution,
453
                    $secondFactorId,
454
                    $keyHandle,
455
                    $this->commonName,
456
                    $this->email,
457
                    $this->preferredLocale,
458
                    DateTime::now(),
459
                    OtpGenerator::generate(8),
460
                ),
461
            );
462
        }
463
    }
464
465
    public function verifyEmail(string $verificationNonce): void
466
    {
467
        $this->assertNotForgotten();
468
469
        $secondFactorToVerify = null;
470
        foreach ($this->unverifiedSecondFactors as $secondFactor) {
471
            /** @var Entity\UnverifiedSecondFactor $secondFactor */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
472
            if ($secondFactor->hasNonce($verificationNonce)) {
473
                $secondFactorToVerify = $secondFactor;
474
            }
475
        }
476
477
        if (!$secondFactorToVerify) {
478
            throw new DomainException(
479
                'Cannot verify second factor, no unverified second factor can be verified using the given nonce',
480
            );
481
        }
482
483
        /** @var Entity\UnverifiedSecondFactor $secondFactorToVerify */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
484
        if (!$secondFactorToVerify->canBeVerifiedNow()) {
485
            throw new DomainException('Cannot verify second factor, the verification window is closed.');
486
        }
487
488
        $secondFactorToVerify->verifyEmail();
489
    }
490
491
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $registrant should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $registrantsSecondFactorId should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $registrantsSecondFactorType should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $registrantsSecondFactorIdentifier should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $registrationCode should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $documentNumber should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $identityVerified should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $secondFactorTypeService should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $secondFactorProvePossessionHelper should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $provePossessionSkipped should have a doc-comment as per coding-style.
Loading history...
492
     * @SuppressWarnings(PHPMD.ExcessiveParameterList)
493
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
494
    public function vetSecondFactor(
495
        IdentityApi                       $registrant,
496
        SecondFactorId                    $registrantsSecondFactorId,
497
        SecondFactorType                  $registrantsSecondFactorType,
498
        SecondFactorIdentifier            $registrantsSecondFactorIdentifier,
499
        string                            $registrationCode,
500
        DocumentNumber                    $documentNumber,
501
        bool                              $identityVerified,
502
        SecondFactorTypeService           $secondFactorTypeService,
503
        SecondFactorProvePossessionHelper $secondFactorProvePossessionHelper,
504
        bool                              $provePossessionSkipped,
505
    ): void {
506
        $this->assertNotForgotten();
507
508
        /** The vetted second factor collection can determine highest loa based on the vetting type,
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
Doc comment short description must be on the first line
Loading history...
509
         * the other can not (as the verified and unverified second factors do not have a vetting type)
510
         * And the vetting type is used to determine if the LoA is diminished (in case of a self
511
         * asserted token registration)
512
         */
513
        /** @var VettedSecondFactor|null $secondFactorWithHighestLoa */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
514
        $secondFactorWithHighestLoa = $this->vettedSecondFactors->getSecondFactorWithHighestLoa(
0 ignored issues
show
Bug introduced by
The method getSecondFactorWithHighestLoa() does not exist on null. ( Ignorable by Annotation )

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

514
        /** @scrutinizer ignore-call */ 
515
        $secondFactorWithHighestLoa = $this->vettedSecondFactors->getSecondFactorWithHighestLoa(

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
515
            $secondFactorTypeService,
516
        );
517
        $registrantsSecondFactor = $registrant->getVerifiedSecondFactor($registrantsSecondFactorId);
518
519
        if (!$registrantsSecondFactor instanceof \Surfnet\Stepup\Identity\Entity\VerifiedSecondFactor) {
520
            throw new DomainException(
521
                sprintf('Registrant second factor with ID %s does not exist', $registrantsSecondFactorId),
522
            );
523
        }
524
525
        if ($secondFactorWithHighestLoa === null) {
526
            throw new DomainException(
527
                sprintf(
528
                    'Vetting failed: authority %s has %d vetted second factors!',
529
                    $this->id,
530
                    count($this->vettedSecondFactors),
0 ignored issues
show
Bug introduced by
It seems like $this->vettedSecondFactors can also be of type null; however, parameter $value of count() does only seem to accept Countable|array, maybe add an additional type check? ( Ignorable by Annotation )

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

530
                    count(/** @scrutinizer ignore-type */ $this->vettedSecondFactors),
Loading history...
531
                ),
532
            );
533
        }
534
535
        if (!$secondFactorWithHighestLoa->hasEqualOrHigherLoaComparedTo(
536
            $registrantsSecondFactor,
537
            $secondFactorTypeService,
538
        )) {
0 ignored issues
show
Coding Style introduced by
Closing parenthesis of a multi-line IF statement must be on a new line
Loading history...
539
            throw new DomainException("Authority does not have the required LoA to vet the registrant's second factor");
540
        }
541
542
        if (!$identityVerified) {
543
            throw new DomainException('Will not vet second factor when physical identity has not been verified.');
544
        }
545
546
        if ($provePossessionSkipped && !$secondFactorProvePossessionHelper->canSkipProvePossession(
547
            $registrantsSecondFactorType,
548
        )) {
0 ignored issues
show
Coding Style introduced by
Closing parenthesis of a multi-line IF statement must be on a new line
Loading history...
549
            throw new DomainException(
550
                sprintf(
551
                    "The possession of registrants second factor with ID '%s' of type '%s' has to be physically proven",
552
                    $registrantsSecondFactorId,
553
                    $registrantsSecondFactorType->getSecondFactorType(),
554
                ),
555
            );
556
        }
557
558
        $registrant->complyWithVettingOfSecondFactor(
559
            $registrantsSecondFactorId,
560
            $registrantsSecondFactorType,
561
            $registrantsSecondFactorIdentifier,
562
            $registrationCode,
563
            $documentNumber,
564
            $provePossessionSkipped,
565
        );
566
    }
567
568
    public function registerSelfAssertedSecondFactor(
569
        SecondFactorIdentifier $secondFactorIdentifier,
570
        SecondFactorTypeService $secondFactorTypeService,
571
        RecoveryTokenId $recoveryTokenId,
572
    ): void {
573
        $this->assertNotForgotten();
574
        $this->assertSelfAssertedTokenRegistrationAllowed();
575
576
        try {
577
            $recoveryToken = $this->recoveryTokens->get($recoveryTokenId);
0 ignored issues
show
Bug introduced by
The method get() does not exist on null. ( Ignorable by Annotation )

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

577
            /** @scrutinizer ignore-call */ 
578
            $recoveryToken = $this->recoveryTokens->get($recoveryTokenId);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
578
        } catch (DomainException) {
579
            throw new DomainException(
580
                sprintf('Recovery token used during registration is not possessed by identity %s', (string)$this->id),
581
            );
582
        }
583
584
        $registeringSecondFactor = null;
585
        foreach ($this->verifiedSecondFactors as $secondFactor) {
586
            if ($secondFactorIdentifier->equals($secondFactor->getIdentifier())) {
587
                $registeringSecondFactor = $secondFactor;
588
            }
589
        }
590
591
        if ($registeringSecondFactor === null) {
0 ignored issues
show
introduced by
The condition $registeringSecondFactor === null is always true.
Loading history...
592
            throw new DomainException(
593
                sprintf(
594
                    'Registering second factor of type %s with ID %s does not exist',
595
                    $secondFactorIdentifier::class,
596
                    $secondFactorIdentifier->getValue(),
597
                ),
598
            );
599
        }
600
        $registeringSecondFactor->vet(true, new SelfAssertedRegistrationVettingType($recoveryToken->getTokenId()));
601
    }
602
603
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $authoringSecondFactorLoa should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $registrationCode should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $secondFactorIdentifier should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $secondFactorTypeService should have a doc-comment as per coding-style.
Loading history...
604
     * Two self-vet scenarios are dealt with
605
     *
606
     * 1. A regular self-vet action. Where an on premise token is used to vet another token
607
     *    from the comfort of the identity's SelfService application. In other words, self vetting
608
     *    allows the identity to activate a second/third/.. token without visiting the service desk
609
     *
610
     * 2. A variation on 1: but here a self-asserted token is used to activate the verified token.
611
     *    This new token will inherit the LoA of the self-asserted token. Effectively giving it a
612
     *    LoA 1.5 level.
613
     *
614
     * The code below uses the following terminology
615
     *
616
     *   RegisteringSecondFactor: This is the verified second factor that is to be activated
617
     *                            using the self-vet vetting type
618
     *   AuthoringSecondFactor:   The vetted token, used to activate (vet) the RegisteringSecondFactor
619
     *   IsSelfVetUsingSAT:       Is self-vetting using a self-asserted token allowed for this
620
     *                            self-vet scenario? All existing vetted tokens must be of the
621
     *                            self-asserted vetting type.
622
     *
623
     */
0 ignored issues
show
Coding Style introduced by
Additional blank lines found at end of doc comment
Loading history...
Coding Style introduced by
Missing @return tag in function comment
Loading history...
624
    public function selfVetSecondFactor(
625
        Loa $authoringSecondFactorLoa,
626
        string $registrationCode,
627
        SecondFactorIdentifier $secondFactorIdentifier,
628
        SecondFactorTypeService $secondFactorTypeService,
629
    ): void {
630
        $this->assertNotForgotten();
631
        $registeringSecondFactor = null;
632
        foreach ($this->verifiedSecondFactors as $secondFactor) {
633
            /** @var VerifiedSecondFactor $secondFactor */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
634
            if ($secondFactor->hasRegistrationCodeAndIdentifier($registrationCode, $secondFactorIdentifier)) {
635
                $registeringSecondFactor = $secondFactor;
636
            }
637
        }
638
639
        if ($registeringSecondFactor === null) {
640
            throw new DomainException(
641
                sprintf(
642
                    'Registrant second factor of type %s with ID %s does not exist',
643
                    $secondFactorIdentifier::class,
644
                    $secondFactorIdentifier->getValue(),
645
                ),
646
            );
647
        }
648
649
        if (!$registeringSecondFactor->hasRegistrationCodeAndIdentifier($registrationCode, $secondFactorIdentifier)) {
650
            throw new DomainException('The verified second factors registration code or identifier do not match.');
651
        }
652
653
        $selfVettingIsAllowed = $authoringSecondFactorLoa->levelIsHigherOrEqualTo(
654
            $registeringSecondFactor->getLoaLevel($secondFactorTypeService),
655
        );
656
657
        // Was the authorizing token a self-asserted token (does it have LoA 1.5?)
658
        $isSelfVetUsingSAT = $authoringSecondFactorLoa->getLevel() === Loa::LOA_SELF_VETTED;
659
660
        if (!$selfVettingIsAllowed && !$isSelfVetUsingSAT) {
661
            throw new DomainException(
662
                "The second factor to be vetted has a higher LoA then the Token used for proving possession",
663
            );
664
        }
665
666
        if ($isSelfVetUsingSAT) {
667
            // Assert that all previously vetted tokens are SAT tokens. If this is not the case, do not allow
668
            // self vetting using a SAT.
669
            $this->assertAllVettedTokensAreSelfAsserted();
670
            $recoveryToken = $this->recoveryTokens->first();
671
            $registeringSecondFactor->vet(true, new SelfAssertedRegistrationVettingType($recoveryToken->getTokenId()));
672
            return;
673
        }
674
        $registeringSecondFactor->vet(true, new SelfVetVettingType($authoringSecondFactorLoa));
675
    }
676
677
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $sourceIdentity should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $secondFactorId should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $targetSecondFactorId should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $maxNumberOfTokens should have a doc-comment as per coding-style.
Loading history...
678
     * Copy a token from the source identity to the target identity
679
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
680
    public function migrateVettedSecondFactor(
681
        IdentityApi $sourceIdentity,
682
        SecondFactorId $secondFactorId,
683
        string $targetSecondFactorId,
684
        int $maxNumberOfTokens,
685
    ): void {
686
        $this->assertNotForgotten();
687
        $this->assertUserMayAddSecondFactor($maxNumberOfTokens);
688
        $secondFactor = $sourceIdentity->getVettedSecondFactorById($secondFactorId);
689
        if (!$secondFactor instanceof VettedSecondFactor) {
690
            throw new DomainException("The second factor on the original identity can not be found");
691
        }
692
        $this->assertTokenNotAlreadyRegistered($secondFactor->getType(), $secondFactor->getIdentifier());
693
        $this->assertTokenTypeNotAlreadyRegistered($secondFactor->getType());
694
        if ($sourceIdentity->getInstitution()->equals($this->getInstitution())) {
695
            throw new DomainException("Cannot move the second factor to the same institution");
696
        }
697
698
        $this->apply(
699
            new SecondFactorMigratedEvent(
700
                $this->getId(),
701
                $this->getNameId(),
702
                $this->getInstitution(),
703
                $sourceIdentity->getInstitution(),
704
                $secondFactorId,
705
                new SecondFactorId($targetSecondFactorId),
706
                $secondFactor->getType(),
707
                $secondFactor->getIdentifier(),
708
                $secondFactor->vettingType(),
709
                $this->getCommonName(),
710
                $this->getEmail(),
711
                $this->getPreferredLocale(),
712
            ),
713
        );
714
715
        $this->apply(
716
            new SecondFactorMigratedToEvent(
717
                $sourceIdentity->getId(),
718
                $sourceIdentity->getInstitution(),
719
                $this->getInstitution(),
720
                $secondFactor->getId(),
0 ignored issues
show
Bug introduced by
It seems like $secondFactor->getId() can also be of type null; however, parameter $secondFactorId of Surfnet\Stepup\Identity\...dToEvent::__construct() does only seem to accept Surfnet\Stepup\Identity\Value\SecondFactorId, maybe add an additional type check? ( Ignorable by Annotation )

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

720
                /** @scrutinizer ignore-type */ $secondFactor->getId(),
Loading history...
721
                new SecondFactorId($targetSecondFactorId),
722
                $secondFactor->getType(),
723
                $secondFactor->getIdentifier(),
724
            ),
725
        );
726
    }
727
728
    public function complyWithVettingOfSecondFactor(
729
        SecondFactorId         $secondFactorId,
730
        SecondFactorType       $secondFactorType,
731
        SecondFactorIdentifier $secondFactorIdentifier,
732
        string                 $registrationCode,
733
        DocumentNumber         $documentNumber,
734
        bool                   $provePossessionSkipped,
735
    ): void {
736
        $this->assertNotForgotten();
737
738
        $secondFactorToVet = null;
739
        foreach ($this->verifiedSecondFactors as $secondFactor) {
740
            /** @var VerifiedSecondFactor $secondFactor */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
741
            if ($secondFactor->hasRegistrationCodeAndIdentifier($registrationCode, $secondFactorIdentifier)) {
742
                $secondFactorToVet = $secondFactor;
743
            }
744
        }
745
746
        if (!$secondFactorToVet) {
747
            throw new DomainException(
748
                'Cannot vet second factor, no verified second factor can be vetted using the given registration code ' .
749
                'and second factor identifier',
750
            );
751
        }
752
753
        if (!$secondFactorToVet->canBeVettedNow()) {
754
            throw new DomainException('Cannot vet second factor, the registration window is closed.');
755
        }
756
757
        $secondFactorToVet->vet($provePossessionSkipped, new OnPremiseVettingType($documentNumber));
758
    }
759
760
    public function revokeSecondFactor(SecondFactorId $secondFactorId): void
761
    {
762
        $this->assertNotForgotten();
763
764
        /** @var UnverifiedSecondFactor|null $unverifiedSecondFactor */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
765
        $unverifiedSecondFactor = $this->unverifiedSecondFactors->get((string)$secondFactorId);
0 ignored issues
show
Bug introduced by
The method get() does not exist on null. ( Ignorable by Annotation )

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

765
        /** @scrutinizer ignore-call */ 
766
        $unverifiedSecondFactor = $this->unverifiedSecondFactors->get((string)$secondFactorId);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
766
        /** @var VerifiedSecondFactor|null $verifiedSecondFactor */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
767
        $verifiedSecondFactor = $this->verifiedSecondFactors->get((string)$secondFactorId);
0 ignored issues
show
Bug introduced by
The method get() does not exist on null. ( Ignorable by Annotation )

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

767
        /** @scrutinizer ignore-call */ 
768
        $verifiedSecondFactor = $this->verifiedSecondFactors->get((string)$secondFactorId);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
768
        /** @var VettedSecondFactor|null $vettedSecondFactor */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
769
        $vettedSecondFactor = $this->vettedSecondFactors->get((string)$secondFactorId);
770
771
        if (!$unverifiedSecondFactor && !$verifiedSecondFactor && !$vettedSecondFactor) {
772
            throw new DomainException('Cannot revoke second factor: no second factor with given id exists.');
773
        }
774
775
        if ($unverifiedSecondFactor) {
776
            $unverifiedSecondFactor->revoke();
777
778
            return;
779
        }
780
781
        if ($verifiedSecondFactor) {
782
            $verifiedSecondFactor->revoke();
783
784
            return;
785
        }
786
787
        $vettedSecondFactor->revoke();
788
789
        if ($this->vettedSecondFactors->isEmpty()) {
790
            $this->allVettedSecondFactorsRemoved();
791
        }
792
    }
793
794
    public function complyWithSecondFactorRevocation(SecondFactorId $secondFactorId, IdentityId $authorityId): void
795
    {
796
        $this->assertNotForgotten();
797
798
        /** @var UnverifiedSecondFactor|null $unverifiedSecondFactor */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
799
        $unverifiedSecondFactor = $this->unverifiedSecondFactors->get((string)$secondFactorId);
800
        /** @var VerifiedSecondFactor|null $verifiedSecondFactor */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
801
        $verifiedSecondFactor = $this->verifiedSecondFactors->get((string)$secondFactorId);
802
        /** @var VettedSecondFactor|null $vettedSecondFactor */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
803
        $vettedSecondFactor = $this->vettedSecondFactors->get((string)$secondFactorId);
804
805
        if (!$unverifiedSecondFactor && !$verifiedSecondFactor && !$vettedSecondFactor) {
806
            throw new DomainException('Cannot revoke second factor: no second factor with given id exists.');
807
        }
808
809
        if ($unverifiedSecondFactor) {
810
            $unverifiedSecondFactor->complyWithRevocation($authorityId);
811
812
            return;
813
        }
814
815
        if ($verifiedSecondFactor) {
816
            $verifiedSecondFactor->complyWithRevocation($authorityId);
817
818
            return;
819
        }
820
821
        $vettedSecondFactor->complyWithRevocation($authorityId);
822
823
        if ($this->vettedSecondFactors->isEmpty()) {
824
            $this->allVettedSecondFactorsRemoved();
825
        }
826
    }
827
828
    public function revokeRecoveryToken(RecoveryTokenId $recoveryTokenId): void
829
    {
830
        $this->assertNotForgotten();
831
        try {
832
            $recoveryToken = $this->recoveryTokens->get($recoveryTokenId);
833
        } catch (DomainException $e) {
834
            throw new DomainException('Cannot revoke recovery token: no token with given id exists.', 0, $e);
835
        }
836
        $recoveryToken->revoke();
837
    }
838
839
    public function complyWithRecoveryTokenRevocation(RecoveryTokenId $recoveryTokenId, IdentityId $authorityId): void
840
    {
841
        $this->assertNotForgotten();
842
        try {
843
            $recoveryToken = $this->recoveryTokens->get($recoveryTokenId);
844
        } catch (DomainException $e) {
845
            throw new DomainException('Cannot revoke recovery token: no token with given id exists.', 0, $e);
846
        }
847
        $recoveryToken->complyWithRevocation($authorityId);
848
    }
849
850
    /**
851
     * @param RegistrationAuthorityRole $role
0 ignored issues
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
852
     * @param Institution $institution
0 ignored issues
show
Coding Style introduced by
Expected 15 spaces after parameter type; 1 found
Loading history...
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
853
     * @param Location $location
0 ignored issues
show
Coding Style introduced by
Expected 18 spaces after parameter type; 1 found
Loading history...
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
854
     * @param ContactInformation $contactInformation
0 ignored issues
show
Coding Style introduced by
Expected 8 spaces after parameter type; 1 found
Loading history...
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
855
     * @param InstitutionConfiguration $institutionConfiguration
0 ignored issues
show
Coding Style introduced by
Expected 2 spaces after parameter type; 1 found
Loading history...
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
856
     * @return void
0 ignored issues
show
Coding Style introduced by
Tag @return cannot be grouped with parameter tags in a doc comment
Loading history...
857
     */
858
    public function accreditWith(
859
        RegistrationAuthorityRole $role,
860
        Institution $institution,
861
        Location $location,
862
        ContactInformation $contactInformation,
863
        InstitutionConfiguration $institutionConfiguration,
864
    ): void {
865
        $this->assertNotForgotten();
866
867
        if (!$institutionConfiguration->isInstitutionAllowedToAccreditRoles(
868
            new ConfigurationInstitution($this->institution->getInstitution()),
869
        )) {
0 ignored issues
show
Coding Style introduced by
Closing parenthesis of a multi-line IF statement must be on a new line
Loading history...
870
            throw new DomainException('An Identity may only be accredited by configured institutions.');
871
        }
872
873
        if (!$this->vettedSecondFactors->count()) {
874
            throw new DomainException(
875
                'An Identity must have at least one vetted second factor before it can be accredited',
876
            );
877
        }
878
879
        if ($this->registrationAuthorities->exists($institution)) {
0 ignored issues
show
Bug introduced by
The method exists() does not exist on null. ( Ignorable by Annotation )

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

879
        if ($this->registrationAuthorities->/** @scrutinizer ignore-call */ exists($institution)) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
880
            throw new DomainException('Cannot accredit Identity as it has already been accredited for institution');
881
        }
882
883
        if ($role->equals(new RegistrationAuthorityRole(RegistrationAuthorityRole::ROLE_RA))) {
884
            $this->apply(
885
                new IdentityAccreditedAsRaForInstitutionEvent(
886
                    $this->id,
887
                    $this->nameId,
888
                    $this->institution,
889
                    $role,
890
                    $location,
891
                    $contactInformation,
892
                    $institution,
893
                ),
894
            );
895
        } elseif ($role->equals(new RegistrationAuthorityRole(RegistrationAuthorityRole::ROLE_RAA))) {
896
            $this->apply(
897
                new IdentityAccreditedAsRaaForInstitutionEvent(
898
                    $this->id,
899
                    $this->nameId,
900
                    $this->institution,
901
                    $role,
902
                    $location,
903
                    $contactInformation,
904
                    $institution,
905
                ),
906
            );
907
        } else {
908
            throw new DomainException('An Identity can only be accredited with either the RA or RAA role');
909
        }
910
    }
911
912
    public function amendRegistrationAuthorityInformation(
913
        Institution $institution,
914
        Location $location,
915
        ContactInformation $contactInformation,
916
    ): void {
917
        $this->assertNotForgotten();
918
919
        if (!$this->registrationAuthorities->exists($institution)) {
920
            throw new DomainException(
921
                'Cannot amend registration authority information: identity is not a registration authority for institution',
922
            );
923
        }
924
925
        $this->apply(
926
            new RegistrationAuthorityInformationAmendedForInstitutionEvent(
927
                $this->id,
928
                $this->institution,
929
                $this->nameId,
930
                $location,
931
                $contactInformation,
932
                $institution,
933
            ),
934
        );
935
    }
936
937
    /**
938
     * This method will appoint an institution to become ra or raa for another institution
939
     *
940
     * @param Institution $institution
0 ignored issues
show
Coding Style introduced by
Expected 15 spaces after parameter type; 1 found
Loading history...
941
     * @param RegistrationAuthorityRole $role
942
     * @param InstitutionConfiguration $institutionConfiguration
0 ignored issues
show
Coding Style introduced by
Expected 2 spaces after parameter type; 1 found
Loading history...
943
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
944
    public function appointAs(
945
        Institution $institution,
946
        RegistrationAuthorityRole $role,
947
        InstitutionConfiguration $institutionConfiguration,
948
    ): void {
949
        $this->assertNotForgotten();
950
951
        if (!$institutionConfiguration->isInstitutionAllowedToAccreditRoles(
952
            new ConfigurationInstitution($this->institution->getInstitution()),
953
        )) {
0 ignored issues
show
Coding Style introduced by
Closing parenthesis of a multi-line IF statement must be on a new line
Loading history...
954
            throw new DomainException(
955
                'Cannot appoint as different RegistrationAuthorityRole: identity is not a registration authority for institution',
956
            );
957
        }
958
959
        $registrationAuthority = $this->registrationAuthorities->get($institution);
960
961
        if ($registrationAuthority->isAppointedAs($role)) {
962
            return;
963
        }
964
965
        if ($role->equals(new RegistrationAuthorityRole(RegistrationAuthorityRole::ROLE_RA))) {
966
            $this->apply(
967
                new AppointedAsRaForInstitutionEvent($this->id, $this->institution, $this->nameId, $institution),
968
            );
969
        } elseif ($role->equals(new RegistrationAuthorityRole(RegistrationAuthorityRole::ROLE_RAA))) {
970
            $this->apply(
971
                new AppointedAsRaaForInstitutionEvent($this->id, $this->institution, $this->nameId, $institution),
972
            );
973
        } else {
974
            throw new DomainException('An Identity can only be appointed as either RA or RAA');
975
        }
976
    }
977
978
    public function retractRegistrationAuthority(Institution $institution): void
979
    {
980
        $this->assertNotForgotten();
981
982
        if (!$this->registrationAuthorities->exists($institution)) {
983
            throw new DomainException(
984
                'Cannot Retract Registration Authority as the Identity is not a registration authority',
985
            );
986
        }
987
988
        $this->apply(
989
            new RegistrationAuthorityRetractedForInstitutionEvent(
990
                $this->id,
991
                $this->institution,
992
                $this->nameId,
993
                $this->commonName,
994
                $this->email,
995
                $institution,
996
            ),
997
        );
998
    }
999
1000
    public function expressPreferredLocale(Locale $preferredLocale): void
1001
    {
1002
        $this->assertNotForgotten();
1003
1004
        if ($this->preferredLocale === $preferredLocale) {
1005
            return;
1006
        }
1007
1008
        $this->apply(new LocalePreferenceExpressedEvent($this->id, $this->institution, $preferredLocale));
1009
    }
1010
1011
    public function forget(): void
1012
    {
1013
        if ($this->registrationAuthorities->count() !== 0) {
1014
            throw new DomainException('Cannot forget an identity that is currently accredited as an RA(A)');
1015
        }
1016
1017
        $this->apply(new IdentityForgottenEvent($this->id, $this->institution));
1018
    }
1019
1020
    public function restore(
1021
        CommonName $commonName,
1022
        Email $email,
1023
    ): void {
1024
        if (!$this->forgotten) {
1025
            return;
1026
        }
1027
1028
        $this->apply(new IdentityRestoredEvent($this->id, $this->institution, $commonName, $email));
1029
    }
1030
1031
    public function allVettedSecondFactorsRemoved(): void
1032
    {
1033
        $this->apply(
1034
            new VettedSecondFactorsAllRevokedEvent(
1035
                $this->id,
1036
                $this->institution,
1037
            ),
1038
        );
1039
    }
1040
1041
    protected function applyIdentityCreatedEvent(IdentityCreatedEvent $event): void
1042
    {
1043
        $this->id = $event->identityId;
1044
        $this->institution = $event->identityInstitution;
1045
        $this->nameId = $event->nameId;
1046
        $this->commonName = $event->commonName;
1047
        $this->email = $event->email;
1048
        $this->preferredLocale = $event->preferredLocale;
1049
        $this->forgotten = false;
1050
1051
        $this->unverifiedSecondFactors = new SecondFactorCollection();
1052
        $this->verifiedSecondFactors = new SecondFactorCollection();
1053
        $this->vettedSecondFactors = new SecondFactorCollection();
1054
        $this->registrationAuthorities = new RegistrationAuthorityCollection();
1055
        $this->recoveryTokens = new RecoveryTokenCollection();
1056
    }
1057
1058
    protected function applyIdentityRestoredEvent(IdentityRestoredEvent $event): void
1059
    {
1060
        $this->unverifiedSecondFactors = new SecondFactorCollection();
1061
        $this->verifiedSecondFactors = new SecondFactorCollection();
1062
        $this->vettedSecondFactors = new SecondFactorCollection();
1063
        $this->registrationAuthorities = new RegistrationAuthorityCollection();
1064
        $this->recoveryTokens = new RecoveryTokenCollection();
1065
1066
        $this->commonName = $event->commonName;
1067
        $this->email = $event->email;
1068
        $this->forgotten = false;
1069
    }
1070
1071
    public function applyIdentityRenamedEvent(IdentityRenamedEvent $event): void
1072
    {
1073
        $this->commonName = $event->commonName;
1074
    }
1075
1076
    public function applyIdentityEmailChangedEvent(IdentityEmailChangedEvent $event): void
1077
    {
1078
        $this->email = $event->email;
1079
    }
1080
1081
    protected function applyYubikeySecondFactorBootstrappedEvent(YubikeySecondFactorBootstrappedEvent $event): void
1082
    {
1083
        $secondFactor = VettedSecondFactor::create(
1084
            $event->secondFactorId,
1085
            $this,
1086
            new SecondFactorType('yubikey'),
1087
            $event->yubikeyPublicId,
1088
            new UnknownVettingType(),
1089
        );
1090
1091
        $this->vettedSecondFactors->set((string)$secondFactor->getId(), $secondFactor);
1092
    }
1093
1094
    protected function applyYubikeyPossessionProvenEvent(YubikeyPossessionProvenEvent $event): void
1095
    {
1096
        $secondFactor = UnverifiedSecondFactor::create(
1097
            $event->secondFactorId,
1098
            $this,
1099
            new SecondFactorType('yubikey'),
1100
            $event->yubikeyPublicId,
1101
            $event->emailVerificationWindow,
1102
            $event->emailVerificationNonce,
1103
        );
1104
1105
        $this->unverifiedSecondFactors->set((string)$secondFactor->getId(), $secondFactor);
1106
    }
1107
1108
    protected function applyYubikeyPossessionProvenAndVerifiedEvent(YubikeyPossessionProvenAndVerifiedEvent $event): void
1109
    {
1110
        $secondFactor = VerifiedSecondFactor::create(
1111
            $event->secondFactorId,
1112
            $this,
1113
            new SecondFactorType('yubikey'),
1114
            $event->yubikeyPublicId,
1115
            $event->registrationRequestedAt,
1116
            $event->registrationCode,
1117
        );
1118
1119
        $this->verifiedSecondFactors->set((string)$secondFactor->getId(), $secondFactor);
1120
    }
1121
1122
    protected function applyPhonePossessionProvenEvent(PhonePossessionProvenEvent $event): void
1123
    {
1124
        $secondFactor = UnverifiedSecondFactor::create(
1125
            $event->secondFactorId,
1126
            $this,
1127
            new SecondFactorType('sms'),
1128
            $event->phoneNumber,
1129
            $event->emailVerificationWindow,
1130
            $event->emailVerificationNonce,
1131
        );
1132
1133
        $this->unverifiedSecondFactors->set((string)$secondFactor->getId(), $secondFactor);
1134
    }
1135
1136
    protected function applyPhonePossessionProvenAndVerifiedEvent(PhonePossessionProvenAndVerifiedEvent $event): void
1137
    {
1138
        $secondFactor = VerifiedSecondFactor::create(
1139
            $event->secondFactorId,
1140
            $this,
1141
            new SecondFactorType('sms'),
1142
            $event->phoneNumber,
1143
            $event->registrationRequestedAt,
1144
            $event->registrationCode,
1145
        );
1146
1147
        $this->verifiedSecondFactors->set((string)$secondFactor->getId(), $secondFactor);
1148
    }
1149
1150
    protected function applyGssfPossessionProvenEvent(GssfPossessionProvenEvent $event): void
1151
    {
1152
        $secondFactor = UnverifiedSecondFactor::create(
1153
            $event->secondFactorId,
1154
            $this,
1155
            new SecondFactorType((string)$event->stepupProvider),
1156
            $event->gssfId,
1157
            $event->emailVerificationWindow,
1158
            $event->emailVerificationNonce,
1159
        );
1160
1161
        $this->unverifiedSecondFactors->set((string)$secondFactor->getId(), $secondFactor);
1162
    }
1163
1164
    protected function applyGssfPossessionProvenAndVerifiedEvent(GssfPossessionProvenAndVerifiedEvent $event): void
1165
    {
1166
        $secondFactor = VerifiedSecondFactor::create(
1167
            $event->secondFactorId,
1168
            $this,
1169
            new SecondFactorType((string)$event->stepupProvider),
1170
            $event->gssfId,
1171
            $event->registrationRequestedAt,
1172
            $event->registrationCode,
1173
        );
1174
1175
        $this->verifiedSecondFactors->set((string)$secondFactor->getId(), $secondFactor);
1176
    }
1177
1178
    protected function applyU2fDevicePossessionProvenEvent(U2fDevicePossessionProvenEvent $event): void
1179
    {
1180
        $secondFactor = UnverifiedSecondFactor::create(
1181
            $event->secondFactorId,
1182
            $this,
1183
            new SecondFactorType('u2f'),
1184
            $event->keyHandle,
1185
            $event->emailVerificationWindow,
1186
            $event->emailVerificationNonce,
1187
        );
1188
1189
        $this->unverifiedSecondFactors->set((string)$secondFactor->getId(), $secondFactor);
1190
    }
1191
1192
    protected function applyU2fDevicePossessionProvenAndVerifiedEvent(U2fDevicePossessionProvenAndVerifiedEvent $event): void
1193
    {
1194
        $secondFactor = VerifiedSecondFactor::create(
1195
            $event->secondFactorId,
1196
            $this,
1197
            new SecondFactorType('u2f'),
1198
            $event->keyHandle,
1199
            $event->registrationRequestedAt,
1200
            $event->registrationCode,
1201
        );
1202
1203
        $this->verifiedSecondFactors->set((string)$secondFactor->getId(), $secondFactor);
1204
    }
1205
1206
    protected function applyPhoneRecoveryTokenPossessionProvenEvent(PhoneRecoveryTokenPossessionProvenEvent $event): void
1207
    {
1208
        $recoveryToken = RecoveryTokenEntity::create($event->recoveryTokenId, RecoveryTokenType::sms(), $this);
1209
1210
        $this->recoveryTokens->set($recoveryToken);
1211
    }
1212
1213
    protected function applySafeStoreSecretRecoveryTokenPossessionPromisedEvent(
1214
        SafeStoreSecretRecoveryTokenPossessionPromisedEvent $event,
1215
    ): void {
1216
        $recoveryToken = RecoveryTokenEntity::create($event->recoveryTokenId, RecoveryTokenType::safeStore(), $this);
1217
1218
        $this->recoveryTokens->set($recoveryToken);
1219
    }
1220
1221
    protected function applyEmailVerifiedEvent(EmailVerifiedEvent $event): void
1222
    {
1223
        $secondFactorId = (string)$event->secondFactorId;
1224
1225
        /** @var UnverifiedSecondFactor $unverified */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
1226
        $unverified = $this->unverifiedSecondFactors->get($secondFactorId);
1227
        $verified = $unverified->asVerified($event->registrationRequestedAt, $event->registrationCode);
1228
1229
        $this->unverifiedSecondFactors->remove($secondFactorId);
1230
        $this->verifiedSecondFactors->set($secondFactorId, $verified);
1231
    }
1232
1233
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $event should have a doc-comment as per coding-style.
Loading history...
1234
     * The SecondFactorMigratedToEvent is applied by creating a new
1235
     * vetted second factor on the target identity. The source
1236
     * second factor is not yet forgotten.
1237
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
1238
    public function applySecondFactorMigratedEvent(SecondFactorMigratedEvent $event): void
1239
    {
1240
        $secondFactorId = (string)$event->newSecondFactorId;
1241
        $vetted = VettedSecondFactor::create(
1242
            $event->newSecondFactorId,
1243
            $this,
1244
            $event->secondFactorType,
1245
            $event->secondFactorIdentifier,
1246
            $event->vettingType,
1247
        );
1248
        $this->vettedSecondFactors->set($secondFactorId, $vetted);
1249
    }
1250
1251
    protected function applySecondFactorVettedEvent(SecondFactorVettedEvent $event): void
1252
    {
1253
        $secondFactorId = (string)$event->secondFactorId;
1254
        $verified = $this->verifiedSecondFactors->get($secondFactorId);
1255
        $vetted = $verified->asVetted($event->vettingType);
1256
        $this->verifiedSecondFactors->remove($secondFactorId);
1257
        $this->vettedSecondFactors->set($secondFactorId, $vetted);
1258
    }
1259
1260
    protected function applySecondFactorVettedWithoutTokenProofOfPossession(
1261
        SecondFactorVettedWithoutTokenProofOfPossession $event,
1262
    ): void {
1263
        $secondFactorId = (string)$event->secondFactorId;
1264
1265
        /** @var VerifiedSecondFactor $verified */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
1266
        $verified = $this->verifiedSecondFactors->get($secondFactorId);
1267
        $vetted = $verified->asVetted($event->vettingType);
0 ignored issues
show
Bug introduced by
It seems like $event->vettingType can also be of type null; however, parameter $vettingType of Surfnet\Stepup\Identity\...econdFactor::asVetted() does only seem to accept Surfnet\Stepup\Identity\Value\VettingType, maybe add an additional type check? ( Ignorable by Annotation )

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

1267
        $vetted = $verified->asVetted(/** @scrutinizer ignore-type */ $event->vettingType);
Loading history...
1268
1269
        $this->verifiedSecondFactors->remove($secondFactorId);
1270
        $this->vettedSecondFactors->set($secondFactorId, $vetted);
1271
    }
1272
1273
    protected function applyUnverifiedSecondFactorRevokedEvent(UnverifiedSecondFactorRevokedEvent $event): void
1274
    {
1275
        $this->unverifiedSecondFactors->remove((string)$event->secondFactorId);
1276
    }
1277
1278
    protected function applyCompliedWithUnverifiedSecondFactorRevocationEvent(
1279
        CompliedWithUnverifiedSecondFactorRevocationEvent $event,
1280
    ): void {
1281
        $this->unverifiedSecondFactors->remove((string)$event->secondFactorId);
1282
    }
1283
1284
    protected function applyVerifiedSecondFactorRevokedEvent(VerifiedSecondFactorRevokedEvent $event): void
1285
    {
1286
        $this->verifiedSecondFactors->remove((string)$event->secondFactorId);
1287
    }
1288
1289
    protected function applyCompliedWithVerifiedSecondFactorRevocationEvent(
1290
        CompliedWithVerifiedSecondFactorRevocationEvent $event,
1291
    ): void {
1292
        $this->verifiedSecondFactors->remove((string)$event->secondFactorId);
1293
    }
1294
1295
    protected function applyVettedSecondFactorRevokedEvent(VettedSecondFactorRevokedEvent $event): void
1296
    {
1297
        $this->vettedSecondFactors->remove((string)$event->secondFactorId);
1298
    }
1299
1300
    protected function applyCompliedWithVettedSecondFactorRevocationEvent(
1301
        CompliedWithVettedSecondFactorRevocationEvent $event,
1302
    ): void {
1303
        $this->vettedSecondFactors->remove((string)$event->secondFactorId);
1304
    }
1305
1306
    protected function applyCompliedWithRecoveryCodeRevocationEvent(CompliedWithRecoveryCodeRevocationEvent $event): void
1307
    {
1308
        $this->recoveryTokens->remove($event->recoveryTokenId);
1309
    }
1310
1311
    protected function applyRecoveryTokenRevokedEvent(RecoveryTokenRevokedEvent $event): void
1312
    {
1313
        $this->recoveryTokens->remove($event->recoveryTokenId);
1314
    }
1315
1316
    protected function applyIdentityAccreditedAsRaForInstitutionEvent(IdentityAccreditedAsRaForInstitutionEvent $event): void
1317
    {
1318
        $this->registrationAuthorities->set(
1319
            $event->raInstitution,
1320
            RegistrationAuthority::accreditWith(
1321
                $event->registrationAuthorityRole,
1322
                $event->location,
1323
                $event->contactInformation,
1324
                $event->raInstitution,
1325
            ),
1326
        );
1327
    }
1328
1329
    protected function applyIdentityAccreditedAsRaaForInstitutionEvent(IdentityAccreditedAsRaaForInstitutionEvent $event,): void
1330
    {
1331
        $this->registrationAuthorities->set(
1332
            $event->raInstitution,
1333
            RegistrationAuthority::accreditWith(
1334
                $event->registrationAuthorityRole,
1335
                $event->location,
1336
                $event->contactInformation,
1337
                $event->raInstitution,
1338
            ),
1339
        );
1340
    }
1341
1342
    protected function applyRegistrationAuthorityInformationAmendedForInstitutionEvent(
1343
        RegistrationAuthorityInformationAmendedForInstitutionEvent $event,
1344
    ): void {
1345
        $this->registrationAuthorities->get($event->raInstitution)->amendInformation(
1346
            $event->location,
1347
            $event->contactInformation,
1348
        );
1349
    }
1350
1351
    protected function applyAppointedAsRaaForInstitutionEvent(AppointedAsRaaForInstitutionEvent $event): void
1352
    {
1353
        $this->registrationAuthorities->get($event->raInstitution)->appointAs(
1354
            new RegistrationAuthorityRole(RegistrationAuthorityRole::ROLE_RAA),
1355
        );
1356
    }
1357
1358
    protected function applyRegistrationAuthorityRetractedForInstitutionEvent(
1359
        RegistrationAuthorityRetractedForInstitutionEvent $event,
1360
    ): void {
1361
        $this->registrationAuthorities->remove($event->raInstitution);
1362
    }
1363
1364
    protected function applyLocalePreferenceExpressedEvent(LocalePreferenceExpressedEvent $event): void
1365
    {
1366
        $this->preferredLocale = $event->preferredLocale;
1367
    }
1368
1369
    protected function applyIdentityForgottenEvent(IdentityForgottenEvent $event): void
0 ignored issues
show
Unused Code introduced by
The parameter $event is not used and could be removed. ( Ignorable by Annotation )

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

1369
    protected function applyIdentityForgottenEvent(/** @scrutinizer ignore-unused */ IdentityForgottenEvent $event): void

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1370
    {
1371
        $this->commonName = CommonName::unknown();
1372
        $this->email = Email::unknown();
1373
        $this->forgotten = true;
1374
    }
1375
1376
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $event should have a doc-comment as per coding-style.
Loading history...
1377
     * This method is kept to be backwards compatible for changes before FGA
1378
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
1379
    protected function applyAppointedAsRaEvent(AppointedAsRaEvent $event): void
1380
    {
1381
        $this->registrationAuthorities->get($event->identityInstitution)
1382
            ->appointAs(new RegistrationAuthorityRole(RegistrationAuthorityRole::ROLE_RA));
1383
    }
1384
1385
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $event should have a doc-comment as per coding-style.
Loading history...
1386
     * This method is kept to be backwards compatible for changes before FGA
1387
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
1388
    protected function applyAppointedAsRaaEvent(AppointedAsRaaEvent $event): void
1389
    {
1390
        $this->registrationAuthorities->get($event->identityInstitution)
1391
            ->appointAs(new RegistrationAuthorityRole(RegistrationAuthorityRole::ROLE_RAA));
1392
    }
1393
1394
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $event should have a doc-comment as per coding-style.
Loading history...
1395
     * This method is kept to be backwards compatible for changes before FGA
1396
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
1397
    protected function applyIdentityAccreditedAsRaEvent(IdentityAccreditedAsRaEvent $event): void
1398
    {
1399
        $this->registrationAuthorities->set(
1400
            $event->identityInstitution,
1401
            RegistrationAuthority::accreditWith(
1402
                $event->registrationAuthorityRole,
1403
                $event->location,
1404
                $event->contactInformation,
1405
                $event->identityInstitution,
1406
            ),
1407
        );
1408
    }
1409
1410
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $event should have a doc-comment as per coding-style.
Loading history...
1411
     * This method is kept to be backwards compatible for changes before FGA
1412
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
1413
    protected function applyIdentityAccreditedAsRaaEvent(IdentityAccreditedAsRaaEvent $event): void
1414
    {
1415
        $this->registrationAuthorities->set(
1416
            $event->identityInstitution,
1417
            RegistrationAuthority::accreditWith(
1418
                $event->registrationAuthorityRole,
1419
                $event->location,
1420
                $event->contactInformation,
1421
                $event->identityInstitution,
1422
            ),
1423
        );
1424
    }
1425
1426
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $event should have a doc-comment as per coding-style.
Loading history...
1427
     * This method is kept to be backwards compatible for changes before FGA
1428
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
1429
    protected function applyAppointedAsRaForInstitutionEvent(AppointedAsRaForInstitutionEvent $event): void
1430
    {
1431
        $this->registrationAuthorities->get($event->identityInstitution)
1432
            ->appointAs(new RegistrationAuthorityRole(RegistrationAuthorityRole::ROLE_RA));
1433
    }
1434
1435
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $event should have a doc-comment as per coding-style.
Loading history...
1436
     * This method is kept to be backwards compatible for changes before FGA
1437
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
1438
    protected function applyRegistrationAuthorityInformationAmendedEvent(
1439
        RegistrationAuthorityInformationAmendedEvent $event,
1440
    ): void {
1441
        $this->registrationAuthorities->get($event->identityInstitution)->amendInformation(
1442
            $event->location,
1443
            $event->contactInformation,
1444
        );
1445
    }
1446
1447
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $event should have a doc-comment as per coding-style.
Loading history...
1448
     * This method is kept to be backwards compatible for changes before FGA
1449
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
1450
    protected function applyRegistrationAuthorityRetractedEvent(RegistrationAuthorityRetractedEvent $event): void
1451
    {
1452
        $this->registrationAuthorities->remove($event->identityInstitution);
1453
    }
1454
1455
1456
    public function getAggregateRootId(): string
1457
    {
1458
        return $this->id->getIdentityId();
1459
    }
1460
1461
    protected function getChildEntities(): array
1462
    {
1463
        return array_merge(
1464
            $this->unverifiedSecondFactors->getValues(),
1465
            $this->verifiedSecondFactors->getValues(),
1466
            $this->vettedSecondFactors->getValues(),
1467
            $this->registrationAuthorities->getValues(),
1468
        );
1469
    }
1470
1471
    /**
1472
     * @throws DomainException
1473
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
1474
    private function assertNotForgotten(): void
0 ignored issues
show
Coding Style introduced by
Private method name "Identity::assertNotForgotten" must be prefixed with an underscore
Loading history...
1475
    {
1476
        if ($this->forgotten) {
1477
            throw new DomainException('Operation on this Identity is not allowed: it has been forgotten');
1478
        }
1479
    }
1480
1481
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $maxNumberOfTokens should have a doc-comment as per coding-style.
Loading history...
1482
     * @throws DomainException
1483
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
1484
    private function assertUserMayAddSecondFactor(int $maxNumberOfTokens): void
0 ignored issues
show
Coding Style introduced by
Private method name "Identity::assertUserMayAddSecondFactor" must be prefixed with an underscore
Loading history...
1485
    {
1486
        $tokenCount = (int) count($this->unverifiedSecondFactors) + count($this->verifiedSecondFactors) + count($this->vettedSecondFactors);
0 ignored issues
show
Bug introduced by
It seems like $this->unverifiedSecondFactors can also be of type null; however, parameter $value of count() does only seem to accept Countable|array, maybe add an additional type check? ( Ignorable by Annotation )

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

1486
        $tokenCount = (int) count(/** @scrutinizer ignore-type */ $this->unverifiedSecondFactors) + count($this->verifiedSecondFactors) + count($this->vettedSecondFactors);
Loading history...
1487
        if ($tokenCount >= $maxNumberOfTokens
1488
        ) {
1489
            throw new DomainException(
1490
                sprintf('User may not have more than %d token(s)', $maxNumberOfTokens),
1491
            );
1492
        }
1493
    }
1494
1495
    private function assertUserMayAddRecoveryToken(RecoveryTokenType $recoveryTokenType): void
0 ignored issues
show
Coding Style introduced by
Private method name "Identity::assertUserMayAddRecoveryToken" must be prefixed with an underscore
Loading history...
1496
    {
1497
        // Assert this token type is not yet registered
1498
        if ($this->recoveryTokens->hasType($recoveryTokenType)) {
1499
            throw new DomainException(
1500
                sprintf('Recovery token type %s is already registered', (string)$recoveryTokenType),
1501
            );
1502
        }
1503
    }
1504
1505
    public function getId(): IdentityId
1506
    {
1507
        return $this->id;
1508
    }
1509
1510
    /**
1511
     * @return NameId
1512
     */
1513
    public function getNameId(): NameId
1514
    {
1515
        return $this->nameId;
1516
    }
1517
1518
    /**
1519
     * @return Institution
1520
     */
1521
    public function getInstitution(): Institution
1522
    {
1523
        return $this->institution;
1524
    }
1525
1526
    public function getCommonName(): CommonName
1527
    {
1528
        return $this->commonName;
1529
    }
1530
1531
    public function getEmail(): Email
1532
    {
1533
        return $this->email;
1534
    }
1535
1536
    public function getPreferredLocale(): Locale
1537
    {
1538
        return $this->preferredLocale;
1539
    }
1540
1541
    public function getVerifiedSecondFactor(SecondFactorId $secondFactorId): ?VerifiedSecondFactor
1542
    {
1543
        return $this->verifiedSecondFactors->get((string)$secondFactorId);
1544
    }
1545
1546
    public function getVettedSecondFactorById(SecondFactorId $secondFactorId): ?VettedSecondFactor
1547
    {
1548
        return $this->vettedSecondFactors->get((string)$secondFactorId);
1549
    }
1550
1551
    private function assertTokenNotAlreadyRegistered(SecondFactorType $type, SecondFactorIdentifier $identifier): void
0 ignored issues
show
Coding Style introduced by
Private method name "Identity::assertTokenNotAlreadyRegistered" must be prefixed with an underscore
Loading history...
1552
    {
1553
        foreach ($this->unverifiedSecondFactors as $unverified) {
1554
            if ($unverified->typeAndIdentifierAreEqual($type, $identifier)) {
1555
                throw new DomainException("The second factor was already registered as an unverified second factor");
1556
            }
1557
        }
1558
        foreach ($this->verifiedSecondFactors as $verified) {
1559
            if ($verified->typeAndIdentifierAreEqual($type, $identifier)) {
1560
                throw new DomainException("The second factor was already registered as a verified second factor");
1561
            }
1562
        }
1563
        foreach ($this->vettedSecondFactors as $vetted) {
1564
            if ($vetted->typeAndIdentifierAreEqual($type, $identifier)) {
1565
                throw new DomainException("The second factor was registered as a vetted second factor");
1566
            }
1567
        }
1568
    }
1569
1570
    private function assertTokenTypeNotAlreadyRegistered(SecondFactorType $type): void
0 ignored issues
show
Coding Style introduced by
Private method name "Identity::assertTokenTypeNotAlreadyRegistered" must be prefixed with an underscore
Loading history...
1571
    {
1572
        foreach ($this->unverifiedSecondFactors as $unverified) {
1573
            if ($unverified->getType()->equals($type)) {
1574
                throw new DomainException("This second factor type was already registered as an unverified second factor");
1575
            }
1576
        }
1577
        foreach ($this->verifiedSecondFactors as $verified) {
1578
            if ($verified->getType()->equals($type)) {
1579
                throw new DomainException("This second factor type was already registered as a verified second factor");
1580
            }
1581
        }
1582
        foreach ($this->vettedSecondFactors as $vetted) {
1583
            if ($vetted->getType()->equals($type)) {
1584
                throw new DomainException("This second factor type was already registered as a vetted second factor");
1585
            }
1586
        }
1587
    }
1588
1589
    private function assertSelfAssertedTokenRegistrationAllowed(): void
0 ignored issues
show
Coding Style introduced by
Private method name "Identity::assertSelfAssertedTokenRegistrationAllowed" must be prefixed with an underscore
Loading history...
1590
    {
1591
        if ($this->vettedSecondFactors->count() !== 0) {
1592
            throw new DomainException(
1593
                "Self-asserted second factor registration is only allowed when no tokens are vetted yet",
1594
            );
1595
        }
1596
        if ($this->recoveryTokens->count() === 0) {
1597
            throw new DomainException("A recovery token is required to perform a self-asserted token registration");
1598
        }
1599
    }
1600
1601
    /**
1602
     * Verify that every vetted second factor is self-asserted
1603
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
1604
    private function assertAllVettedTokensAreSelfAsserted(): void
0 ignored issues
show
Coding Style introduced by
Private method name "Identity::assertAllVettedTokensAreSelfAsserted" must be prefixed with an underscore
Loading history...
1605
    {
1606
        /** @var VettedSecondFactor $vettedSecondFactor */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
1607
        foreach ($this->vettedSecondFactors as $vettedSecondFactor) {
1608
            if ($vettedSecondFactor->vettingType()->type() !== VettingType::TYPE_SELF_ASSERTED_REGISTRATION) {
1609
                throw new DomainException(
1610
                    'Not all tokens are self-asserted, it is not allowed to self-vet using the self-asserted token',
1611
                );
1612
            }
1613
        }
1614
    }
1615
}
1616