Completed
Push — feature/verify-email-proven-ev... ( 178167 )
by
unknown
04:44
created

Identity::applyYubikeyPossessionProvenEvent()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 28
Code Lines 20

Duplication

Lines 28
Ratio 100 %

Importance

Changes 0
Metric Value
dl 28
loc 28
rs 8.8571
c 0
b 0
f 0
cc 2
eloc 20
nc 2
nop 1
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
 */
18
19
namespace Surfnet\Stepup\Identity;
20
21
use Broadway\EventSourcing\EventSourcedAggregateRoot;
22
use Surfnet\Stepup\DateTime\DateTime;
23
use Surfnet\Stepup\Exception\DomainException;
24
use Surfnet\Stepup\Identity\Api\Identity as IdentityApi;
25
use Surfnet\Stepup\Identity\Entity\RegistrationAuthority;
26
use Surfnet\Stepup\Identity\Entity\SecondFactorCollection;
27
use Surfnet\Stepup\Identity\Entity\UnverifiedSecondFactor;
28
use Surfnet\Stepup\Identity\Entity\VerifiedSecondFactor;
29
use Surfnet\Stepup\Identity\Entity\VettedSecondFactor;
30
use Surfnet\Stepup\Identity\Event\AppointedAsRaaEvent;
31
use Surfnet\Stepup\Identity\Event\AppointedAsRaEvent;
32
use Surfnet\Stepup\Identity\Event\CompliedWithUnverifiedSecondFactorRevocationEvent;
33
use Surfnet\Stepup\Identity\Event\CompliedWithVerifiedSecondFactorRevocationEvent;
34
use Surfnet\Stepup\Identity\Event\CompliedWithVettedSecondFactorRevocationEvent;
35
use Surfnet\Stepup\Identity\Event\EmailVerifiedEvent;
36
use Surfnet\Stepup\Identity\Event\GssfPossessionProvenEvent;
37
use Surfnet\Stepup\Identity\Event\IdentityAccreditedAsRaaEvent;
38
use Surfnet\Stepup\Identity\Event\IdentityAccreditedAsRaEvent;
39
use Surfnet\Stepup\Identity\Event\IdentityCreatedEvent;
40
use Surfnet\Stepup\Identity\Event\IdentityEmailChangedEvent;
41
use Surfnet\Stepup\Identity\Event\IdentityForgottenEvent;
42
use Surfnet\Stepup\Identity\Event\IdentityRenamedEvent;
43
use Surfnet\Stepup\Identity\Event\ImplicitlyVerifiedByIdp;
44
use Surfnet\Stepup\Identity\Event\LocalePreferenceExpressedEvent;
45
use Surfnet\Stepup\Identity\Event\PhonePossessionProvenEvent;
46
use Surfnet\Stepup\Identity\Event\RegistrationAuthorityInformationAmendedEvent;
47
use Surfnet\Stepup\Identity\Event\RegistrationAuthorityRetractedEvent;
48
use Surfnet\Stepup\Identity\Event\SecondFactorVettedEvent;
49
use Surfnet\Stepup\Identity\Event\U2fDevicePossessionProvenEvent;
50
use Surfnet\Stepup\Identity\Event\UnverifiedSecondFactorRevokedEvent;
51
use Surfnet\Stepup\Identity\Event\VerifiedSecondFactorRevokedEvent;
52
use Surfnet\Stepup\Identity\Event\VettedSecondFactorRevokedEvent;
53
use Surfnet\Stepup\Identity\Event\YubikeyPossessionProvenEvent;
54
use Surfnet\Stepup\Identity\Event\YubikeySecondFactorBootstrappedEvent;
55
use Surfnet\Stepup\Identity\Value\CommonName;
56
use Surfnet\Stepup\Identity\Value\ContactInformation;
57
use Surfnet\Stepup\Identity\Value\DocumentNumber;
58
use Surfnet\Stepup\Identity\Value\Email;
59
use Surfnet\Stepup\Identity\Value\EmailVerificationWindow;
60
use Surfnet\Stepup\Identity\Value\GssfId;
61
use Surfnet\Stepup\Identity\Value\IdentityId;
62
use Surfnet\Stepup\Identity\Value\Institution;
63
use Surfnet\Stepup\Identity\Value\Locale;
64
use Surfnet\Stepup\Identity\Value\Location;
65
use Surfnet\Stepup\Identity\Value\NameId;
66
use Surfnet\Stepup\Identity\Value\PhoneNumber;
67
use Surfnet\Stepup\Identity\Value\RegistrationAuthorityRole;
68
use Surfnet\Stepup\Identity\Value\SecondFactorId;
69
use Surfnet\Stepup\Identity\Value\SecondFactorIdentifier;
70
use Surfnet\Stepup\Identity\Value\StepupProvider;
71
use Surfnet\Stepup\Identity\Value\U2fKeyHandle;
72
use Surfnet\Stepup\Identity\Value\YubikeyPublicId;
73
use Surfnet\Stepup\Token\TokenGenerator;
74
use Surfnet\StepupBundle\Security\OtpGenerator;
75
use Surfnet\StepupBundle\Service\SecondFactorTypeService;
76
use Surfnet\StepupBundle\Value\SecondFactorType;
77
78
/**
79
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
80
 * @SuppressWarnings(PHPMD.TooManyMethods)
81
 * @SuppressWarnings(PHPMD.TooManyPublicMethods)
82
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
83
 */
84
class Identity extends EventSourcedAggregateRoot implements IdentityApi
85
{
86
    /**
87
     * @var IdentityId
88
     */
89
    private $id;
90
91
    /**
92
     * @var Institution
93
     */
94
    private $institution;
95
96
    /**
97
     * @var NameId
98
     */
99
    private $nameId;
100
101
    /**
102
     * @var \Surfnet\Stepup\Identity\Value\CommonName
103
     */
104
    private $commonName;
105
106
    /**
107
     * @var \Surfnet\Stepup\Identity\Value\Email
108
     */
109
    private $email;
110
111
    /**
112
     * @var SecondFactorCollection|UnverifiedSecondFactor[]
113
     */
114
    private $unverifiedSecondFactors;
115
116
    /**
117
     * @var SecondFactorCollection|VerifiedSecondFactor[]
118
     */
119
    private $verifiedSecondFactors;
120
121
    /**
122
     * @var SecondFactorCollection|VettedSecondFactor[]
123
     */
124
    private $vettedSecondFactors;
125
126
    /**
127
     * @var RegistrationAuthority
128
     */
129
    private $registrationAuthority;
130
131
    /**
132
     * @var Locale
133
     */
134
    private $preferredLocale;
135
136
    /**
137
     * @var boolean
138
     */
139
    private $forgotten;
140
141
    /**
142
     * @var int
143
     */
144
    private $maxNumberOfTokens = 1;
145
146
    public static function create(
147
        IdentityId $id,
148
        Institution $institution,
149
        NameId $nameId,
150
        CommonName $commonName,
151
        Email $email,
152
        Locale $preferredLocale
153
    ) {
154
        $identity = new self();
155
        $identity->apply(new IdentityCreatedEvent($id, $institution, $nameId, $commonName, $email, $preferredLocale));
156
157
        return $identity;
158
    }
159
160
    final public function __construct()
161
    {
162
    }
163
164
    public function rename(CommonName $commonName)
165
    {
166
        $this->assertNotForgotten();
167
168
        if ($this->commonName->equals($commonName)) {
169
            return;
170
        }
171
172
        $this->commonName = $commonName;
173
        $this->apply(new IdentityRenamedEvent($this->id, $this->institution, $commonName));
174
    }
175
176
    public function changeEmail(Email $email)
177
    {
178
        $this->assertNotForgotten();
179
180
        if ($this->email->equals($email)) {
181
            return;
182
        }
183
184
        $this->email = $email;
185
        $this->apply(new IdentityEmailChangedEvent($this->id, $this->institution, $email));
186
    }
187
188
    /**
189
     * @param int $numberOfTokens
190
     */
191
    public function setMaxNumberOfTokens($numberOfTokens)
192
    {
193
        $this->maxNumberOfTokens = $numberOfTokens;
194
    }
195
196
    public function bootstrapYubikeySecondFactor(SecondFactorId $secondFactorId, YubikeyPublicId $yubikeyPublicId)
197
    {
198
        $this->assertNotForgotten();
199
        $this->assertUserMayAddSecondFactor();
200
201
        $this->apply(
202
            new YubikeySecondFactorBootstrappedEvent(
203
                $this->id,
204
                $this->nameId,
205
                $this->institution,
206
                $this->commonName,
207
                $this->email,
208
                $this->preferredLocale,
209
                $secondFactorId,
210
                $yubikeyPublicId
211
            )
212
        );
213
    }
214
215 View Code Duplication
    public function provePossessionOfYubikey(
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
216
        SecondFactorId $secondFactorId,
217
        YubikeyPublicId $yubikeyPublicId,
218
        $emailVerificationRequired,
219
        EmailVerificationWindow $emailVerificationWindow
220
    ) {
221
        $this->assertNotForgotten();
222
        $this->assertUserMayAddSecondFactor();
223
224
        $emailVerificationNonce = TokenGenerator::generateNonce();
225
226
        $this->apply(
227
            new YubikeyPossessionProvenEvent(
228
                $this->id,
229
                $this->institution,
230
                $secondFactorId,
231
                $yubikeyPublicId,
232
                $emailVerificationRequired,
233
                $emailVerificationWindow,
234
                $emailVerificationNonce,
235
                $this->commonName,
236
                $this->email,
237
                $this->preferredLocale
238
            )
239
        );
240
241
        if (!$emailVerificationRequired) {
242
            $this->verifyImplicitly($secondFactorId);
243
        }
244
    }
245
246 View Code Duplication
    public function provePossessionOfPhone(
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
247
        SecondFactorId $secondFactorId,
248
        PhoneNumber $phoneNumber,
249
        $emailVerificationRequired,
250
        EmailVerificationWindow $emailVerificationWindow
251
    ) {
252
        $this->assertNotForgotten();
253
        $this->assertUserMayAddSecondFactor();
254
255
        $emailVerificationNonce = TokenGenerator::generateNonce();
256
257
        $this->apply(
258
            new PhonePossessionProvenEvent(
259
                $this->id,
260
                $this->institution,
261
                $secondFactorId,
262
                $phoneNumber,
263
                $emailVerificationRequired,
264
                $emailVerificationWindow,
265
                $emailVerificationNonce,
266
                $this->commonName,
267
                $this->email,
268
                $this->preferredLocale
269
            )
270
        );
271
272
        if (!$emailVerificationRequired) {
273
            $this->verifyImplicitly($secondFactorId);
274
        }
275
    }
276
277 View Code Duplication
    public function provePossessionOfGssf(
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
278
        SecondFactorId $secondFactorId,
279
        StepupProvider $provider,
280
        GssfId $gssfId,
281
        $emailVerificationRequired,
282
        EmailVerificationWindow $emailVerificationWindow
283
    ) {
284
        $this->assertNotForgotten();
285
        $this->assertUserMayAddSecondFactor();
286
287
        $emailVerificationNonce = TokenGenerator::generateNonce();
288
289
        $this->apply(
290
            new GssfPossessionProvenEvent(
291
                $this->id,
292
                $this->institution,
293
                $secondFactorId,
294
                $provider,
295
                $gssfId,
296
                $emailVerificationRequired,
297
                $emailVerificationWindow,
298
                $emailVerificationNonce,
299
                $this->commonName,
300
                $this->email,
301
                $this->preferredLocale
302
            )
303
        );
304
305
        if (!$emailVerificationRequired) {
306
            $this->verifyImplicitly($secondFactorId);
307
        }
308
    }
309
310 View Code Duplication
    public function provePossessionOfU2fDevice(
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
311
        SecondFactorId $secondFactorId,
312
        U2fKeyHandle $keyHandle,
313
        $emailVerificationRequired,
314
        EmailVerificationWindow $emailVerificationWindow
315
    ) {
316
        $this->assertNotForgotten();
317
        $this->assertUserMayAddSecondFactor();
318
319
        $emailVerificationNonce = TokenGenerator::generateNonce();
320
321
        $this->apply(
322
            new U2fDevicePossessionProvenEvent(
323
                $this->id,
324
                $this->institution,
325
                $secondFactorId,
326
                $keyHandle,
327
                $emailVerificationRequired,
328
                $emailVerificationWindow,
329
                $emailVerificationNonce,
330
                $this->commonName,
331
                $this->email,
332
                $this->preferredLocale
333
            )
334
        );
335
336
        if (!$emailVerificationRequired) {
337
            $this->verifyImplicitly($secondFactorId);
338
        }
339
    }
340
341
    public function verifyEmail($verificationNonce)
342
    {
343
        $this->assertNotForgotten();
344
345
        $secondFactorToVerify = null;
346
        foreach ($this->unverifiedSecondFactors as $secondFactor) {
347
            /** @var Entity\UnverifiedSecondFactor $secondFactor */
348
            if ($secondFactor->hasNonce($verificationNonce)) {
349
                $secondFactorToVerify = $secondFactor;
350
            }
351
        }
352
353
        if (!$secondFactorToVerify) {
354
            throw new DomainException(
355
                'Cannot verify second factor, no unverified second factor can be verified using the given nonce'
356
            );
357
        }
358
359
        /** @var Entity\UnverifiedSecondFactor $secondFactorToVerify */
360
        if (!$secondFactorToVerify->canBeVerifiedNow()) {
361
            throw new DomainException('Cannot verify second factor, the verification window is closed.');
362
        }
363
364
        $secondFactorToVerify->verifyEmail();
365
    }
366
367
    /**
368
     * @param string $secondFactorId
369
     */
370
    public function verifyImplicitly($secondFactorId)
371
    {
372
        $this->assertNotForgotten();
373
374
        $secondFactorToVerify = null;
375
        foreach ($this->verifiedSecondFactors as $secondFactor) {
376
            /** @var Entity\UnverifiedSecondFactor $secondFactor */
377
            if ($secondFactor->getId() === $secondFactorId) {
378
                $secondFactorToVerify = $secondFactor;
379
            }
380
        }
381
382
        if (!$secondFactorToVerify) {
383
            throw new DomainException(
384
                'Cannot verify second factor, no unverified second factor can be verified using the given nonce'
385
            );
386
        }
387
388
        $secondFactorToVerify->verifyImplicitly();
0 ignored issues
show
Bug introduced by
The method verifyImplicitly() does not seem to exist on object<Surfnet\Stepup\Id...UnverifiedSecondFactor>.

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...
389
    }
390
391
    public function vetSecondFactor(
392
        IdentityApi $registrant,
393
        SecondFactorId $registrantsSecondFactorId,
394
        SecondFactorType $registrantsSecondFactorType,
395
        SecondFactorIdentifier $registrantsSecondFactorIdentifier,
0 ignored issues
show
Comprehensibility Naming introduced by
The variable name $registrantsSecondFactorIdentifier exceeds the maximum configured length of 30.

Very long variable names usually make code harder to read. It is therefore recommended not to make variable names too verbose.

Loading history...
396
        $registrationCode,
397
        DocumentNumber $documentNumber,
398
        $identityVerified,
399
        SecondFactorTypeService $secondFactorTypeService
400
    ) {
401
        $this->assertNotForgotten();
402
403
        /** @var VettedSecondFactor|null $secondFactorWithHighestLoa */
404
        $secondFactorWithHighestLoa = $this->vettedSecondFactors->getSecondFactorWithHighestLoa($secondFactorTypeService);
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 122 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
405
        $registrantsSecondFactor = $registrant->getVerifiedSecondFactor($registrantsSecondFactorId);
406
407
        if ($registrantsSecondFactor === null) {
408
            throw new DomainException(
409
                sprintf('Registrant second factor with ID %s does not exist', $registrantsSecondFactorId)
410
            );
411
        }
412
413
        if (!$secondFactorWithHighestLoa->hasEqualOrHigherLoaComparedTo(
414
            $registrantsSecondFactor,
415
            $secondFactorTypeService
416
        )) {
417
            throw new DomainException("Authority does not have the required LoA to vet the registrant's second factor");
418
        }
419
420
        if (!$identityVerified) {
421
            throw new DomainException('Will not vet second factor when physical identity has not been verified.');
422
        }
423
424
        $registrant->complyWithVettingOfSecondFactor(
425
            $registrantsSecondFactorId,
426
            $registrantsSecondFactorType,
427
            $registrantsSecondFactorIdentifier,
428
            $registrationCode,
429
            $documentNumber
430
        );
431
    }
432
433
    public function complyWithVettingOfSecondFactor(
434
        SecondFactorId $secondFactorId,
435
        SecondFactorType $secondFactorType,
436
        SecondFactorIdentifier $secondFactorIdentifier,
437
        $registrationCode,
438
        DocumentNumber $documentNumber
439
    ) {
440
        $this->assertNotForgotten();
441
442
        $secondFactorToVet = null;
443
        foreach ($this->verifiedSecondFactors as $secondFactor) {
444
            /** @var VerifiedSecondFactor $secondFactor */
445
            if ($secondFactor->hasRegistrationCodeAndIdentifier($registrationCode, $secondFactorIdentifier)) {
446
                $secondFactorToVet = $secondFactor;
447
            }
448
        }
449
450
        if (!$secondFactorToVet) {
451
            throw new DomainException(
452
                'Cannot vet second factor, no verified second factor can be vetted using the given registration code ' .
453
                'and second factor identifier'
454
            );
455
        }
456
457
        if (!$secondFactorToVet->canBeVettedNow()) {
458
            throw new DomainException('Cannot vet second factor, the registration window is closed.');
459
        }
460
461
        $secondFactorToVet->vet($documentNumber);
462
    }
463
464
    public function revokeSecondFactor(SecondFactorId $secondFactorId)
465
    {
466
        $this->assertNotForgotten();
467
468
        /** @var UnverifiedSecondFactor|null $unverifiedSecondFactor */
469
        $unverifiedSecondFactor = $this->unverifiedSecondFactors->get((string) $secondFactorId);
470
        /** @var VerifiedSecondFactor|null $verifiedSecondFactor */
471
        $verifiedSecondFactor = $this->verifiedSecondFactors->get((string) $secondFactorId);
472
        /** @var VettedSecondFactor|null $vettedSecondFactor */
473
        $vettedSecondFactor = $this->vettedSecondFactors->get((string) $secondFactorId);
474
475
        if (!$unverifiedSecondFactor && !$verifiedSecondFactor && !$vettedSecondFactor) {
476
            throw new DomainException('Cannot revoke second factor: no second factor with given id exists.');
477
        }
478
479
        if ($unverifiedSecondFactor) {
480
            $unverifiedSecondFactor->revoke();
481
482
            return;
483
        }
484
485
        if ($verifiedSecondFactor) {
486
            $verifiedSecondFactor->revoke();
487
488
            return;
489
        }
490
491
        $vettedSecondFactor->revoke();
492
    }
493
494
    public function complyWithSecondFactorRevocation(SecondFactorId $secondFactorId, IdentityId $authorityId)
495
    {
496
        $this->assertNotForgotten();
497
498
        /** @var UnverifiedSecondFactor|null $unverifiedSecondFactor */
499
        $unverifiedSecondFactor = $this->unverifiedSecondFactors->get((string) $secondFactorId);
500
        /** @var VerifiedSecondFactor|null $verifiedSecondFactor */
501
        $verifiedSecondFactor = $this->verifiedSecondFactors->get((string) $secondFactorId);
502
        /** @var VettedSecondFactor|null $vettedSecondFactor */
503
        $vettedSecondFactor = $this->vettedSecondFactors->get((string) $secondFactorId);
504
505
        if (!$unverifiedSecondFactor && !$verifiedSecondFactor && !$vettedSecondFactor) {
506
            throw new DomainException('Cannot revoke second factor: no second factor with given id exists.');
507
        }
508
509
        if ($unverifiedSecondFactor) {
510
            $unverifiedSecondFactor->complyWithRevocation($authorityId);
511
512
            return;
513
        }
514
515
        if ($verifiedSecondFactor) {
516
            $verifiedSecondFactor->complyWithRevocation($authorityId);
517
518
            return;
519
        }
520
521
        $vettedSecondFactor->complyWithRevocation($authorityId);
522
    }
523
524
    /**
525
     * @param Institution               $institution
526
     * @param RegistrationAuthorityRole $role
527
     * @param Location                  $location
528
     * @param ContactInformation        $contactInformation
529
     * @return void
530
     */
531
    public function accreditWith(
532
        RegistrationAuthorityRole $role,
533
        Institution $institution,
534
        Location $location,
535
        ContactInformation $contactInformation
536
    ) {
537
        $this->assertNotForgotten();
538
539
        if (!$this->institution->equals($institution)) {
540
            throw new DomainException('An Identity may only be accredited within its own institution');
541
        }
542
543
        if (!$this->vettedSecondFactors->count()) {
544
            throw new DomainException(
545
                'An Identity must have at least one vetted second factor before it can be accredited'
546
            );
547
        }
548
549
        if ($this->registrationAuthority) {
550
            throw new DomainException('Cannot accredit Identity as it has already been accredited');
551
        }
552
553
        if ($role->equals(new RegistrationAuthorityRole(RegistrationAuthorityRole::ROLE_RA))) {
554
            $this->apply(new IdentityAccreditedAsRaEvent(
555
                $this->id,
556
                $this->nameId,
557
                $this->institution,
558
                $role,
559
                $location,
560
                $contactInformation
561
            ));
562 View Code Duplication
        } elseif ($role->equals(new RegistrationAuthorityRole(RegistrationAuthorityRole::ROLE_RAA))) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
563
            $this->apply(new IdentityAccreditedAsRaaEvent(
564
                $this->id,
565
                $this->nameId,
566
                $this->institution,
567
                $role,
568
                $location,
569
                $contactInformation
570
            ));
571
        } else {
572
            throw new DomainException('An Identity can only be accredited with either the RA or RAA role');
573
        }
574
    }
575
576
    public function amendRegistrationAuthorityInformation(Location $location, ContactInformation $contactInformation)
577
    {
578
        $this->assertNotForgotten();
579
580
        if (!$this->registrationAuthority) {
581
            throw new DomainException(
582
                'Cannot amend registration authority information: identity is not a registration authority'
583
            );
584
        }
585
586
        $this->apply(
587
            new RegistrationAuthorityInformationAmendedEvent(
588
                $this->id,
589
                $this->institution,
590
                $this->nameId,
591
                $location,
592
                $contactInformation
593
            )
594
        );
595
    }
596
597
    public function appointAs(RegistrationAuthorityRole $role)
598
    {
599
        $this->assertNotForgotten();
600
601
        if (!$this->registrationAuthority) {
602
            throw new DomainException(
603
                'Cannot appoint as different RegistrationAuthorityRole: identity is not a registration authority'
604
            );
605
        }
606
607
        if ($this->registrationAuthority->isAppointedAs($role)) {
608
            return;
609
        }
610
611
        if ($role->equals(new RegistrationAuthorityRole(RegistrationAuthorityRole::ROLE_RA))) {
612
            $this->apply(new AppointedAsRaEvent($this->id, $this->institution, $this->nameId));
613 View Code Duplication
        } elseif ($role->equals(new RegistrationAuthorityRole(RegistrationAuthorityRole::ROLE_RAA))) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
614
            $this->apply(new AppointedAsRaaEvent($this->id, $this->institution, $this->nameId));
615
        } else {
616
            throw new DomainException('An Identity can only be appointed as either RA or RAA');
617
        }
618
    }
619
620
    public function retractRegistrationAuthority()
621
    {
622
        $this->assertNotForgotten();
623
624
        if (!$this->registrationAuthority) {
625
            throw new DomainException(
626
                'Cannot Retract Registration Authority as the Identity is not a registration authority'
627
            );
628
        }
629
630
        $this->apply(new RegistrationAuthorityRetractedEvent(
631
            $this->id,
632
            $this->institution,
633
            $this->nameId,
634
            $this->commonName,
635
            $this->email
636
        ));
637
    }
638
639
    public function expressPreferredLocale(Locale $preferredLocale)
640
    {
641
        $this->assertNotForgotten();
642
643
        if ($this->preferredLocale === $preferredLocale) {
644
            return;
645
        }
646
647
        $this->apply(new LocalePreferenceExpressedEvent($this->id, $this->institution, $preferredLocale));
648
    }
649
650
    public function forget()
651
    {
652
        $this->assertNotForgotten();
653
654
        if ($this->registrationAuthority) {
655
            throw new DomainException('Cannot forget an identity that is currently accredited as an RA(A)');
656
        }
657
658
        $this->apply(new IdentityForgottenEvent($this->id, $this->institution));
659
    }
660
661
    protected function applyIdentityCreatedEvent(IdentityCreatedEvent $event)
662
    {
663
        $this->id                      = $event->identityId;
664
        $this->institution             = $event->identityInstitution;
665
        $this->nameId                  = $event->nameId;
666
        $this->commonName              = $event->commonName;
667
        $this->email                   = $event->email;
668
        $this->preferredLocale         = $event->preferredLocale;
669
        $this->forgotten               = false;
670
671
        $this->unverifiedSecondFactors = new SecondFactorCollection();
672
        $this->verifiedSecondFactors   = new SecondFactorCollection();
673
        $this->vettedSecondFactors     = new SecondFactorCollection();
674
    }
675
676
    public function applyIdentityRenamedEvent(IdentityRenamedEvent $event)
677
    {
678
        $this->commonName = $event->commonName;
679
    }
680
681
    public function applyIdentityEmailChangedEvent(IdentityEmailChangedEvent $event)
682
    {
683
        $this->email = $event->email;
684
    }
685
686
    protected function applyYubikeySecondFactorBootstrappedEvent(YubikeySecondFactorBootstrappedEvent $event)
687
    {
688
        $secondFactor = VettedSecondFactor::create(
689
            $event->secondFactorId,
690
            $this,
691
            new SecondFactorType('yubikey'),
692
            $event->yubikeyPublicId
693
        );
694
695
        $this->vettedSecondFactors->set((string) $secondFactor->getId(), $secondFactor);
696
    }
697
698 View Code Duplication
    protected function applyYubikeyPossessionProvenEvent(YubikeyPossessionProvenEvent $event)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
699
    {
700
        $secondFactorType = new SecondFactorType('yubikey');
701
702
        if ($event->emailVerificationRequired) {
703
            $secondFactor = UnverifiedSecondFactor::create(
704
                $event->secondFactorId,
705
                $this,
706
                $secondFactorType,
707
                $event->yubikeyPublicId,
708
                $event->emailVerificationWindow,
709
                $event->emailVerificationNonce
710
            );
711
712
            $this->unverifiedSecondFactors->set((string)$secondFactor->getId(), $secondFactor);
713
        } else {
714
            $secondFactor = VerifiedSecondFactor::create(
715
                $event->secondFactorId,
716
                $this,
717
                $secondFactorType,
718
                $event->yubikeyPublicId,
719
                DateTime::now(),
720
                OtpGenerator::generate(8)
721
            );
722
723
            $this->verifiedSecondFactors->set((string)$secondFactor->getId(), $secondFactor);
724
        }
725
    }
726
727 View Code Duplication
    protected function applyPhonePossessionProvenEvent(PhonePossessionProvenEvent $event)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
728
    {
729
        $secondFactorType = new SecondFactorType('sms');
730
731
        if ($event->emailVerificationRequired) {
732
            $secondFactor = UnverifiedSecondFactor::create(
733
                $event->secondFactorId,
734
                $this,
735
                $secondFactorType,
736
                $event->phoneNumber,
737
                $event->emailVerificationWindow,
738
                $event->emailVerificationNonce
739
            );
740
741
            $this->unverifiedSecondFactors->set((string)$secondFactor->getId(), $secondFactor);
742
        } else {
743
            $secondFactor = VerifiedSecondFactor::create(
744
                $event->secondFactorId,
745
                $this,
746
                $secondFactorType,
747
                $event->phoneNumber,
748
                DateTime::now(),
749
                OtpGenerator::generate(8)
750
            );
751
752
            $this->verifiedSecondFactors->set((string)$secondFactor->getId(), $secondFactor);
753
        }
754
    }
755
756 View Code Duplication
    protected function applyGssfPossessionProvenEvent(GssfPossessionProvenEvent $event)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
757
    {
758
        $secondFactorType = new SecondFactorType((string)$event->stepupProvider);
759
760
        if ($event->emailVerificationRequired) {
761
            $secondFactor = UnverifiedSecondFactor::create(
762
                $event->secondFactorId,
763
                $this,
764
                $secondFactorType,
765
                $event->gssfId,
766
                $event->emailVerificationWindow,
767
                $event->emailVerificationNonce
768
            );
769
770
            $this->unverifiedSecondFactors->set((string)$secondFactor->getId(), $secondFactor);
771
        } else {
772
            // WIP! Culprit here!
773
            //
774
            // In 2.7, we can't create the verified second factor here,
775
            // because that's done via the email verified event. Post 2.7, we
776
            // want to create it directly. But that's in conflict because this
777
            // breaks replay of old events.
778
            //
779
            // To solve this, we need to introduce a version on the event to
780
            // distinguish between version 2.7 and 2.8.
781
            $secondFactor = VerifiedSecondFactor::create(
782
                $event->secondFactorId,
783
                $this,
784
                $secondFactorType,
785
                $event->gssfId,
786
                DateTime::now(),
787
                OtpGenerator::generate(8)
788
            );
789
790
            $this->verifiedSecondFactors->set((string)$secondFactor->getId(), $secondFactor);
791
        }
792
    }
793
794 View Code Duplication
    protected function applyU2fDevicePossessionProvenEvent(U2fDevicePossessionProvenEvent $event)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
795
    {
796
        $secondFactorType = new SecondFactorType('u2f');
797
798
        if ($event->emailVerificationRequired) {
799
            $secondFactor = UnverifiedSecondFactor::create(
800
                $event->secondFactorId,
801
                $this,
802
                $secondFactorType,
803
                $event->keyHandle,
804
                $event->emailVerificationWindow,
805
                $event->emailVerificationNonce
806
            );
807
808
            $this->unverifiedSecondFactors->set((string)$secondFactor->getId(), $secondFactor);
809
        } else {
810
            $secondFactor = VerifiedSecondFactor::create(
811
                $event->secondFactorId,
812
                $this,
813
                $secondFactorType,
814
                $event->keyHandle,
815
                DateTime::now(),
816
                OtpGenerator::generate(8)
817
            );
818
819
            $this->verifiedSecondFactors->set((string)$secondFactor->getId(), $secondFactor);
820
        }
821
    }
822
823
    protected function applyEmailVerifiedEvent(EmailVerifiedEvent $event)
824
    {
825
        $secondFactorId = (string) $event->secondFactorId;
826
827
        /** @var UnverifiedSecondFactor $unverified */
828
        $unverified = $this->unverifiedSecondFactors->get($secondFactorId);
829
830
        // In version 2.7.x of middleware, institutions that do not require
831
        // email verification still transitioned from unverified to verified,
832
        // but having an EmailVerifiedEvent applied when handling a
833
        // "possession proven" event.
834
        //
835
        // Starting from 2.8.0, the proven event immediately creates a
836
        // verified second factor without triggering the "email verified"
837
        // event. The 2.7-style events must be supported in later versions of
838
        // Stepup because they are replayed by Broadway when revoking
839
        // tokens. To accomodate revocation of tokens created without email
840
        // verification in release 2.7, we check an unverified second factor
841
        // exists before removing it.
842
        //
843
        // So, "$unverified === null" occurs when email verification is
844
        // enabled, or when email verification was disabled in release 2.7.
845
        if ($unverified !== null) {
846
            $verified = $unverified->asVerified($event->registrationRequestedAt, $event->registrationCode);
847
848
            $this->unverifiedSecondFactors->remove($secondFactorId);
849
            $this->verifiedSecondFactors->set($secondFactorId, $verified);
850
        }
851
    }
852
853
    protected function applySecondFactorVettedEvent(SecondFactorVettedEvent $event)
854
    {
855
        $secondFactorId = (string) $event->secondFactorId;
856
857
        /** @var VerifiedSecondFactor $verified */
858
        $verified = $this->verifiedSecondFactors->get($secondFactorId);
859
        $vetted = $verified->asVetted();
860
861
        $this->verifiedSecondFactors->remove($secondFactorId);
862
        $this->vettedSecondFactors->set($secondFactorId, $vetted);
863
    }
864
865
    protected function applyUnverifiedSecondFactorRevokedEvent(UnverifiedSecondFactorRevokedEvent $event)
866
    {
867
        $this->unverifiedSecondFactors->remove((string) $event->secondFactorId);
868
    }
869
870
    protected function applyCompliedWithUnverifiedSecondFactorRevocationEvent(
871
        CompliedWithUnverifiedSecondFactorRevocationEvent $event
872
    ) {
873
        $this->unverifiedSecondFactors->remove((string) $event->secondFactorId);
874
    }
875
876
    protected function applyVerifiedSecondFactorRevokedEvent(VerifiedSecondFactorRevokedEvent $event)
877
    {
878
        $this->verifiedSecondFactors->remove((string) $event->secondFactorId);
879
    }
880
881
    protected function applyCompliedWithVerifiedSecondFactorRevocationEvent(
882
        CompliedWithVerifiedSecondFactorRevocationEvent $event
883
    ) {
884
        $this->verifiedSecondFactors->remove((string) $event->secondFactorId);
885
    }
886
887
    protected function applyVettedSecondFactorRevokedEvent(VettedSecondFactorRevokedEvent $event)
888
    {
889
        $this->vettedSecondFactors->remove((string) $event->secondFactorId);
890
    }
891
892
    protected function applyCompliedWithVettedSecondFactorRevocationEvent(
893
        CompliedWithVettedSecondFactorRevocationEvent $event
894
    ) {
895
        $this->vettedSecondFactors->remove((string) $event->secondFactorId);
896
    }
897
898
    protected function applyIdentityAccreditedAsRaEvent(IdentityAccreditedAsRaEvent $event)
899
    {
900
        $this->registrationAuthority = RegistrationAuthority::accreditWith(
901
            $event->registrationAuthorityRole,
902
            $event->location,
903
            $event->contactInformation
904
        );
905
    }
906
907
    protected function applyIdentityAccreditedAsRaaEvent(IdentityAccreditedAsRaaEvent $event)
908
    {
909
        $this->registrationAuthority = RegistrationAuthority::accreditWith(
910
            $event->registrationAuthorityRole,
911
            $event->location,
912
            $event->contactInformation
913
        );
914
    }
915
916
    protected function applyRegistrationAuthorityInformationAmendedEvent(
917
        RegistrationAuthorityInformationAmendedEvent $event
918
    ) {
919
        $this->registrationAuthority->amendInformation($event->location, $event->contactInformation);
920
    }
921
922
    protected function applyAppointedAsRaEvent(AppointedAsRaEvent $event)
0 ignored issues
show
Unused Code introduced by
The parameter $event is not used and could be removed.

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

Loading history...
923
    {
924
        $this->registrationAuthority->appointAs(new RegistrationAuthorityRole(RegistrationAuthorityRole::ROLE_RA));
925
    }
926
927
    protected function applyAppointedAsRaaEvent(AppointedAsRaaEvent $event)
0 ignored issues
show
Unused Code introduced by
The parameter $event is not used and could be removed.

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

Loading history...
928
    {
929
        $this->registrationAuthority->appointAs(new RegistrationAuthorityRole(RegistrationAuthorityRole::ROLE_RAA));
930
    }
931
932
    protected function applyRegistrationAuthorityRetractedEvent(RegistrationAuthorityRetractedEvent $event)
0 ignored issues
show
Unused Code introduced by
The parameter $event is not used and could be removed.

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

Loading history...
933
    {
934
        $this->registrationAuthority = null;
935
    }
936
937
    protected function applyLocalePreferenceExpressedEvent(LocalePreferenceExpressedEvent $event)
938
    {
939
        $this->preferredLocale = $event->preferredLocale;
940
    }
941
942
    protected function applyIdentityForgottenEvent(IdentityForgottenEvent $event)
0 ignored issues
show
Unused Code introduced by
The parameter $event is not used and could be removed.

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

Loading history...
943
    {
944
        $this->commonName = CommonName::unknown();
945
        $this->email      = Email::unknown();
946
        $this->forgotten  = true;
947
    }
948
949
    public function getAggregateRootId()
950
    {
951
        return $this->id;
952
    }
953
954
    protected function getChildEntities()
955
    {
956
        return array_merge(
957
            $this->unverifiedSecondFactors->getValues(),
958
            $this->verifiedSecondFactors->getValues(),
959
            $this->vettedSecondFactors->getValues()
960
        );
961
    }
962
963
    /**
964
     * @throws DomainException
965
     */
966
    private function assertNotForgotten()
967
    {
968
        if ($this->forgotten) {
969
            throw new DomainException('Operation on this Identity is not allowed: it has been forgotten');
970
        }
971
    }
972
973
    /**
974
     * @throws DomainException
975
     */
976
    private function assertUserMayAddSecondFactor()
977
    {
978
        if (count($this->unverifiedSecondFactors) +
979
            count($this->verifiedSecondFactors) +
980
            count($this->vettedSecondFactors) >= $this->maxNumberOfTokens
981
        ) {
982
            throw new DomainException(
983
                sprintf('User may not have more than %d token(s)', $this->maxNumberOfTokens)
984
            );
985
        }
986
    }
987
988
    public function getId()
989
    {
990
        return $this->id;
991
    }
992
993
    /**
994
     * @return NameId
995
     */
996
    public function getNameId()
997
    {
998
        return $this->nameId;
999
    }
1000
1001
    /**
1002
     * @return Institution
1003
     */
1004
    public function getInstitution()
1005
    {
1006
        return $this->institution;
1007
    }
1008
1009
    public function getCommonName()
1010
    {
1011
        return $this->commonName;
1012
    }
1013
1014
    public function getEmail()
1015
    {
1016
        return $this->email;
1017
    }
1018
1019
    public function getPreferredLocale()
1020
    {
1021
        return $this->preferredLocale;
1022
    }
1023
1024
    /**
1025
     * @param SecondFactorId $secondFactorId
1026
     * @return VerifiedSecondFactor|null
1027
     */
1028
    public function getVerifiedSecondFactor(SecondFactorId $secondFactorId)
1029
    {
1030
        return $this->verifiedSecondFactors->get((string) $secondFactorId);
1031
    }
1032
}
1033