diff --git a/lambdas/check-existing-identity/src/main/java/uk/gov/di/ipv/core/checkexistingidentity/CheckExistingIdentityHandler.java b/lambdas/check-existing-identity/src/main/java/uk/gov/di/ipv/core/checkexistingidentity/CheckExistingIdentityHandler.java index ea2d000ce3..810ebe87a1 100644 --- a/lambdas/check-existing-identity/src/main/java/uk/gov/di/ipv/core/checkexistingidentity/CheckExistingIdentityHandler.java +++ b/lambdas/check-existing-identity/src/main/java/uk/gov/di/ipv/core/checkexistingidentity/CheckExistingIdentityHandler.java @@ -63,6 +63,7 @@ import java.util.Objects; import java.util.Optional; +import static com.amazonaws.util.CollectionUtils.isNullOrEmpty; import static uk.gov.di.ipv.core.library.config.CoreFeatureFlag.EVCS_READ_ENABLED; import static uk.gov.di.ipv.core.library.config.CoreFeatureFlag.EVCS_WRITE_ENABLED; import static uk.gov.di.ipv.core.library.config.CoreFeatureFlag.INHERITED_IDENTITY; @@ -74,6 +75,7 @@ import static uk.gov.di.ipv.core.library.domain.ProfileType.OPERATIONAL_HMRC; import static uk.gov.di.ipv.core.library.domain.VocabConstants.VOT_CLAIM_NAME; import static uk.gov.di.ipv.core.library.enums.EvcsVCState.CURRENT; +import static uk.gov.di.ipv.core.library.enums.EvcsVCState.PENDING_RETURN; import static uk.gov.di.ipv.core.library.helpers.LogHelper.LogField.LOG_MESSAGE_DESCRIPTION; import static uk.gov.di.ipv.core.library.helpers.LogHelper.LogField.LOG_VOT; import static uk.gov.di.ipv.core.library.helpers.RequestHelper.getIpAddress; @@ -90,6 +92,7 @@ import static uk.gov.di.ipv.core.library.journeyuris.JourneyUris.JOURNEY_REPEAT_FRAUD_CHECK_PATH; import static uk.gov.di.ipv.core.library.journeyuris.JourneyUris.JOURNEY_REPROVE_IDENTITY_PATH; import static uk.gov.di.ipv.core.library.journeyuris.JourneyUris.JOURNEY_REUSE_PATH; +import static uk.gov.di.ipv.core.library.journeyuris.JourneyUris.JOURNEY_REUSE_WITH_STORE_PATH; /** Check Existing Identity response Lambda */ public class CheckExistingIdentityHandler @@ -97,6 +100,8 @@ public class CheckExistingIdentityHandler private static final Logger LOGGER = LogManager.getLogger(); private static final JourneyResponse JOURNEY_REUSE = new JourneyResponse(JOURNEY_REUSE_PATH); + private static final JourneyResponse JOURNEY_REUSE_WITH_STORE = + new JourneyResponse(JOURNEY_REUSE_WITH_STORE_PATH); private static final JourneyResponse JOURNEY_OPERATIONAL_PROFILE_REUSE = new JourneyResponse(JOURNEY_OPERATIONAL_PROFILE_REUSE_PATH); private static final JourneyResponse JOURNEY_IN_MIGRATION_REUSE = @@ -185,6 +190,15 @@ public CheckExistingIdentityHandler() { VcHelper.setConfigService(this.configService); } + private record VerifiableCredentialBundle( + List credentials, + boolean isEvcsIdentity, + boolean isPendingEvcsIdentity) { + private boolean isF2fIdentity() { + return credentials.stream().anyMatch(vc -> vc.getCriId().equals(F2F.getId())); + } + } + @Override @Tracing @Logging(clearState = true) @@ -229,12 +243,16 @@ private JourneyResponse getJourneyResponse( AuditEventUser auditEventUser = new AuditEventUser(userId, ipvSessionId, govukSigninJourneyId, ipAddress); - var vcs = getVerifiableCredentials(userId, clientOAuthSessionItem.getEvcsAccessToken()); - var hasF2fVc = vcs.stream().anyMatch(vc -> vc.getCriId().equals(F2F.getId())); + var evcsAccessToken = clientOAuthSessionItem.getEvcsAccessToken(); + var vcs = getVerifiableCredentials(userId, evcsAccessToken); CriResponseItem f2fRequest = criResponseService.getFaceToFaceRequest(userId); + final boolean hasF2fVc = vcs.isF2fIdentity(); final boolean isF2FIncomplete = !Objects.isNull(f2fRequest) && !hasF2fVc; - final boolean isF2FComplete = !Objects.isNull(f2fRequest) && hasF2fVc; - + final boolean isF2FComplete = + !Objects.isNull(f2fRequest) + && hasF2fVc + && (!configService.enabled(EVCS_READ_ENABLED) + || vcs.isPendingEvcsIdentity); var contraIndicators = ciMitService.getContraIndicators( clientOAuthSessionItem.getUserId(), govukSigninJourneyId, ipAddress); @@ -258,7 +276,7 @@ private JourneyResponse getJourneyResponse( } // Check for credentials correlation failure - var areGpg45VcsCorrelated = userIdentityService.areVcsCorrelated(vcs); + var areGpg45VcsCorrelated = userIdentityService.areVcsCorrelated(vcs.credentials); var profileMatchResponse = checkForProfileMatch( @@ -310,16 +328,25 @@ private JourneyResponse getJourneyResponse( } @Tracing - private List getVerifiableCredentials( + private VerifiableCredentialBundle getVerifiableCredentials( String userId, String evcsAccessToken) throws CredentialParseException, EvcsServiceException { if (configService.enabled(EVCS_READ_ENABLED)) { - var vcs = evcsService.getVerifiableCredentials(userId, evcsAccessToken, CURRENT); - if (vcs != null && !vcs.isEmpty()) { - return vcs; + var vcs = + evcsService.getVerifiableCredentialsByState( + userId, evcsAccessToken, CURRENT, PENDING_RETURN); + var pendingReturnVcs = vcs.get(PENDING_RETURN); + // use pending return vcs to determine identity if available + if (!isNullOrEmpty(pendingReturnVcs)) { + return new VerifiableCredentialBundle(pendingReturnVcs, true, true); + } + var currentVcs = vcs.get(CURRENT); + if (!isNullOrEmpty(currentVcs)) { + return new VerifiableCredentialBundle(currentVcs, true, false); } } - return verifiableCredentialService.getVcs(userId); + return new VerifiableCredentialBundle( + verifiableCredentialService.getVcs(userId), false, false); } @Tracing @@ -362,7 +389,7 @@ private Optional checkForProfileMatch( ClientOAuthSessionItem clientOAuthSessionItem, AuditEventUser auditEventUser, String deviceInformation, - List vcs, + VerifiableCredentialBundle vcBundle, boolean areGpg45VcsCorrelated) throws ParseException, UnknownEvidenceTypeException, SqsException, CredentialParseException, VerifiableCredentialException, EvcsServiceException { @@ -370,7 +397,7 @@ private Optional checkForProfileMatch( var strongestAttainedVotFromVtr = getStrongestAttainedVotForVtr( clientOAuthSessionItem.getVtr(), - vcs, + vcBundle.credentials, auditEventUser, deviceInformation, areGpg45VcsCorrelated); @@ -382,7 +409,7 @@ private Optional checkForProfileMatch( strongestAttainedVotFromVtr.get(), ipvSessionItem, clientOAuthSessionItem, - vcs, + vcBundle, auditEventUser, deviceInformation)); } @@ -446,7 +473,7 @@ private JourneyResponse buildReuseResponse( Vot attainedVot, IpvSessionItem ipvSessionItem, ClientOAuthSessionItem clientOAuthSessionItem, - List vcs, + VerifiableCredentialBundle vcBundle, AuditEventUser auditEventUser, String deviceInformation) throws SqsException, VerifiableCredentialException, EvcsServiceException { @@ -455,11 +482,11 @@ private JourneyResponse buildReuseResponse( String evcsAccessToken = clientOAuthSessionItem.getEvcsAccessToken(); if (configService.enabled(REPEAT_FRAUD_CHECK) && attainedVot.getProfileType() == GPG45 - && !hasCurrentFraudVc(vcs)) { + && !hasCurrentFraudVc(vcBundle.credentials)) { LOGGER.info(LogHelper.buildLogMessage("Expired fraud VC found")); sessionCredentialsService.persistCredentials( - allVcsExceptFraud(vcs), auditEventUser.getSessionId(), false); - migrateCredentialsToEVCS(userId, vcs, evcsAccessToken); + allVcsExceptFraud(vcBundle.credentials), auditEventUser.getSessionId(), false); + migrateCredentialsToEVCS(userId, vcBundle, evcsAccessToken); return JOURNEY_REPEAT_FRAUD_CHECK; } @@ -474,7 +501,7 @@ private JourneyResponse buildReuseResponse( boolean isCurrentlyMigrating = ipvSessionItem.isInheritedIdentityReceivedThisSession(); sessionCredentialsService.persistCredentials( - VcHelper.filterVCBasedOnProfileType(vcs, OPERATIONAL_HMRC), + VcHelper.filterVCBasedOnProfileType(vcBundle.credentials, OPERATIONAL_HMRC), auditEventUser.getSessionId(), isCurrentlyMigrating); @@ -484,18 +511,21 @@ private JourneyResponse buildReuseResponse( } sessionCredentialsService.persistCredentials( - VcHelper.filterVCBasedOnProfileType(vcs, attainedVot.getProfileType()), + VcHelper.filterVCBasedOnProfileType( + vcBundle.credentials, attainedVot.getProfileType()), auditEventUser.getSessionId(), false); - migrateCredentialsToEVCS(userId, vcs, evcsAccessToken); - return JOURNEY_REUSE; + migrateCredentialsToEVCS(userId, vcBundle, evcsAccessToken); + + return vcBundle.isPendingEvcsIdentity ? JOURNEY_REUSE_WITH_STORE : JOURNEY_REUSE; } private void migrateCredentialsToEVCS( - String userId, List credentials, String evcsAccessToken) + String userId, VerifiableCredentialBundle vcBundle, String evcsAccessToken) throws EvcsServiceException, VerifiableCredentialException { - if (configService.enabled(EVCS_WRITE_ENABLED)) { - evcsMigrationService.migrateExistingIdentity(userId, credentials, evcsAccessToken); + if (configService.enabled(EVCS_WRITE_ENABLED) && !vcBundle.isEvcsIdentity) { + evcsMigrationService.migrateExistingIdentity( + userId, vcBundle.credentials, evcsAccessToken); } } diff --git a/lambdas/check-existing-identity/src/test/java/uk/gov/di/ipv/core/checkexistingidentity/CheckExistingIdentityHandlerTest.java b/lambdas/check-existing-identity/src/test/java/uk/gov/di/ipv/core/checkexistingidentity/CheckExistingIdentityHandlerTest.java index e246cd5124..089d32f60a 100644 --- a/lambdas/check-existing-identity/src/test/java/uk/gov/di/ipv/core/checkexistingidentity/CheckExistingIdentityHandlerTest.java +++ b/lambdas/check-existing-identity/src/test/java/uk/gov/di/ipv/core/checkexistingidentity/CheckExistingIdentityHandlerTest.java @@ -39,8 +39,10 @@ import uk.gov.di.ipv.core.library.domain.cimitvc.Mitigation; import uk.gov.di.ipv.core.library.enums.EvcsVCState; import uk.gov.di.ipv.core.library.enums.Vot; +import uk.gov.di.ipv.core.library.exception.EvcsServiceException; import uk.gov.di.ipv.core.library.exceptions.ConfigException; import uk.gov.di.ipv.core.library.exceptions.CredentialParseException; +import uk.gov.di.ipv.core.library.exceptions.HttpResponseExceptionWithErrorBody; import uk.gov.di.ipv.core.library.exceptions.UnrecognisedCiException; import uk.gov.di.ipv.core.library.exceptions.VerifiableCredentialException; import uk.gov.di.ipv.core.library.gpg45.Gpg45ProfileEvaluator; @@ -65,7 +67,7 @@ import uk.gov.di.ipv.core.library.verifiablecredential.service.VerifiableCredentialService; import java.time.Instant; -import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -94,6 +96,7 @@ import static uk.gov.di.ipv.core.library.domain.Cri.F2F; import static uk.gov.di.ipv.core.library.domain.Cri.HMRC_MIGRATION; import static uk.gov.di.ipv.core.library.domain.VocabConstants.VOT_CLAIM_NAME; +import static uk.gov.di.ipv.core.library.enums.EvcsVCState.PENDING_RETURN; import static uk.gov.di.ipv.core.library.enums.Vot.P2; import static uk.gov.di.ipv.core.library.fixtures.TestFixtures.EC_PRIVATE_KEY_JWK; import static uk.gov.di.ipv.core.library.fixtures.VcFixtures.EXPIRED_M1A_EXPERIAN_FRAUD_VC; @@ -117,6 +120,7 @@ import static uk.gov.di.ipv.core.library.journeyuris.JourneyUris.JOURNEY_REPEAT_FRAUD_CHECK_PATH; import static uk.gov.di.ipv.core.library.journeyuris.JourneyUris.JOURNEY_REPROVE_IDENTITY_PATH; import static uk.gov.di.ipv.core.library.journeyuris.JourneyUris.JOURNEY_REUSE_PATH; +import static uk.gov.di.ipv.core.library.journeyuris.JourneyUris.JOURNEY_REUSE_WITH_STORE_PATH; @ExtendWith(MockitoExtension.class) class CheckExistingIdentityHandlerTest { @@ -132,6 +136,8 @@ class CheckExistingIdentityHandlerTest { public static final String EVCS_TEST_TOKEN = "evcsTestToken"; private static List VCS_FROM_STORE; private static final JourneyResponse JOURNEY_REUSE = new JourneyResponse(JOURNEY_REUSE_PATH); + private static final JourneyResponse JOURNEY_REUSE_WITH_STORE = + new JourneyResponse(JOURNEY_REUSE_WITH_STORE_PATH); private static final JourneyResponse JOURNEY_OP_PROFILE_REUSE = new JourneyResponse(JOURNEY_OPERATIONAL_PROFILE_REUSE_PATH); private static final JourneyResponse JOURNEY_IN_MIGRATION_REUSE = @@ -243,28 +249,38 @@ public void reuseSetup() { @Test void shouldUseEvcsServiceWhenEnabled() throws Exception { when(configService.enabled(EVCS_READ_ENABLED)).thenReturn(true); - when(mockEvcsService.getVerifiableCredentials(any(), any(), any(EvcsVCState.class))) - .thenReturn(List.of(gpg45Vc, vcHmrcMigration())); + when(mockEvcsService.getVerifiableCredentialsByState( + any(), any(), any(EvcsVCState.class), any(EvcsVCState.class))) + .thenReturn(Map.of(PENDING_RETURN, List.of(gpg45Vc, vcHmrcMigration()))); checkExistingIdentityHandler.handleRequest(event, context); verify(clientOAuthSessionDetailsService, times(1)).getClientOAuthSession(any()); verify(mockEvcsService, times(1)) - .getVerifiableCredentials(TEST_USER_ID, EVCS_TEST_TOKEN, EvcsVCState.CURRENT); + .getVerifiableCredentialsByState( + TEST_USER_ID, + EVCS_TEST_TOKEN, + EvcsVCState.CURRENT, + EvcsVCState.PENDING_RETURN); verify(mockVerifiableCredentialService, never()).getVcs(TEST_USER_ID); } @Test void shouldUseVcServiceWhenEvcsServiceWhenAndReturnsEmpty() throws Exception { when(configService.enabled(EVCS_READ_ENABLED)).thenReturn(true); - when(mockEvcsService.getVerifiableCredentials(any(), any(), any(EvcsVCState.class))) - .thenReturn(new ArrayList()); + when(mockEvcsService.getVerifiableCredentialsByState( + any(), any(), any(EvcsVCState.class), any(EvcsVCState.class))) + .thenReturn(new HashMap>()); checkExistingIdentityHandler.handleRequest(event, context); verify(clientOAuthSessionDetailsService, times(1)).getClientOAuthSession(any()); verify(mockEvcsService, times(1)) - .getVerifiableCredentials(TEST_USER_ID, EVCS_TEST_TOKEN, EvcsVCState.CURRENT); + .getVerifiableCredentialsByState( + TEST_USER_ID, + EVCS_TEST_TOKEN, + EvcsVCState.CURRENT, + EvcsVCState.PENDING_RETURN); verify(mockVerifiableCredentialService, times(1)).getVcs(TEST_USER_ID); } @@ -311,6 +327,32 @@ void shouldReturnJourneyReuseResponseIfScoresSatisfyM1AGpg45Profile_alsoStoreVcs TEST_USER_ID, List.of(gpg45Vc, hmrcMigrationVC), EVCS_TEST_TOKEN); } + @Test + void shouldReturnJourneyReuseUpdateResponseIfVcIsF2fAndHasPendingReturnInEvcs() + throws CredentialParseException, EvcsServiceException, + HttpResponseExceptionWithErrorBody, VerifiableCredentialException { + when(configService.enabled(EVCS_READ_ENABLED)).thenReturn(true); + + when(mockEvcsService.getVerifiableCredentialsByState( + any(), any(), any(EvcsVCState.class), any(EvcsVCState.class))) + .thenReturn(Map.of(PENDING_RETURN, List.of(vcF2fM1a()))); + + when(criResponseService.getFaceToFaceRequest(any())).thenReturn(new CriResponseItem()); + when(gpg45ProfileEvaluator.getFirstMatchingProfile( + any(), eq(P2.getSupportedGpg45Profiles()))) + .thenReturn(Optional.of(Gpg45Profile.M1A)); + when(userIdentityService.areVcsCorrelated(any())).thenReturn(true); + + JourneyResponse journeyResponse = + toResponseClass( + checkExistingIdentityHandler.handleRequest(event, context), + JourneyResponse.class); + + assertEquals(JOURNEY_REUSE_WITH_STORE, journeyResponse); + // pending vcs should not be migrated + verify(mockEvcsMigrationService, never()).migrateExistingIdentity(any(), any(), any()); + } + @Test void shouldReturnJourneyReuseResponseIfScoresSatisfyM1BGpg45Profile() throws Exception { when(gpg45ProfileEvaluator.getFirstMatchingProfile( diff --git a/lambdas/process-journey-event/src/main/resources/statemachine/journey-maps/initial-journey-selection.yaml b/lambdas/process-journey-event/src/main/resources/statemachine/journey-maps/initial-journey-selection.yaml index ee331f02d3..3f67c40367 100644 --- a/lambdas/process-journey-event/src/main/resources/statemachine/journey-maps/initial-journey-selection.yaml +++ b/lambdas/process-journey-event/src/main/resources/statemachine/journey-maps/initial-journey-selection.yaml @@ -26,6 +26,9 @@ states: reuse: targetJourney: REUSE_EXISTING_IDENTITY targetState: START + reuse-with-store: + targetJourney: REUSE_EXISTING_IDENTITY + targetState: REUSE_WITH_STORE_IDENTITY operational-profile-reuse: targetJourney: OPERATIONAL_PROFILE_REUSE targetState: START diff --git a/lambdas/process-journey-event/src/main/resources/statemachine/journey-maps/reuse-existing-identity.yaml b/lambdas/process-journey-event/src/main/resources/statemachine/journey-maps/reuse-existing-identity.yaml index 5bc521950d..5d7e999769 100644 --- a/lambdas/process-journey-event/src/main/resources/statemachine/journey-maps/reuse-existing-identity.yaml +++ b/lambdas/process-journey-event/src/main/resources/statemachine/journey-maps/reuse-existing-identity.yaml @@ -23,8 +23,33 @@ states: next: targetState: UPDATE_DETAILS_PAGE + REUSE_WITH_STORE_IDENTITY: + events: + next: + targetState: UPDATE_IDENTITY_BEFORE_IDENTITY_REUSE_PAGE + # Journey states + STORE_NEW_IDENTITY: + response: + type: process + lambda: store-identity + lambdaInput: + identityType: NEW + events: + error: + targetJourney: TECHNICAL_ERROR + targetState: ERROR + identity-stored: + targetState: IDENTITY_REUSE_PAGE + checkFeatureFlag: + ticfCriBeta: + targetState: CRI_TICF_BEFORE_REUSE + deleteDetailsEnabled: + targetState: IDENTITY_REUSE_PAGE_TEST + coiEnabled: + targetState: IDENTITY_REUSE_PAGE_COI + CRI_TICF_BEFORE_REUSE: response: type: process diff --git a/libs/evcs-service/src/main/java/uk/gov/di/ipv/core/library/service/EvcsService.java b/libs/evcs-service/src/main/java/uk/gov/di/ipv/core/library/service/EvcsService.java index 14d63282fb..b15d8935fb 100644 --- a/libs/evcs-service/src/main/java/uk/gov/di/ipv/core/library/service/EvcsService.java +++ b/libs/evcs-service/src/main/java/uk/gov/di/ipv/core/library/service/EvcsService.java @@ -16,7 +16,9 @@ import java.text.ParseException; import java.util.ArrayList; +import java.util.EnumMap; import java.util.List; +import java.util.Map; import static uk.gov.di.ipv.core.library.enums.EvcsVCState.CURRENT; import static uk.gov.di.ipv.core.library.enums.EvcsVCState.PENDING_RETURN; @@ -65,15 +67,28 @@ public void storePendingIdentity( public List getVerifiableCredentials( String userId, String evcsAccessToken, EvcsVCState... states) throws CredentialParseException, EvcsServiceException { + return getVerifiableCredentialsByState(userId, evcsAccessToken, states).values().stream() + .flatMap(List::stream) + .toList(); + } + + @Tracing + public Map> getVerifiableCredentialsByState( + String userId, String evcsAccessToken, EvcsVCState... states) + throws CredentialParseException, EvcsServiceException { List vcs = evcsClient.getUserVcs(userId, evcsAccessToken, List.of(states)).vcs(); - List credentials = new ArrayList<>(); + Map> credentials = new EnumMap<>(EvcsVCState.class); for (var vc : vcs) { try { var jwt = SignedJWT.parse(vc.vc()); var cri = configService.getCriByIssuer(jwt.getJWTClaimsSet().getIssuer()); - credentials.add(VerifiableCredential.fromValidJwt(userId, cri.getId(), jwt)); + var credential = VerifiableCredential.fromValidJwt(userId, cri.getId(), jwt); + if (!credentials.containsKey(vc.state())) { + credentials.put(vc.state(), new ArrayList<>()); + } + credentials.get(vc.state()).add(credential); } catch (NoCriForIssuerException e) { throw new CredentialParseException("Failed to find credential issuer for vc", e); } catch (ParseException e) { diff --git a/libs/journey-uris/src/main/java/uk/gov/di/ipv/core/library/journeyuris/JourneyUris.java b/libs/journey-uris/src/main/java/uk/gov/di/ipv/core/library/journeyuris/JourneyUris.java index 937ec962f1..fd6f6b5a43 100644 --- a/libs/journey-uris/src/main/java/uk/gov/di/ipv/core/library/journeyuris/JourneyUris.java +++ b/libs/journey-uris/src/main/java/uk/gov/di/ipv/core/library/journeyuris/JourneyUris.java @@ -29,6 +29,7 @@ private JourneyUris() { public static final String JOURNEY_REPEAT_FRAUD_CHECK_PATH = "/journey/repeat-fraud-check"; public static final String JOURNEY_REPROVE_IDENTITY_PATH = "/journey/reprove-identity"; public static final String JOURNEY_REUSE_PATH = "/journey/reuse"; + public static final String JOURNEY_REUSE_WITH_STORE_PATH = "/journey/reuse-with-store"; public static final String JOURNEY_TEMPORARILY_UNAVAILABLE_PATH = "/journey/temporarily-unavailable"; public static final String JOURNEY_UNMET_PATH = "/journey/unmet";