From a96a5a75d5b54edc02d358881a8460c62557e594 Mon Sep 17 00:00:00 2001 From: Bas Strooband Date: Thu, 9 Jul 2020 14:09:45 +0200 Subject: [PATCH] Add optional prove possession --- config/legacy/parameters.yaml.dist | 6 + config/packages/events.yaml | 1 + config/services.yaml | 7 + docs/personal-data.md | 12 + docs/postman/3.json | 116 ++++--- .../SecondFactorProvePossessionHelper.php | 56 ++++ src/Surfnet/Stepup/Identity/Api/Identity.php | 14 +- .../Identity/Entity/VerifiedSecondFactor.php | 21 +- ...torVettedWithoutTokenProofOfPossession.php | 177 +++++++++++ src/Surfnet/Stepup/Identity/Identity.php | 37 ++- .../Identity/Event/ForgettableEventsTest.php | 1 + .../VerifiedSecondFactorController.php | 32 +- .../Identity/Entity/AuditLogEntry.php | 1 + .../Projector/RaSecondFactorProjector.php | 11 + .../Projector/SecondFactorProjector.php | 15 + .../Repository/AuditLogRepository.php | 1 + .../ApiBundle/Resources/config/routing.yml | 6 + .../Command/VetSecondFactorCommand.php | 7 + .../CommandHandler/IdentityCommandHandler.php | 51 +--- .../SecondFactorVettedEmailProcessor.php | 6 + .../Resources/config/command_handlers.yml | 1 + .../IdentityCommandHandlerTest.php | 282 ++++++++++++++++++ .../SecondFactorRevocationTest.php | 120 ++++++++ .../Projector/SecondFactorProjector.php | 16 + 24 files changed, 901 insertions(+), 96 deletions(-) create mode 100644 src/Surfnet/Stepup/Helper/SecondFactorProvePossessionHelper.php create mode 100644 src/Surfnet/Stepup/Identity/Event/SecondFactorVettedWithoutTokenProofOfPossession.php diff --git a/config/legacy/parameters.yaml.dist b/config/legacy/parameters.yaml.dist index 3fe631d91..9357cd1fb 100644 --- a/config/legacy/parameters.yaml.dist +++ b/config/legacy/parameters.yaml.dist @@ -65,3 +65,9 @@ parameters: # institution config (middleware api). The value configured in the parameters.yml will be used as the # fallback/default value. number_of_tokens_per_identity: 1 + + # Sets the tokens that can skip the prove possession step. + # + # This is the global, application wide default. The configuration consists of an array with second factors types + # that will skip the prove possession step in RA. + skip_prove_possession_second_factors: [] diff --git a/config/packages/events.yaml b/config/packages/events.yaml index b86ad8d42..9761a346f 100644 --- a/config/packages/events.yaml +++ b/config/packages/events.yaml @@ -38,6 +38,7 @@ parameters: - Surfnet\Stepup\Identity\Event\InstitutionsRemovedFromWhitelistEvent - Surfnet\Stepup\Identity\Event\U2fDevicePossessionProvenEvent - Surfnet\Stepup\Identity\Event\SecondFactorVettedEvent + - Surfnet\Stepup\Identity\Event\SecondFactorVettedWithoutTokenProofOfPossession - Surfnet\Stepup\Identity\Event\VerifiedSecondFactorRevokedEvent - Surfnet\Stepup\Identity\Event\WhitelistCreatedEvent - Surfnet\Stepup\Identity\Event\UnverifiedSecondFactorRevokedEvent diff --git a/config/services.yaml b/config/services.yaml index 8c2287ec0..6226b9e58 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -2,3 +2,10 @@ imports: - { resource: 'legacy/bundles.yaml' } - { resource: 'legacy/parameters.yaml' } + +services: + + Surfnet\Stepup\Helper\SecondFactorProvePossessionHelper: + arguments: + - "@surfnet_stepup.service.second_factor_type" + - '%skip_prove_possession_second_factors%' \ No newline at end of file diff --git a/docs/personal-data.md b/docs/personal-data.md index 020e46c65..bd1f38cdb 100644 --- a/docs/personal-data.md +++ b/docs/personal-data.md @@ -215,6 +215,18 @@ A list of all the [Identity events]((../src/Surfnet/Stepup/Identity/Event/) in s - Forgettable: secondFactorIdentifier - Forgettable: documentNumber +[SecondFactorVettedWithoutTokenProofOfPossession](../src/Surfnet/Stepup/Identity/Event/SecondFactorVettedEvent.php) +- identity_id +- name_id +- identity_institution +- second_factor_id +- second_factor_type +- preferred_locale +- Forgettable: email +- Forgettable: commonName +- Forgettable: secondFactorIdentifier +- Forgettable: documentNumber + [U2fDevicePossessionProvenAndVerifiedEvent](../src/Surfnet/Stepup/Identity/Event/U2fDevicePossessionProvenAndVerifiedEvent.php) - identity_id - identity_institution diff --git a/docs/postman/3.json b/docs/postman/3.json index 5c4df63a4..91f330b42 100644 --- a/docs/postman/3.json +++ b/docs/postman/3.json @@ -30,7 +30,7 @@ "raw": "{\n \"command\": {\n \"name\": \"Identity:CreateIdentity\",\n \"uuid\": \"4783486c-5f9d-4d5e-8ad1-721272280595\",\n \"payload\": {\n \"id\": \"dd98c118-0f48-401b-9013-69be3a19e571\",\n \"name_id\": \"urn:collab:person:institution-b.example.com:joe-b1\",\n \"institution\": \"institution-b.example.com\",\n \"email\": \"joe+joe-b1@stepup.example.com\",\n \"common_name\": \"joe-b1 institution-b.example.com\",\n \"preferred_locale \": \"nl_NL\"\n }\n },\n \"meta\": {\n \"actor_id\": null,\n \"actor_institution\": null\n }\n}" }, "url": { - "raw": "http://middleware.stepup.example.com/app_dev.php/command?XDEBUG_SESSION_START=PHPSTORM", + "raw": "http://middleware.stepup.example.com/command?XDEBUG_SESSION_START=PHPSTORM", "protocol": "http", "host": [ "middleware", @@ -39,7 +39,6 @@ "com" ], "path": [ - "app_dev.php", "command" ], "query": [ @@ -75,7 +74,7 @@ "raw": "" }, "url": { - "raw": "http://middleware.stepup.example.com/app_dev.php/identity?institution=stepup.example.com&NameId=urn:collab:person:stepup.example.com:admin&XDEBUG_SESSION_START=PHPSTORM", + "raw": "http://middleware.stepup.example.com/identity?institution=stepup.example.com&NameId=urn:collab:person:stepup.example.com:admin&XDEBUG_SESSION_START=PHPSTORM", "protocol": "http", "host": [ "middleware", @@ -84,7 +83,6 @@ "com" ], "path": [ - "app_dev.php", "identity" ], "query": [ @@ -125,7 +123,7 @@ "raw": "" }, "url": { - "raw": "http://middleware.stepup.example.com/app_dev.php/identity/db9b8bdf-720c-44ba-a4c4-154953e45f14", + "raw": "http://middleware.stepup.example.com/identity/db9b8bdf-720c-44ba-a4c4-154953e45f14", "protocol": "http", "host": [ "middleware", @@ -134,7 +132,6 @@ "com" ], "path": [ - "app_dev.php", "identity", "db9b8bdf-720c-44ba-a4c4-154953e45f14" ] @@ -166,7 +163,7 @@ "raw": "" }, "url": { - "raw": "http://mw-dev.stepup.coin.surf.net/app_dev.php/unverified-second-factors?identityId=8b5cdd14-74b1-43a2-a806-c171728b1bf1", + "raw": "http://middleware.stepup.example.com/unverified-second-factors?identityId=8b5cdd14-74b1-43a2-a806-c171728b1bf1", "protocol": "http", "host": [ "mw-dev", @@ -176,7 +173,6 @@ "net" ], "path": [ - "app_dev.php", "unverified-second-factors" ], "query": [ @@ -213,7 +209,7 @@ "raw": "" }, "url": { - "raw": "http://mw-dev.stepup.coin.surf.net/app_dev.php/unverified-second-factors/4984057f-5952-4a82-a77f-44bc9cd62ce4", + "raw": "http://middleware.stepup.example.com/unverified-second-factor/4984057f-5952-4a82-a77f-44bc9cd62ce4", "protocol": "http", "host": [ "mw-dev", @@ -223,8 +219,7 @@ "net" ], "path": [ - "app_dev.php", - "unverified-second-factors", + "unverified-second-factor", "4984057f-5952-4a82-a77f-44bc9cd62ce4" ] }, @@ -232,6 +227,48 @@ }, "response": [] }, + { + "name": "/unverified-second-factor/{secondFactorId}/skip-prove-possession", + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Basic c3M6YmFy" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "http://middleware.stepup.example.com/unverified-second-factor/4984057f-5952-4a82-a77f-44bc9cd62ce4", + "protocol": "http", + "host": [ + "mw-dev", + "stepup", + "coin", + "surf", + "net" + ], + "path": [ + "unverified-second-factors", + "4984057f-5952-4a82-a77f-44bc9cd62ce4", + "skip-prove-possession" + ] + }, + "description": "- `secondFactorId` UUIDv4 of the second factor" + }, + "response": [] + }, { "name": "/verified-second-factors", "protocolProfileBehavior": { @@ -255,7 +292,7 @@ ], "body": {}, "url": { - "raw": "https://middleware.stepup.example.com/app_dev.php/verified-second-factors?actorInstitution=institution-a.example.com&actorId=62096060-8b60-4bb1-bcc1-e00158a4051b&XDEBUG_SESSION_START=PHPSTORM", + "raw": "https://middleware.stepup.example.com/verified-second-factors?actorInstitution=institution-a.example.com&actorId=62096060-8b60-4bb1-bcc1-e00158a4051b&XDEBUG_SESSION_START=PHPSTORM", "protocol": "https", "host": [ "middleware", @@ -264,7 +301,6 @@ "com" ], "path": [ - "app_dev.php", "verified-second-factors" ], "query": [ @@ -309,7 +345,7 @@ "raw": "" }, "url": { - "raw": "http://middleware.stepup.example.com/app_dev.php/verified-second-factor/4984057f-5952-4a82-a77f-44bc9cd62ce4", + "raw": "http://middleware.stepup.example.com/verified-second-factor/4984057f-5952-4a82-a77f-44bc9cd62ce4", "protocol": "http", "host": [ "middleware", @@ -318,7 +354,6 @@ "com" ], "path": [ - "app_dev.php", "verified-second-factor", "4984057f-5952-4a82-a77f-44bc9cd62ce4" ] @@ -350,7 +385,7 @@ "raw": "" }, "url": { - "raw": "http://middleware.stepup.example.com/app_dev.php/vetted-second-factors?registrationCode=TEST&XDEBUG_SESSION_START=PHPSTORM", + "raw": "http://middleware.stepup.example.com/vetted-second-factors?registrationCode=TEST&XDEBUG_SESSION_START=PHPSTORM", "protocol": "http", "host": [ "middleware", @@ -359,7 +394,6 @@ "com" ], "path": [ - "app_dev.php", "vetted-second-factors" ], "query": [ @@ -400,7 +434,7 @@ "raw": "" }, "url": { - "raw": "http://middleware.stepup.example.com/app_dev.php/vetted-second-factor/c732d0ac-9f61-4ae1-924e-40d5172fca86", + "raw": "http://middleware.stepup.example.com/vetted-second-factor/c732d0ac-9f61-4ae1-924e-40d5172fca86", "protocol": "http", "host": [ "middleware", @@ -409,7 +443,6 @@ "com" ], "path": [ - "app_dev.php", "vetted-second-factor", "c732d0ac-9f61-4ae1-924e-40d5172fca86" ] @@ -441,7 +474,7 @@ "raw": "" }, "url": { - "raw": "http://middleware.stepup.example.com/app_dev.php/vetted-second-factor/c732d0ac-9f61-4ae1-924e-40d5172fca86", + "raw": "http://middleware.stepup.example.com/vetted-second-factor/c732d0ac-9f61-4ae1-924e-40d5172fca86", "protocol": "http", "host": [ "middleware", @@ -450,7 +483,6 @@ "com" ], "path": [ - "app_dev.php", "vetted-second-factor", "c732d0ac-9f61-4ae1-924e-40d5172fca86" ] @@ -478,7 +510,7 @@ "raw": "" }, "url": { - "raw": "http://middleware.stepup.example.com/app_dev.php/sraa/3858f62230ac3c915f300c664312c63f", + "raw": "http://middleware.stepup.example.com/sraa/3858f62230ac3c915f300c664312c63f", "protocol": "http", "host": [ "middleware", @@ -487,7 +519,6 @@ "com" ], "path": [ - "app_dev.php", "sraa", "3858f62230ac3c915f300c664312c63f" ] @@ -515,7 +546,7 @@ "raw": "" }, "url": { - "raw": "http://middleware.stepup.example.com/app_dev.php/registration-authority?institution=institution-a.example.com&XDEBUG_SESSION_START=PHPSTORM", + "raw": "http://middleware.stepup.example.com/registration-authority?institution=institution-a.example.com&XDEBUG_SESSION_START=PHPSTORM", "protocol": "http", "host": [ "middleware", @@ -524,7 +555,6 @@ "com" ], "path": [ - "app_dev.php", "registration-authority" ], "query": [ @@ -561,7 +591,7 @@ "raw": "" }, "url": { - "raw": "http://middleware.stepup.example.com/app_dev.php/registration-authority/41f8aded-e21f-43f1-a4e3-e5aa84c1f82e", + "raw": "http://middleware.stepup.example.com/registration-authority/41f8aded-e21f-43f1-a4e3-e5aa84c1f82e", "protocol": "http", "host": [ "middleware", @@ -570,7 +600,6 @@ "com" ], "path": [ - "app_dev.php", "registration-authority", "41f8aded-e21f-43f1-a4e3-e5aa84c1f82e" ] @@ -602,7 +631,7 @@ "raw": "" }, "url": { - "raw": "http://middleware.stepup.example.com/app_dev.php/ra-listing?actorId=b954e4a3-1ed4-489b-9996-f22512d31eef&actorInstitution=institution-a.example.com&institution=institution-a.example.com&orderBy=commonName&p=1&XDEBUG_SESSION_START=PHPSTORM", + "raw": "http://middleware.stepup.example.com/ra-listing?actorId=b954e4a3-1ed4-489b-9996-f22512d31eef&actorInstitution=institution-a.example.com&institution=institution-a.example.com&orderBy=commonName&p=1&XDEBUG_SESSION_START=PHPSTORM", "protocol": "http", "host": [ "middleware", @@ -611,7 +640,6 @@ "com" ], "path": [ - "app_dev.php", "ra-listing" ], "query": [ @@ -664,7 +692,7 @@ ], "body": {}, "url": { - "raw": "http://middleware.stepup.example.com/app_dev.php/ra-listing/62096060-8b60-4bb1-bcc1-e00158a4051b/stepup.example.com?actorId=62096060-8b60-4bb1-bcc1-e00158a4051b&actorInstitution=institution-a.example.com&XDEBUG_SESSION_START=PHPSTORM", + "raw": "http://middleware.stepup.example.com/ra-listing/62096060-8b60-4bb1-bcc1-e00158a4051b/stepup.example.com?actorId=62096060-8b60-4bb1-bcc1-e00158a4051b&actorInstitution=institution-a.example.com&XDEBUG_SESSION_START=PHPSTORM", "protocol": "http", "host": [ "middleware", @@ -673,7 +701,6 @@ "com" ], "path": [ - "app_dev.php", "ra-listing", "62096060-8b60-4bb1-bcc1-e00158a4051b", "stepup.example.com" @@ -720,7 +747,7 @@ ], "body": {}, "url": { - "raw": "http://middleware.stepup.example.com/app_dev.php/ra-candidate?actorId=b954e4a3-1ed4-489b-9996-f22512d31eef&actorInstitution=institution-a.example.com&secondFactorTypes%5B2%5D=yubikey&secondFactorTypes%5B3%5D=u2f&p=1&XDEBUG_SESSION_START=PHPSTORM", + "raw": "http://middleware.stepup.example.com/ra-candidate?actorId=b954e4a3-1ed4-489b-9996-f22512d31eef&actorInstitution=institution-a.example.com&secondFactorTypes%5B2%5D=yubikey&secondFactorTypes%5B3%5D=u2f&p=1&XDEBUG_SESSION_START=PHPSTORM", "protocol": "http", "host": [ "middleware", @@ -729,7 +756,6 @@ "com" ], "path": [ - "app_dev.php", "ra-candidate" ], "query": [ @@ -782,7 +808,7 @@ ], "body": {}, "url": { - "raw": "http://middleware.stepup.example.com/app_dev.php/ra-candidate/11db6fa9-33a7-413a-8b16-6d27ffe94486/Institution-f.example.com?actorId=b954e4a3-1ed4-489b-9996-f22512d31eef&XDEBUG_SESSION_START=PHPSTORM", + "raw": "http://middleware.stepup.example.com/ra-candidate/11db6fa9-33a7-413a-8b16-6d27ffe94486/Institution-f.example.com?actorId=b954e4a3-1ed4-489b-9996-f22512d31eef&XDEBUG_SESSION_START=PHPSTORM", "protocol": "http", "host": [ "middleware", @@ -791,7 +817,6 @@ "com" ], "path": [ - "app_dev.php", "ra-candidate", "11db6fa9-33a7-413a-8b16-6d27ffe94486", "Institution-f.example.com" @@ -830,7 +855,7 @@ "raw": "" }, "url": { - "raw": "http://middleware.stepup.example.com/app_dev.php/institution-listing", + "raw": "http://middleware.stepup.example.com/institution-listing", "protocol": "http", "host": [ "middleware", @@ -839,7 +864,6 @@ "com" ], "path": [ - "app_dev.php", "institution-listing" ] }, @@ -866,7 +890,7 @@ "raw": "" }, "url": { - "raw": "http://middleware.stepup.example.com/app_dev.php/audit-log/second-factors?institution=Ibuildings&identityId=8b5cdd14-74b1-43a2-a806-c171728b1bf1&orderBy=recordedOn&orderDirection=asc", + "raw": "http://middleware.stepup.example.com/audit-log/second-factors?institution=Ibuildings&identityId=8b5cdd14-74b1-43a2-a806-c171728b1bf1&orderBy=recordedOn&orderDirection=asc", "protocol": "http", "host": [ "middleware", @@ -875,7 +899,6 @@ "com" ], "path": [ - "app_dev.php", "audit-log", "second-factors" ], @@ -925,7 +948,7 @@ "raw": "" }, "url": { - "raw": "http://mw-dev.stepup.coin.surf.net/app_dev.php/ra-location?institution=Ibuildings", + "raw": "http://middleware.stepup.example.com/ra-location?institution=Ibuildings", "protocol": "http", "host": [ "mw-dev", @@ -935,7 +958,6 @@ "net" ], "path": [ - "app_dev.php", "ra-location" ], "query": [ @@ -972,7 +994,7 @@ "raw": "" }, "url": { - "raw": "http://mw-dev.stepup.coin.surf.net/app_dev.php/ra-location/123e4567-e89b-12d3-a456-426655440000", + "raw": "http://middleware.stepup.example.com/ra-location/123e4567-e89b-12d3-a456-426655440000", "protocol": "http", "host": [ "mw-dev", @@ -982,7 +1004,6 @@ "net" ], "path": [ - "app_dev.php", "ra-location", "123e4567-e89b-12d3-a456-426655440000" ] @@ -1014,7 +1035,7 @@ ], "body": {}, "url": { - "raw": "http://middleware.stepup.example.com/app_dev.php/institution-configuration-options/stepup.example.com", + "raw": "http://middleware.stepup.example.com/institution-configuration-options/stepup.example.com", "protocol": "http", "host": [ "middleware", @@ -1023,7 +1044,6 @@ "com" ], "path": [ - "app_dev.php", "institution-configuration-options", "stepup.example.com" ] @@ -1051,7 +1071,7 @@ "raw": "" }, "url": { - "raw": "http://middleware.stepup.example.com/app_dev.php/ra-second-factors?actorInstitution=institution-a.example.com&actorId=62096060-8b60-4bb1-bcc1-e00158a4051b&p=1&XDEBUG_SESSION_START=PHPSTORM", + "raw": "http://middleware.stepup.example.com/ra-second-factors?actorInstitution=institution-a.example.com&actorId=62096060-8b60-4bb1-bcc1-e00158a4051b&p=1&XDEBUG_SESSION_START=PHPSTORM", "protocol": "http", "host": [ "middleware", @@ -1060,7 +1080,6 @@ "com" ], "path": [ - "app_dev.php", "ra-second-factors" ], "query": [ @@ -1105,7 +1124,7 @@ "raw": "" }, "url": { - "raw": "http://middleware.stepup.example.com/app_dev.php/ra-second-factors-export?actorInstitution=institution-a.example.com&actorId=62096060-8b60-4bb1-bcc1-e00158a4051b&p=1&XDEBUG_SESSION_START=PHPSTORM", + "raw": "http://middleware.stepup.example.com/ra-second-factors-export?actorInstitution=institution-a.example.com&actorId=62096060-8b60-4bb1-bcc1-e00158a4051b&p=1&XDEBUG_SESSION_START=PHPSTORM", "protocol": "http", "host": [ "middleware", @@ -1114,7 +1133,6 @@ "com" ], "path": [ - "app_dev.php", "ra-second-factors-export" ], "query": [ diff --git a/src/Surfnet/Stepup/Helper/SecondFactorProvePossessionHelper.php b/src/Surfnet/Stepup/Helper/SecondFactorProvePossessionHelper.php new file mode 100644 index 000000000..945414320 --- /dev/null +++ b/src/Surfnet/Stepup/Helper/SecondFactorProvePossessionHelper.php @@ -0,0 +1,56 @@ +getAvailableSecondFactorTypes(), + 'Unsupported second factor type configured to skip prove possession' + ); + + $this->skipProvePossessionSecondFactorTypes = $skipProvePossessionSecondFactorTypes; + } + + /** + * @param SecondFactorType $secondFactorType + * @return bool + */ + public function canSkipProvePossession(SecondFactorType $secondFactorType) + { + return in_array($secondFactorType->getSecondFactorType(), $this->skipProvePossessionSecondFactorTypes); + } +} diff --git a/src/Surfnet/Stepup/Identity/Api/Identity.php b/src/Surfnet/Stepup/Identity/Api/Identity.php index bde8c2e48..9cfbc6ca3 100644 --- a/src/Surfnet/Stepup/Identity/Api/Identity.php +++ b/src/Surfnet/Stepup/Identity/Api/Identity.php @@ -21,7 +21,7 @@ use Broadway\Domain\AggregateRoot; use Surfnet\Stepup\Configuration\InstitutionConfiguration; use Surfnet\Stepup\Exception\DomainException; -use Surfnet\Stepup\Identity\Collection\InstitutionCollection; +use Surfnet\Stepup\Helper\SecondFactorProvePossessionHelper; use Surfnet\Stepup\Identity\Entity\VerifiedSecondFactor; use Surfnet\Stepup\Identity\Value\CommonName; use Surfnet\Stepup\Identity\Value\ContactInformation; @@ -166,7 +166,11 @@ public function verifyEmail($verificationNonce); * @param DocumentNumber $documentNumber * @param bool $identityVerified * @param SecondFactorTypeService $secondFactorTypeService + * @param SecondFactorProvePossessionHelper $secondFactorProvePossessionHelper + * @param bool $provePossessionSkipped * @return void + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function vetSecondFactor( Identity $registrant, @@ -176,7 +180,9 @@ public function vetSecondFactor( $registrationCode, DocumentNumber $documentNumber, $identityVerified, - SecondFactorTypeService $secondFactorTypeService + SecondFactorTypeService $secondFactorTypeService, + SecondFactorProvePossessionHelper $secondFactorProvePossessionHelper, + $provePossessionSkipped ); /** @@ -187,6 +193,7 @@ public function vetSecondFactor( * @param SecondFactorIdentifier $secondFactorIdentifier * @param string $registrationCode * @param DocumentNumber $documentNumber + * @param bool $provePossessionSkipped * @throws DomainException * @return void */ @@ -195,7 +202,8 @@ public function complyWithVettingOfSecondFactor( SecondFactorType $secondFactorType, SecondFactorIdentifier $secondFactorIdentifier, $registrationCode, - DocumentNumber $documentNumber + DocumentNumber $documentNumber, + $provePossessionSkipped ); /** diff --git a/src/Surfnet/Stepup/Identity/Entity/VerifiedSecondFactor.php b/src/Surfnet/Stepup/Identity/Entity/VerifiedSecondFactor.php index e50296a41..3e7709d96 100644 --- a/src/Surfnet/Stepup/Identity/Entity/VerifiedSecondFactor.php +++ b/src/Surfnet/Stepup/Identity/Entity/VerifiedSecondFactor.php @@ -24,6 +24,7 @@ use Surfnet\Stepup\Identity\Api\Identity; use Surfnet\Stepup\Identity\Event\CompliedWithVerifiedSecondFactorRevocationEvent; use Surfnet\Stepup\Identity\Event\IdentityForgottenEvent; +use Surfnet\Stepup\Identity\Event\SecondFactorVettedWithoutTokenProofOfPossession; use Surfnet\Stepup\Identity\Event\SecondFactorVettedEvent; use Surfnet\Stepup\Identity\Event\VerifiedSecondFactorRevokedEvent; use Surfnet\Stepup\Identity\Value\DocumentNumber; @@ -138,8 +139,26 @@ public function canBeVettedNow() ); } - public function vet(DocumentNumber $documentNumber) + public function vet(DocumentNumber $documentNumber, $provePossessionSkipped) { + if ($provePossessionSkipped) { + $this->apply( + new SecondFactorVettedWithoutTokenProofOfPossession( + $this->identity->getId(), + $this->identity->getNameId(), + $this->identity->getInstitution(), + $this->id, + $this->type, + $this->secondFactorIdentifier, + $documentNumber, + $this->identity->getCommonName(), + $this->identity->getEmail(), + $this->identity->getPreferredLocale() + ) + ); + return; + } + $this->apply( new SecondFactorVettedEvent( $this->identity->getId(), diff --git a/src/Surfnet/Stepup/Identity/Event/SecondFactorVettedWithoutTokenProofOfPossession.php b/src/Surfnet/Stepup/Identity/Event/SecondFactorVettedWithoutTokenProofOfPossession.php new file mode 100644 index 000000000..a93df7cb8 --- /dev/null +++ b/src/Surfnet/Stepup/Identity/Event/SecondFactorVettedWithoutTokenProofOfPossession.php @@ -0,0 +1,177 @@ +nameId = $nameId; + $this->secondFactorId = $secondFactorId; + $this->secondFactorType = $secondFactorType; + $this->secondFactorIdentifier = $secondFactorIdentifier; + $this->documentNumber = $documentNumber; + $this->commonName = $commonName; + $this->email = $email; + $this->preferredLocale = $preferredLocale; + } + + public function getAuditLogMetadata() + { + $metadata = new Metadata(); + $metadata->identityId = $this->identityId; + $metadata->identityInstitution = $this->identityInstitution; + $metadata->secondFactorId = $this->secondFactorId; + $metadata->secondFactorType = $this->secondFactorType; + $metadata->secondFactorIdentifier = $this->secondFactorIdentifier; + + return $metadata; + } + + public static function deserialize(array $data) + { + $secondFactorType = new SecondFactorType($data['second_factor_type']); + + return new self( + new IdentityId($data['identity_id']), + new NameId($data['name_id']), + new Institution($data['identity_institution']), + new SecondFactorId($data['second_factor_id']), + $secondFactorType, + SecondFactorIdentifierFactory::unknownForType($secondFactorType), + DocumentNumber::unknown(), + CommonName::unknown(), + Email::unknown(), + new Locale($data['preferred_locale']) + ); + } + + public function serialize(): array + { + return [ + 'identity_id' => (string) $this->identityId, + 'name_id' => (string) $this->nameId, + 'identity_institution' => (string) $this->identityInstitution, + 'second_factor_id' => (string) $this->secondFactorId, + 'second_factor_type' => (string) $this->secondFactorType, + 'preferred_locale' => (string) $this->preferredLocale, + ]; + } + + public function getSensitiveData() + { + return (new SensitiveData) + ->withCommonName($this->commonName) + ->withEmail($this->email) + ->withSecondFactorIdentifier($this->secondFactorIdentifier, $this->secondFactorType) + ->withDocumentNumber($this->documentNumber); + } + + public function setSensitiveData(SensitiveData $sensitiveData) + { + $this->email = $sensitiveData->getEmail(); + $this->commonName = $sensitiveData->getCommonName(); + $this->secondFactorIdentifier = $sensitiveData->getSecondFactorIdentifier(); + $this->documentNumber = $sensitiveData->getDocumentNumber(); + } +} diff --git a/src/Surfnet/Stepup/Identity/Identity.php b/src/Surfnet/Stepup/Identity/Identity.php index a8618c624..a336424f8 100644 --- a/src/Surfnet/Stepup/Identity/Identity.php +++ b/src/Surfnet/Stepup/Identity/Identity.php @@ -23,6 +23,7 @@ use Surfnet\Stepup\Configuration\Value\Institution as ConfigurationInstitution; use Surfnet\Stepup\DateTime\DateTime; use Surfnet\Stepup\Exception\DomainException; +use Surfnet\Stepup\Helper\SecondFactorProvePossessionHelper; use Surfnet\Stepup\Identity\Api\Identity as IdentityApi; use Surfnet\Stepup\Identity\Entity\RegistrationAuthority; use Surfnet\Stepup\Identity\Entity\RegistrationAuthorityCollection; @@ -55,6 +56,7 @@ use Surfnet\Stepup\Identity\Event\RegistrationAuthorityInformationAmendedForInstitutionEvent; use Surfnet\Stepup\Identity\Event\RegistrationAuthorityRetractedEvent; use Surfnet\Stepup\Identity\Event\RegistrationAuthorityRetractedForInstitutionEvent; +use Surfnet\Stepup\Identity\Event\SecondFactorVettedWithoutTokenProofOfPossession; use Surfnet\Stepup\Identity\Event\SecondFactorVettedEvent; use Surfnet\Stepup\Identity\Event\U2fDevicePossessionProvenAndVerifiedEvent; use Surfnet\Stepup\Identity\Event\U2fDevicePossessionProvenEvent; @@ -427,6 +429,9 @@ public function verifyEmail($verificationNonce) $secondFactorToVerify->verifyEmail(); } + /** + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ public function vetSecondFactor( IdentityApi $registrant, SecondFactorId $registrantsSecondFactorId, @@ -435,7 +440,9 @@ public function vetSecondFactor( $registrationCode, DocumentNumber $documentNumber, $identityVerified, - SecondFactorTypeService $secondFactorTypeService + SecondFactorTypeService $secondFactorTypeService, + SecondFactorProvePossessionHelper $secondFactorProvePossessionHelper, + $provePossessionSkipped ) { $this->assertNotForgotten(); @@ -470,12 +477,21 @@ public function vetSecondFactor( throw new DomainException('Will not vet second factor when physical identity has not been verified.'); } + if ($provePossessionSkipped && !$secondFactorProvePossessionHelper->canSkipProvePossession($registrantsSecondFactorType)) { + throw new DomainException(sprintf( + "The possession of registrants second factor with ID '%s' of type '%s' has to be physically proven", + $registrantsSecondFactorId, + $registrantsSecondFactorType->getSecondFactorType() + )); + } + $registrant->complyWithVettingOfSecondFactor( $registrantsSecondFactorId, $registrantsSecondFactorType, $registrantsSecondFactorIdentifier, $registrationCode, - $documentNumber + $documentNumber, + $provePossessionSkipped ); } @@ -484,7 +500,8 @@ public function complyWithVettingOfSecondFactor( SecondFactorType $secondFactorType, SecondFactorIdentifier $secondFactorIdentifier, $registrationCode, - DocumentNumber $documentNumber + DocumentNumber $documentNumber, + $provePossessionSkipped ) { $this->assertNotForgotten(); @@ -507,7 +524,7 @@ public function complyWithVettingOfSecondFactor( throw new DomainException('Cannot vet second factor, the registration window is closed.'); } - $secondFactorToVet->vet($documentNumber); + $secondFactorToVet->vet($documentNumber, $provePossessionSkipped); } public function revokeSecondFactor(SecondFactorId $secondFactorId) @@ -917,6 +934,18 @@ protected function applySecondFactorVettedEvent(SecondFactorVettedEvent $event) $this->vettedSecondFactors->set($secondFactorId, $vetted); } + protected function applySecondFactorVettedWithoutTokenProofOfPossession(SecondFactorVettedWithoutTokenProofOfPossession $event) + { + $secondFactorId = (string)$event->secondFactorId; + + /** @var VerifiedSecondFactor $verified */ + $verified = $this->verifiedSecondFactors->get($secondFactorId); + $vetted = $verified->asVetted(); + + $this->verifiedSecondFactors->remove($secondFactorId); + $this->vettedSecondFactors->set($secondFactorId, $vetted); + } + protected function applyUnverifiedSecondFactorRevokedEvent(UnverifiedSecondFactorRevokedEvent $event) { $this->unverifiedSecondFactors->remove((string)$event->secondFactorId); diff --git a/src/Surfnet/Stepup/Tests/Identity/Event/ForgettableEventsTest.php b/src/Surfnet/Stepup/Tests/Identity/Event/ForgettableEventsTest.php index f968af3ed..05495ba80 100644 --- a/src/Surfnet/Stepup/Tests/Identity/Event/ForgettableEventsTest.php +++ b/src/Surfnet/Stepup/Tests/Identity/Event/ForgettableEventsTest.php @@ -44,6 +44,7 @@ public function certain_events_are_forgettable_events_and_others_are_not() 'Surfnet\Stepup\Identity\Event\RegistrationAuthorityRetractedEvent', 'Surfnet\Stepup\Identity\Event\SecondFactorRevokedEvent', 'Surfnet\Stepup\Identity\Event\SecondFactorVettedEvent', + 'Surfnet\Stepup\Identity\Event\SecondFactorVettedWithoutTokenProofOfPossession', 'Surfnet\Stepup\Identity\Event\U2fDevicePossessionProvenEvent', 'Surfnet\Stepup\Identity\Event\U2fDevicePossessionProvenAndVerifiedEvent', 'Surfnet\Stepup\Identity\Event\UnverifiedSecondFactorRevokedEvent', diff --git a/src/Surfnet/StepupMiddleware/ApiBundle/Controller/VerifiedSecondFactorController.php b/src/Surfnet/StepupMiddleware/ApiBundle/Controller/VerifiedSecondFactorController.php index bec08cdf5..c5c31f77c 100644 --- a/src/Surfnet/StepupMiddleware/ApiBundle/Controller/VerifiedSecondFactorController.php +++ b/src/Surfnet/StepupMiddleware/ApiBundle/Controller/VerifiedSecondFactorController.php @@ -19,8 +19,10 @@ namespace Surfnet\StepupMiddleware\ApiBundle\Controller; use Surfnet\Stepup\Configuration\Value\InstitutionRole; +use Surfnet\Stepup\Helper\SecondFactorProvePossessionHelper; use Surfnet\Stepup\Identity\Value\IdentityId; use Surfnet\Stepup\Identity\Value\SecondFactorId; +use Surfnet\StepupBundle\Value\SecondFactorType; use Surfnet\StepupMiddleware\ApiBundle\Authorization\Service\AuthorizationContextService; use Surfnet\StepupMiddleware\ApiBundle\Identity\Query\VerifiedSecondFactorOfIdentityQuery; use Surfnet\StepupMiddleware\ApiBundle\Identity\Query\VerifiedSecondFactorQuery; @@ -31,6 +33,9 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class VerifiedSecondFactorController extends Controller { /** @@ -43,12 +48,20 @@ class VerifiedSecondFactorController extends Controller */ private $institutionAuthorizationService; + /** + * @var SecondFactorProvePossessionHelper + */ + private $secondFactorProvePossessionHelper; + + public function __construct( SecondFactorService $secondFactorService, - AuthorizationContextService $authorizationService + AuthorizationContextService $authorizationService, + SecondFactorProvePossessionHelper $secondFactorProvePossessionHelper ) { $this->secondFactorService = $secondFactorService; $this->institutionAuthorizationService = $authorizationService; + $this->secondFactorProvePossessionHelper = $secondFactorProvePossessionHelper; } public function getAction($id) @@ -104,4 +117,21 @@ public function collectionOfIdentityAction(Request $request) return JsonCollectionResponse::fromPaginator($paginator); } + + public function getCanSkipProvePossession($id) + { + $this->denyAccessUnlessGranted(['ROLE_RA']); + + $secondFactor = $this->secondFactorService->findVerified(new SecondFactorId($id)); + + if ($secondFactor === null) { + throw new NotFoundHttpException(sprintf("Verified second factor '%s' does not exist", $id)); + } + + $secondFactorType = new SecondFactorType($secondFactor->type); + + $skipVetting = $this->secondFactorProvePossessionHelper->canSkipProvePossession($secondFactorType); + + return new JsonResponse($skipVetting); + } } diff --git a/src/Surfnet/StepupMiddleware/ApiBundle/Identity/Entity/AuditLogEntry.php b/src/Surfnet/StepupMiddleware/ApiBundle/Identity/Entity/AuditLogEntry.php index acedf0767..01f58335b 100644 --- a/src/Surfnet/StepupMiddleware/ApiBundle/Identity/Entity/AuditLogEntry.php +++ b/src/Surfnet/StepupMiddleware/ApiBundle/Identity/Entity/AuditLogEntry.php @@ -57,6 +57,7 @@ class AuditLogEntry implements JsonSerializable 'Surfnet\Stepup\Identity\Event\PhonePossessionProvenEvent' => 'possession_proven', 'Surfnet\Stepup\Identity\Event\PhonePossessionProvenAndVerifiedEvent' => 'possession_proven', 'Surfnet\Stepup\Identity\Event\SecondFactorVettedEvent' => 'vetted', + 'Surfnet\Stepup\Identity\Event\SecondFactorVettedWithoutTokenProofOfPossession' => 'vetted_possession_unknown', 'Surfnet\Stepup\Identity\Event\UnverifiedSecondFactorRevokedEvent' => 'revoked', 'Surfnet\Stepup\Identity\Event\VerifiedSecondFactorRevokedEvent' => 'revoked', 'Surfnet\Stepup\Identity\Event\VettedSecondFactorRevokedEvent' => 'revoked', diff --git a/src/Surfnet/StepupMiddleware/ApiBundle/Identity/Projector/RaSecondFactorProjector.php b/src/Surfnet/StepupMiddleware/ApiBundle/Identity/Projector/RaSecondFactorProjector.php index 904bd8d61..55b83e31d 100644 --- a/src/Surfnet/StepupMiddleware/ApiBundle/Identity/Projector/RaSecondFactorProjector.php +++ b/src/Surfnet/StepupMiddleware/ApiBundle/Identity/Projector/RaSecondFactorProjector.php @@ -30,6 +30,7 @@ use Surfnet\Stepup\Identity\Event\IdentityRenamedEvent; use Surfnet\Stepup\Identity\Event\PhonePossessionProvenAndVerifiedEvent; use Surfnet\Stepup\Identity\Event\PhonePossessionProvenEvent; +use Surfnet\Stepup\Identity\Event\SecondFactorVettedWithoutTokenProofOfPossession; use Surfnet\Stepup\Identity\Event\SecondFactorVettedEvent; use Surfnet\Stepup\Identity\Event\U2fDevicePossessionProvenAndVerifiedEvent; use Surfnet\Stepup\Identity\Event\U2fDevicePossessionProvenEvent; @@ -267,6 +268,16 @@ public function applySecondFactorVettedEvent(SecondFactorVettedEvent $event) $this->raSecondFactorRepository->save($secondFactor); } + public function applySecondFactorVettedWithoutTokenProofOfPossession(SecondFactorVettedWithoutTokenProofOfPossession $event) + { + $secondFactor = $this->raSecondFactorRepository->find((string) $event->secondFactorId); + + $secondFactor->documentNumber = $event->documentNumber; + $secondFactor->status = SecondFactorStatus::vetted(); + + $this->raSecondFactorRepository->save($secondFactor); + } + protected function applyUnverifiedSecondFactorRevokedEvent(UnverifiedSecondFactorRevokedEvent $event) { $this->updateStatus($event->secondFactorId, SecondFactorStatus::revoked()); diff --git a/src/Surfnet/StepupMiddleware/ApiBundle/Identity/Projector/SecondFactorProjector.php b/src/Surfnet/StepupMiddleware/ApiBundle/Identity/Projector/SecondFactorProjector.php index 6f804c082..b0c6604d8 100644 --- a/src/Surfnet/StepupMiddleware/ApiBundle/Identity/Projector/SecondFactorProjector.php +++ b/src/Surfnet/StepupMiddleware/ApiBundle/Identity/Projector/SecondFactorProjector.php @@ -28,6 +28,7 @@ use Surfnet\Stepup\Identity\Event\IdentityForgottenEvent; use Surfnet\Stepup\Identity\Event\PhonePossessionProvenAndVerifiedEvent; use Surfnet\Stepup\Identity\Event\PhonePossessionProvenEvent; +use Surfnet\Stepup\Identity\Event\SecondFactorVettedWithoutTokenProofOfPossession; use Surfnet\Stepup\Identity\Event\SecondFactorVettedEvent; use Surfnet\Stepup\Identity\Event\U2fDevicePossessionProvenAndVerifiedEvent; use Surfnet\Stepup\Identity\Event\U2fDevicePossessionProvenEvent; @@ -235,6 +236,20 @@ public function applySecondFactorVettedEvent(SecondFactorVettedEvent $event) $this->verifiedRepository->remove($verified); } + public function applySecondFactorVettedWithoutTokenProofOfPossession(SecondFactorVettedWithoutTokenProofOfPossession $event) + { + $verified = $this->verifiedRepository->find($event->secondFactorId->getSecondFactorId()); + + $vetted = new VettedSecondFactor(); + $vetted->id = $event->secondFactorId->getSecondFactorId(); + $vetted->identityId = $event->identityId->getIdentityId(); + $vetted->type = $event->secondFactorType->getSecondFactorType(); + $vetted->secondFactorIdentifier = $event->secondFactorIdentifier->getValue(); + + $this->vettedRepository->save($vetted); + $this->verifiedRepository->remove($verified); + } + protected function applyUnverifiedSecondFactorRevokedEvent(UnverifiedSecondFactorRevokedEvent $event) { $this->unverifiedRepository->remove($this->unverifiedRepository->find($event->secondFactorId->getSecondFactorId())); diff --git a/src/Surfnet/StepupMiddleware/ApiBundle/Identity/Repository/AuditLogRepository.php b/src/Surfnet/StepupMiddleware/ApiBundle/Identity/Repository/AuditLogRepository.php index 36c58e5ed..99235ae0b 100644 --- a/src/Surfnet/StepupMiddleware/ApiBundle/Identity/Repository/AuditLogRepository.php +++ b/src/Surfnet/StepupMiddleware/ApiBundle/Identity/Repository/AuditLogRepository.php @@ -47,6 +47,7 @@ public function __construct(ManagerRegistry $registry) 'Surfnet\Stepup\Identity\Event\PhonePossessionProvenAndVerifiedEvent', 'Surfnet\Stepup\Identity\Event\EmailVerifiedEvent', 'Surfnet\Stepup\Identity\Event\SecondFactorVettedEvent', + 'Surfnet\Stepup\Identity\Event\SecondFactorVettedWithoutTokenProofOfPossession', 'Surfnet\Stepup\Identity\Event\UnverifiedSecondFactorRevokedEvent', 'Surfnet\Stepup\Identity\Event\VerifiedSecondFactorRevokedEvent', 'Surfnet\Stepup\Identity\Event\VettedSecondFactorRevokedEvent', diff --git a/src/Surfnet/StepupMiddleware/ApiBundle/Resources/config/routing.yml b/src/Surfnet/StepupMiddleware/ApiBundle/Resources/config/routing.yml index d2e2be7d3..ccfc09934 100644 --- a/src/Surfnet/StepupMiddleware/ApiBundle/Resources/config/routing.yml +++ b/src/Surfnet/StepupMiddleware/ApiBundle/Resources/config/routing.yml @@ -62,6 +62,12 @@ verified_second_factor: methods: [GET] condition: "request.headers.get('Accept') matches '/^application\\\\/json($|[;,])/'" +verified_second_factor_can_skip_prove_posession: + path: /verified-second-factor/{id}/skip-prove-possession + defaults: { _controller: SurfnetStepupMiddlewareApiBundle:VerifiedSecondFactor:getCanSkipProvePossession } + methods: [GET] + condition: "request.headers.get('Accept') matches '/^application\\\\/json($|[;,])/'" + vetted_second_factor: path: /vetted-second-factor/{id} defaults: { _controller: SurfnetStepupMiddlewareApiBundle:VettedSecondFactor:get } diff --git a/src/Surfnet/StepupMiddleware/CommandHandlingBundle/Identity/Command/VetSecondFactorCommand.php b/src/Surfnet/StepupMiddleware/CommandHandlingBundle/Identity/Command/VetSecondFactorCommand.php index 52269eb22..0b1aa5a12 100644 --- a/src/Surfnet/StepupMiddleware/CommandHandlingBundle/Identity/Command/VetSecondFactorCommand.php +++ b/src/Surfnet/StepupMiddleware/CommandHandlingBundle/Identity/Command/VetSecondFactorCommand.php @@ -89,6 +89,13 @@ class VetSecondFactorCommand extends AbstractCommand implements RaExecutable */ public $identityVerified; + /** + * @Assert\Type(type="bool") + * + * @var boolean + */ + public $provePossessionSkipped; + /** * @inheritDoc */ diff --git a/src/Surfnet/StepupMiddleware/CommandHandlingBundle/Identity/CommandHandler/IdentityCommandHandler.php b/src/Surfnet/StepupMiddleware/CommandHandlingBundle/Identity/CommandHandler/IdentityCommandHandler.php index 4fda8fe48..fc14c0163 100644 --- a/src/Surfnet/StepupMiddleware/CommandHandlingBundle/Identity/CommandHandler/IdentityCommandHandler.php +++ b/src/Surfnet/StepupMiddleware/CommandHandlingBundle/Identity/CommandHandler/IdentityCommandHandler.php @@ -19,12 +19,10 @@ namespace Surfnet\StepupMiddleware\CommandHandlingBundle\Identity\CommandHandler; use Broadway\CommandHandling\SimpleCommandHandler; -use Broadway\Repository\AggregateNotFoundException; use Broadway\Repository\Repository as RepositoryInterface; use Surfnet\Stepup\Configuration\EventSourcing\InstitutionConfigurationRepository; -use Surfnet\Stepup\Configuration\InstitutionConfiguration; use Surfnet\Stepup\Configuration\Value\Institution as ConfigurationInstitution; -use Surfnet\Stepup\Configuration\Value\InstitutionConfigurationId; +use Surfnet\Stepup\Helper\SecondFactorProvePossessionHelper; use Surfnet\Stepup\Identity\Api\Identity as IdentityApi; use Surfnet\Stepup\Identity\Entity\ConfigurableSettings; use Surfnet\Stepup\Identity\Identity; @@ -102,6 +100,10 @@ class IdentityCommandHandler extends SimpleCommandHandler * @var InstitutionConfigurationRepository */ private $institutionConfigurationRepository; + /** + * @var SecondFactorProvePossessionHelper + */ + private $provePossessionHelper; /** * @param RepositoryInterface $eventSourcedRepository @@ -109,6 +111,7 @@ class IdentityCommandHandler extends SimpleCommandHandler * @param ConfigurableSettings $configurableSettings * @param AllowedSecondFactorListService $allowedSecondFactorListService * @param SecondFactorTypeService $secondFactorTypeService + * @param SecondFactorProvePossessionHelper $provePossessionHelper * @param InstitutionConfigurationOptionsService $institutionConfigurationOptionsService * @param InstitutionConfigurationRepository $institutionConfigurationRepository */ @@ -118,6 +121,7 @@ public function __construct( ConfigurableSettings $configurableSettings, AllowedSecondFactorListService $allowedSecondFactorListService, SecondFactorTypeService $secondFactorTypeService, + SecondFactorProvePossessionHelper $provePossessionHelper, InstitutionConfigurationOptionsService $institutionConfigurationOptionsService, InstitutionConfigurationRepository $institutionConfigurationRepository ) { @@ -126,6 +130,7 @@ public function __construct( $this->configurableSettings = $configurableSettings; $this->allowedSecondFactorListService = $allowedSecondFactorListService; $this->secondFactorTypeService = $secondFactorTypeService; + $this->provePossessionHelper = $provePossessionHelper; $this->institutionConfigurationOptionsService = $institutionConfigurationOptionsService; $this->institutionConfigurationRepository = $institutionConfigurationRepository; } @@ -135,18 +140,13 @@ public function handleCreateIdentityCommand(CreateIdentityCommand $command) $preferredLocale = new Locale($command->preferredLocale); $this->assertIsValidLocale($preferredLocale); - $institution = new Institution($command->institution); - - $institutionConfiguration = $this->loadInstitutionConfigurationFor($institution); - $identity = Identity::create( new IdentityId($command->id), new Institution($command->institution), new NameId($command->nameId), new CommonName($command->commonName), new Email($command->email), - $preferredLocale, - $institutionConfiguration + $preferredLocale ); $this->eventSourcedRepository->save($identity); @@ -176,16 +176,13 @@ public function handleBootstrapIdentityWithYubikeySecondFactorCommand( throw DuplicateIdentityException::forBootstrappingWithYubikeySecondFactor($nameId, $institution); } - $institutionConfiguration = $this->loadInstitutionConfigurationFor($institution); - $identity = Identity::create( new IdentityId($command->identityId), $institution, $nameId, new CommonName($command->commonName), new Email($command->email), - $preferredLocale, - $institutionConfiguration + $preferredLocale ); $configurationInstitution = new ConfigurationInstitution( @@ -341,7 +338,9 @@ public function handleVetSecondFactorCommand(VetSecondFactorCommand $command) $command->registrationCode, new DocumentNumber($command->documentNumber), $command->identityVerified, - $this->secondFactorTypeService + $this->secondFactorTypeService, + $this->provePossessionHelper, + $command->provePossessionSkipped ); $this->eventSourcedRepository->save($authority); @@ -427,28 +426,4 @@ private function emailVerificationIsRequired(IdentityApi $identity) return $configuration->verifyEmailOption->isEnabled(); } - - /** - * @deprecated Should be used until existing institution configurations have been migrated to using normalized ids - * - * @param Institution $institution - * @return InstitutionConfiguration - */ - private function loadInstitutionConfigurationFor(Institution $institution) - { - $institution = new ConfigurationInstitution($institution->getInstitution()); - try { - $institutionConfigurationId = InstitutionConfigurationId::normalizedFrom($institution); - $institutionConfiguration = $this->institutionConfigurationRepository->load( - $institutionConfigurationId->getInstitutionConfigurationId() - ); - } catch (AggregateNotFoundException $exception) { - $institutionConfigurationId = InstitutionConfigurationId::from($institution); - $institutionConfiguration = $this->institutionConfigurationRepository->load( - $institutionConfigurationId->getInstitutionConfigurationId() - ); - } - - return $institutionConfiguration; - } } diff --git a/src/Surfnet/StepupMiddleware/CommandHandlingBundle/Processor/SecondFactorVettedEmailProcessor.php b/src/Surfnet/StepupMiddleware/CommandHandlingBundle/Processor/SecondFactorVettedEmailProcessor.php index ba4a3257e..3b7be414e 100644 --- a/src/Surfnet/StepupMiddleware/CommandHandlingBundle/Processor/SecondFactorVettedEmailProcessor.php +++ b/src/Surfnet/StepupMiddleware/CommandHandlingBundle/Processor/SecondFactorVettedEmailProcessor.php @@ -19,6 +19,7 @@ namespace Surfnet\StepupMiddleware\CommandHandlingBundle\Processor; use Broadway\Processor\Processor; +use Surfnet\Stepup\Identity\Event\SecondFactorVettedWithoutTokenProofOfPossession; use Surfnet\Stepup\Identity\Event\SecondFactorVettedEvent; use Surfnet\StepupMiddleware\CommandHandlingBundle\Identity\Service\SecondFactorVettedMailService; @@ -38,4 +39,9 @@ public function handleSecondFactorVettedEvent(SecondFactorVettedEvent $event) { $this->secondFactorVettedMailService->sendVettedEmail($event->preferredLocale, $event->commonName, $event->email); } + + public function handleSecondFactorVettedWithoutTokenProofOfPossession(SecondFactorVettedWithoutTokenProofOfPossession $event) + { + $this->secondFactorVettedMailService->sendVettedEmail($event->preferredLocale, $event->commonName, $event->email); + } } diff --git a/src/Surfnet/StepupMiddleware/CommandHandlingBundle/Resources/config/command_handlers.yml b/src/Surfnet/StepupMiddleware/CommandHandlingBundle/Resources/config/command_handlers.yml index c77ac0b62..726d9a985 100644 --- a/src/Surfnet/StepupMiddleware/CommandHandlingBundle/Resources/config/command_handlers.yml +++ b/src/Surfnet/StepupMiddleware/CommandHandlingBundle/Resources/config/command_handlers.yml @@ -7,6 +7,7 @@ services: - "@identity.entity.configurable_settings" - "@surfnet_stepup_middleware_api.service.allowed_second_factor_list" - "@surfnet_stepup.service.second_factor_type" + - '@Surfnet\Stepup\Helper\SecondFactorProvePossessionHelper' - "@surfnet_stepup_middleware_api.service.institution_configuration_options" - "@surfnet_stepup.repository.institution_configuration" tags: [{ name: command_bus.command_handler }] diff --git a/src/Surfnet/StepupMiddleware/CommandHandlingBundle/Tests/Identity/CommandHandler/IdentityCommandHandlerTest.php b/src/Surfnet/StepupMiddleware/CommandHandlingBundle/Tests/Identity/CommandHandler/IdentityCommandHandlerTest.php index c4ba18d94..b633f64ab 100644 --- a/src/Surfnet/StepupMiddleware/CommandHandlingBundle/Tests/Identity/CommandHandler/IdentityCommandHandlerTest.php +++ b/src/Surfnet/StepupMiddleware/CommandHandlingBundle/Tests/Identity/CommandHandler/IdentityCommandHandlerTest.php @@ -23,11 +23,13 @@ use Broadway\EventSourcing\AggregateFactory\PublicConstructorAggregateFactory; use Broadway\EventStore\EventStore as EventStoreInterface; use DateTime as CoreDateTime; +use Hamcrest\Matchers; use Mockery as m; use Surfnet\Stepup\Configuration\EventSourcing\InstitutionConfigurationRepository; use Surfnet\Stepup\Configuration\InstitutionConfiguration; use Surfnet\Stepup\Configuration\Value\AllowedSecondFactorList; use Surfnet\Stepup\DateTime\DateTime; +use Surfnet\Stepup\Helper\SecondFactorProvePossessionHelper; use Surfnet\Stepup\Identity\Entity\ConfigurableSettings; use Surfnet\Stepup\Identity\Event\EmailVerifiedEvent; use Surfnet\Stepup\Identity\Event\GssfPossessionProvenEvent; @@ -36,6 +38,7 @@ use Surfnet\Stepup\Identity\Event\IdentityRenamedEvent; use Surfnet\Stepup\Identity\Event\LocalePreferenceExpressedEvent; use Surfnet\Stepup\Identity\Event\PhonePossessionProvenEvent; +use Surfnet\Stepup\Identity\Event\SecondFactorVettedWithoutTokenProofOfPossession; use Surfnet\Stepup\Identity\Event\SecondFactorVettedEvent; use Surfnet\Stepup\Identity\Event\U2fDevicePossessionProvenEvent; use Surfnet\Stepup\Identity\Event\YubikeyPossessionProvenEvent; @@ -99,6 +102,11 @@ class IdentityCommandHandlerTest extends CommandHandlerTest */ private $secondFactorTypeService; + /** + * @var SecondFactorProvePossessionHelper|m\MockInterface + */ + private $secondFactorProvePossessionHelper; + /** * @var InstitutionConfigurationOptionsService $configService */ @@ -130,6 +138,8 @@ protected function createCommandHandler(EventStoreInterface $eventStore, EventBu $this->identityProjectionRepository = m::mock(IdentityProjectionRepository::class); $this->secondFactorTypeService = m::mock(SecondFactorTypeService::class); $this->secondFactorTypeService->shouldIgnoreMissing(); + $this->secondFactorProvePossessionHelper = m::mock(SecondFactorProvePossessionHelper::class); + $this->secondFactorTypeService->shouldIgnoreMissing(); $this->configService = m::mock(InstitutionConfigurationOptionsService::class); $this->configService->shouldIgnoreMissing(); @@ -147,6 +157,7 @@ protected function createCommandHandler(EventStoreInterface $eventStore, EventBu ConfigurableSettings::create(self::$window, ['nl_NL', 'en_GB']), $this->allowedSecondFactorListServiceMock, $this->secondFactorTypeService, + $this->secondFactorProvePossessionHelper, $this->configService, $this->institutionConfigurationRepositoryMock ); @@ -1189,6 +1200,7 @@ public function a_second_factor_can_be_vetted() $command->secondFactorIdentifier = '00028278'; $command->documentNumber = 'NH9392'; $command->identityVerified = true; + $command->provePossessionSkipped = false; $authorityId = new IdentityId($command->authorityId); $authorityNameId = new NameId($this->uuid()); @@ -1206,6 +1218,11 @@ public function a_second_factor_can_be_vetted() $this->secondFactorTypeService->shouldReceive('hasEqualOrLowerLoaComparedTo')->andReturn(true); + $secondFactorType = new SecondFactorType($command->secondFactorType); + $this->secondFactorProvePossessionHelper->shouldReceive('canSkipProvePossession') + ->with(Matchers::equalTo($secondFactorType)) + ->andReturn(false); + $this->scenario ->withAggregateId($authorityId) ->given([ @@ -1426,6 +1443,271 @@ public function a_second_factor_cannot_be_vetted_without_a_secure_enough_vetted_ ]); } + + /** + * @test + * @group command-handler + */ + public function a_second_factor_can_be_vetted_without_a_physical_proven_possession() + { + $command = new VetSecondFactorCommand(); + $command->authorityId = 'AID'; + $command->identityId = 'IID'; + $command->secondFactorId = 'ISFID'; + $command->registrationCode = 'REGCODE'; + $command->secondFactorType = 'yubikey'; + $command->secondFactorIdentifier = '00028278'; + $command->documentNumber = 'NH9392'; + $command->identityVerified = true; + $command->provePossessionSkipped = true; + + $authorityId = new IdentityId($command->authorityId); + $authorityNameId = new NameId($this->uuid()); + $authorityInstitution = new Institution('Wazoo'); + $authorityEmail = new Email('info@domain.invalid'); + $authorityCommonName = new CommonName('Henk Westbroek'); + + $registrantId = new IdentityId($command->identityId); + $registrantInstitution = new Institution('A Corp.'); + $registrantNameId = new NameId('3'); + $registrantEmail = new Email('reg@domain.invalid'); + $registrantCommonName = new CommonName('Reginald Waterloo'); + $registrantSecFacId = new SecondFactorId('ISFID'); + $registrantSecFacIdentifier = new YubikeyPublicId('00028278'); + + $this->secondFactorTypeService->shouldReceive('hasEqualOrLowerLoaComparedTo')->andReturn(true); + + $secondFactorType = new SecondFactorType($command->secondFactorType); + $this->secondFactorProvePossessionHelper->shouldReceive('canSkipProvePossession') + ->with(Matchers::equalTo($secondFactorType)) + ->andReturn(true); + + $this->scenario + ->withAggregateId($authorityId) + ->given([ + new IdentityCreatedEvent( + $authorityId, + $authorityInstitution, + $authorityNameId, + $authorityCommonName, + $authorityEmail, + new Locale('en_GB') + ), + new YubikeySecondFactorBootstrappedEvent( + $authorityId, + $authorityNameId, + $authorityInstitution, + $authorityCommonName, + $authorityEmail, + new Locale('en_GB'), + new SecondFactorId($this->uuid()), + new YubikeyPublicId('00000012') + ) + ]) + ->withAggregateId($registrantId) + ->given([ + new IdentityCreatedEvent( + $registrantId, + $registrantInstitution, + $registrantNameId, + $registrantCommonName, + $registrantEmail, + new Locale('en_GB') + ), + new YubikeyPossessionProvenEvent( + $registrantId, + $registrantInstitution, + $registrantSecFacId, + $registrantSecFacIdentifier, + true, + EmailVerificationWindow::createFromTimeFrameStartingAt( + TimeFrame::ofSeconds(static::$window), + DateTime::now() + ), + 'nonce', + $registrantCommonName, + $registrantEmail, + new Locale('en_GB') + ), + new EmailVerifiedEvent( + $registrantId, + $registrantInstitution, + $registrantSecFacId, + new SecondFactorType('yubikey'), + $registrantSecFacIdentifier, + DateTime::now(), + 'REGCODE', + $registrantCommonName, + $registrantEmail, + new Locale('en_GB') + ), + ]) + ->when($command) + ->then([ + new SecondFactorVettedWithoutTokenProofOfPossession( + $registrantId, + $registrantNameId, + $registrantInstitution, + $registrantSecFacId, + new SecondFactorType('yubikey'), + new YubikeyPublicId('00028278'), + new DocumentNumber('NH9392'), + $registrantCommonName, + $registrantEmail, + new Locale('en_GB') + ), + ]); + } + + /** + * @test + * @group command-handler + */ + public function a_second_factor_cannot_be_vetted_without_physical_prove_of_possession_when_not_configured() + { + $this->expectExceptionMessage("The possession of registrants second factor with ID 'ISFID' of type 'yubikey' has to be physically proven"); + $this->expectException(\Surfnet\Stepup\Exception\DomainException::class); + + $command = new VetSecondFactorCommand(); + $command->authorityId = 'AID'; + $command->identityId = 'IID'; + $command->secondFactorId = 'ISFID'; + $command->registrationCode = 'REGCODE'; + $command->secondFactorType = 'yubikey'; + $command->secondFactorIdentifier = '00028278'; + $command->documentNumber = 'NH9392'; + $command->identityVerified = true; + $command->provePossessionSkipped = true; + + $authorityId = new IdentityId($command->authorityId); + $authorityInstitution = new Institution('Wazoo'); + $authorityNameId = new NameId($this->uuid()); + $authorityEmail = new Email('info@domain.invalid'); + $authorityCommonName = new CommonName('Henk Westbroek'); + $authorityPhoneSfId = new SecondFactorId($this->uuid()); + $authorityPhoneNo = new PhoneNumber('+31 (0) 612345678'); + + $registrantId = new IdentityId($command->identityId); + $registrantInstitution = new Institution('A Corp.'); + $registrantNameId = new NameId('3'); + $registrantEmail = new Email('reg@domain.invalid'); + $registrantCommonName = new CommonName('Reginald Waterloo'); + $registrantSecFacId = new SecondFactorId('ISFID'); + $registrantPubId = new YubikeyPublicId('00028278'); + + $this->secondFactorTypeService->shouldReceive('hasEqualOrLowerLoaComparedTo')->andReturn(true); + + $secondFactorType = new SecondFactorType($command->secondFactorType); + $this->secondFactorProvePossessionHelper->shouldReceive('canSkipProvePossession') + ->with(Matchers::equalTo($secondFactorType)) + ->andReturn(false); + + $this->scenario + ->withAggregateId($authorityId) + ->given([ + new IdentityCreatedEvent( + $authorityId, + $authorityInstitution, + $authorityNameId, + $authorityCommonName, + $authorityEmail, + new Locale('en_GB') + ), + new PhonePossessionProvenEvent( + $authorityId, + $authorityInstitution, + $authorityPhoneSfId, + $authorityPhoneNo, + true, + EmailVerificationWindow::createFromTimeFrameStartingAt( + TimeFrame::ofSeconds(static::$window), + DateTime::now() + ), + 'nonce', + $authorityCommonName, + $authorityEmail, + new Locale('en_GB') + ), + new EmailVerifiedEvent( + $authorityId, + $authorityInstitution, + $authorityPhoneSfId, + new SecondFactorType('sms'), + $authorityPhoneNo, + DateTime::now(), + 'regcode', + $authorityCommonName, + $authorityEmail, + new Locale('en_GB') + ), + new SecondFactorVettedEvent( + $authorityId, + $authorityNameId, + $authorityInstitution, + $authorityPhoneSfId, + new SecondFactorType('sms'), + $authorityPhoneNo, + new DocumentNumber('NG-RB-81'), + $authorityCommonName, + $authorityEmail, + new Locale('en_GB') + ) + ]) + ->withAggregateId($registrantId) + ->given([ + new IdentityCreatedEvent( + $registrantId, + $registrantInstitution, + $registrantNameId, + $registrantCommonName, + $registrantEmail, + new Locale('en_GB') + ), + new YubikeyPossessionProvenEvent( + $registrantId, + $registrantInstitution, + $registrantSecFacId, + $registrantPubId, + true, + EmailVerificationWindow::createFromTimeFrameStartingAt( + TimeFrame::ofSeconds(static::$window), + DateTime::now() + ), + 'nonce', + $registrantCommonName, + $registrantEmail, + new Locale('en_GB') + ), + new EmailVerifiedEvent( + $registrantId, + $registrantInstitution, + $registrantSecFacId, + new SecondFactorType('yubikey'), + $registrantPubId, + DateTime::now(), + 'REGCODE', + $registrantCommonName, + $registrantEmail, + new Locale('en_GB') + ), + ]) + ->when($command) + ->then([ + new SecondFactorVettedEvent( + $registrantId, + $registrantNameId, + $registrantInstitution, + $registrantSecFacId, + new SecondFactorType('yubikey'), + new YubikeyPublicId('00028278'), + new DocumentNumber('NH9392'), + $registrantCommonName, + $registrantEmail, + new Locale('en_GB') + ), + ]); + } + /** * @test * @group command-handler diff --git a/src/Surfnet/StepupMiddleware/CommandHandlingBundle/Tests/Identity/CommandHandler/SecondFactorRevocationTest.php b/src/Surfnet/StepupMiddleware/CommandHandlingBundle/Tests/Identity/CommandHandler/SecondFactorRevocationTest.php index 8ad34f69a..494dfd310 100644 --- a/src/Surfnet/StepupMiddleware/CommandHandlingBundle/Tests/Identity/CommandHandler/SecondFactorRevocationTest.php +++ b/src/Surfnet/StepupMiddleware/CommandHandlingBundle/Tests/Identity/CommandHandler/SecondFactorRevocationTest.php @@ -25,12 +25,14 @@ use Mockery as m; use Surfnet\Stepup\Configuration\EventSourcing\InstitutionConfigurationRepository; use Surfnet\Stepup\DateTime\DateTime; +use Surfnet\Stepup\Helper\SecondFactorProvePossessionHelper; use Surfnet\Stepup\Identity\Entity\ConfigurableSettings; use Surfnet\Stepup\Identity\Event\CompliedWithUnverifiedSecondFactorRevocationEvent; use Surfnet\Stepup\Identity\Event\CompliedWithVerifiedSecondFactorRevocationEvent; use Surfnet\Stepup\Identity\Event\CompliedWithVettedSecondFactorRevocationEvent; use Surfnet\Stepup\Identity\Event\EmailVerifiedEvent; use Surfnet\Stepup\Identity\Event\IdentityCreatedEvent; +use Surfnet\Stepup\Identity\Event\SecondFactorVettedWithoutTokenProofOfPossession; use Surfnet\Stepup\Identity\Event\SecondFactorVettedEvent; use Surfnet\Stepup\Identity\Event\U2fDevicePossessionProvenEvent; use Surfnet\Stepup\Identity\Event\UnverifiedSecondFactorRevokedEvent; @@ -83,6 +85,7 @@ protected function createCommandHandler(EventStoreInterface $eventStore, EventBu ConfigurableSettings::create(self::$window, []), m::mock(AllowedSecondFactorListService::class), m::mock(SecondFactorTypeService::class)->shouldIgnoreMissing(), + m::mock(SecondFactorProvePossessionHelper::class)->shouldIgnoreMissing(), m::mock(InstitutionConfigurationOptionsService::class)->shouldIgnoreMissing(), m::mock(InstitutionConfigurationRepository::class) ); @@ -600,6 +603,123 @@ public function a_registration_authority_can_revoke_a_vetted_second_factor() + /** + * @test + * @group command-handler + */ + public function a_registration_authority_can_revoke_a_possession_proved_skipped_vetted_second_factor() + { + $command = new RevokeRegistrantsSecondFactorCommand(); + $command->authorityId = static::uuid(); + $command->identityId = static::uuid(); + $command->secondFactorId = static::uuid(); + + $authorityId = new IdentityId($command->authorityId); + $authorityNameId = new NameId(static::uuid()); + $authorityInstitution = new Institution('Wazoo'); + $authorityEmail = new Email('info@domain.invalid'); + $authorityCommonName = new CommonName('Henk Westbroek'); + + $registrantId = new IdentityId($command->identityId); + $registrantInstitution = new Institution('A Corp.'); + $registrantNameId = new NameId('3'); + $registrantSecondFactorId = new SecondFactorId($command->secondFactorId); + $registrantSecondFactorType = new SecondFactorType('yubikey'); + $registrantSecondFactorIdentifier = new YubikeyPublicId('00890782'); + $registrantEmail = new Email('matti@domain.invalid'); + $registrantCommonName = new CommonName('Matti Vanhanen'); + + $this->scenario + ->withAggregateId($authorityId) + ->given([ + new IdentityCreatedEvent( + $authorityId, + $authorityInstitution, + $authorityNameId, + $authorityCommonName, + $authorityEmail, + new Locale('en_GB') + ), + new YubikeySecondFactorBootstrappedEvent( + $authorityId, + $authorityNameId, + $authorityInstitution, + $authorityCommonName, + $authorityEmail, + new Locale('en_GB'), + new SecondFactorId(static::uuid()), + new YubikeyPublicId('12345678') + ) + ]) + ->withAggregateId($registrantId) + ->given([ + new IdentityCreatedEvent( + $registrantId, + $registrantInstitution, + $registrantNameId, + $registrantCommonName, + $registrantEmail, + new Locale('en_GB') + ), + new YubikeyPossessionProvenEvent( + $registrantId, + $registrantInstitution, + $registrantSecondFactorId, + $registrantSecondFactorIdentifier, + true, + EmailVerificationWindow::createFromTimeFrameStartingAt( + TimeFrame::ofSeconds(static::$window), + DateTime::now() + ), + 'nonce', + $registrantCommonName, + $registrantEmail, + new Locale('en_GB') + ), + new EmailVerifiedEvent( + $registrantId, + $registrantInstitution, + $registrantSecondFactorId, + $registrantSecondFactorType, + $registrantSecondFactorIdentifier, + DateTime::now(), + 'REGISTRATION_CODE', + $registrantCommonName, + $registrantEmail, + new Locale('en_GB') + ), + new SecondFactorVettedWithoutTokenProofOfPossession( + $registrantId, + $registrantNameId, + $registrantInstitution, + $registrantSecondFactorId, + $registrantSecondFactorType, + $registrantSecondFactorIdentifier, + new DocumentNumber('DOCUMENT_NUMBER'), + $registrantCommonName, + $registrantEmail, + new Locale('en_GB') + ) + ]) + ->when($command) + ->then([ + new CompliedWithVettedSecondFactorRevocationEvent( + $registrantId, + $registrantInstitution, + $registrantSecondFactorId, + new SecondFactorType('yubikey'), + $registrantSecondFactorIdentifier, + $authorityId + ), + new VettedSecondFactorsAllRevokedEvent( + $registrantId, + $registrantInstitution + ), + ]); + } + + + /** * Test if the VettedSecondFactorsAllRevokedEvent is not triggered with multiple 2fa's * @test diff --git a/src/Surfnet/StepupMiddleware/GatewayBundle/Projector/SecondFactorProjector.php b/src/Surfnet/StepupMiddleware/GatewayBundle/Projector/SecondFactorProjector.php index fdfacc12f..6a3d61b3c 100644 --- a/src/Surfnet/StepupMiddleware/GatewayBundle/Projector/SecondFactorProjector.php +++ b/src/Surfnet/StepupMiddleware/GatewayBundle/Projector/SecondFactorProjector.php @@ -22,6 +22,7 @@ use Surfnet\Stepup\Identity\Event\CompliedWithVettedSecondFactorRevocationEvent; use Surfnet\Stepup\Identity\Event\IdentityForgottenEvent; use Surfnet\Stepup\Identity\Event\LocalePreferenceExpressedEvent; +use Surfnet\Stepup\Identity\Event\SecondFactorVettedWithoutTokenProofOfPossession; use Surfnet\Stepup\Identity\Event\SecondFactorVettedEvent; use Surfnet\Stepup\Identity\Event\VettedSecondFactorRevokedEvent; use Surfnet\Stepup\Identity\Event\YubikeySecondFactorBootstrappedEvent; @@ -74,6 +75,21 @@ public function applySecondFactorVettedEvent(SecondFactorVettedEvent $event) ); } + public function applySecondFactorVettedWithoutTokenProofOfPossession(SecondFactorVettedWithoutTokenProofOfPossession $event) + { + $this->repository->save( + new SecondFactor( + (string) $event->identityId, + (string) $event->nameId, + (string) $event->identityInstitution, + (string) $event->preferredLocale, + (string) $event->secondFactorId, + $event->secondFactorIdentifier, + $event->secondFactorType + ) + ); + } + protected function applyVettedSecondFactorRevokedEvent(VettedSecondFactorRevokedEvent $event) { $secondFactor = $this->repository->findOneBySecondFactorId($event->secondFactorId);