diff --git a/account-management-api/build.gradle b/account-management-api/build.gradle index 2544db3d56c..e6dedf5080f 100644 --- a/account-management-api/build.gradle +++ b/account-management-api/build.gradle @@ -23,6 +23,7 @@ dependencies { libs.jetbrainsAnnotations implementation project(":shared") + implementation project(':audit-events') runtimeOnly libs.bundles.loggingRuntime, platform(libs.openTelemetryBom), diff --git a/account-management-api/src/main/java/uk/gov/di/accountmanagement/domain/AccountManagementAuditableEvent.java b/account-management-api/src/main/java/uk/gov/di/accountmanagement/domain/AccountManagementAuditableEvent.java index 2537d9a10fe..1d34849189e 100644 --- a/account-management-api/src/main/java/uk/gov/di/accountmanagement/domain/AccountManagementAuditableEvent.java +++ b/account-management-api/src/main/java/uk/gov/di/accountmanagement/domain/AccountManagementAuditableEvent.java @@ -21,7 +21,8 @@ public enum AccountManagementAuditableEvent implements AuditableEvent { AUTH_CODE_VERIFIED, AUTH_INVALID_CODE_SENT, AUTH_PHONE_CODE_SENT, - AUTH_UPDATE_PROFILE_AUTH_APP; + AUTH_UPDATE_PROFILE_AUTH_APP, + AUTH_EMAIL_FRAUD_CHECK_DECISION_USED; public AuditableEvent parseFromName(String name) { return valueOf(name); diff --git a/account-management-api/src/main/java/uk/gov/di/accountmanagement/lambda/UpdateEmailHandler.java b/account-management-api/src/main/java/uk/gov/di/accountmanagement/lambda/UpdateEmailHandler.java index 2308da47e7a..b7f570ae3d2 100644 --- a/account-management-api/src/main/java/uk/gov/di/accountmanagement/lambda/UpdateEmailHandler.java +++ b/account-management-api/src/main/java/uk/gov/di/accountmanagement/lambda/UpdateEmailHandler.java @@ -17,7 +17,11 @@ import uk.gov.di.accountmanagement.services.AwsSqsClient; import uk.gov.di.accountmanagement.services.CodeStorageService; import uk.gov.di.audit.AuditContext; +import uk.gov.di.authentication.auditevents.entity.AuthEmailFraudCheckBypassed; +import uk.gov.di.authentication.auditevents.entity.AuthEmailFraudCheckDecisionUsed; +import uk.gov.di.authentication.auditevents.services.StructuredAuditService; import uk.gov.di.authentication.shared.entity.EmailCheckResultStatus; +import uk.gov.di.authentication.shared.entity.EmailCheckResultStore; import uk.gov.di.authentication.shared.entity.ErrorResponse; import uk.gov.di.authentication.shared.entity.JourneyType; import uk.gov.di.authentication.shared.exceptions.UserNotFoundException; @@ -40,19 +44,20 @@ import java.util.ArrayList; import java.util.Map; +import java.util.Objects; import java.util.Optional; -import java.util.concurrent.atomic.AtomicReference; import static uk.gov.di.accountmanagement.constants.AccountManagementConstants.AUDIT_EVENT_COMPONENT_ID_AUTH; import static uk.gov.di.authentication.shared.domain.RequestHeaders.SESSION_ID_HEADER; import static uk.gov.di.authentication.shared.helpers.ApiGatewayResponseHelper.generateApiGatewayProxyErrorResponse; import static uk.gov.di.authentication.shared.helpers.ApiGatewayResponseHelper.generateEmptySuccessApiGatewayResponse; +import static uk.gov.di.authentication.shared.helpers.EmailCheckResultExtractorHelper.getEmailFraudCheckResponseJsonFromResult; +import static uk.gov.di.authentication.shared.helpers.EmailCheckResultExtractorHelper.getRestrictedJsonFromResult; import static uk.gov.di.authentication.shared.helpers.InstrumentationHelper.segmentedFunctionCall; import static uk.gov.di.authentication.shared.helpers.LocaleHelper.getUserLanguageFromRequestHeaders; import static uk.gov.di.authentication.shared.helpers.LocaleHelper.matchSupportedLanguage; import static uk.gov.di.authentication.shared.helpers.LogLineHelper.attachSessionIdToLogs; import static uk.gov.di.authentication.shared.helpers.LogLineHelper.attachTraceId; -import static uk.gov.di.authentication.shared.services.AuditService.MetadataPair.pair; public class UpdateEmailHandler implements RequestHandler { @@ -65,6 +70,7 @@ public class UpdateEmailHandler private static final Logger LOG = LogManager.getLogger(UpdateEmailHandler.class); private final AuditService auditService; private final ConfigurationService configurationService; + private final StructuredAuditService structuredAuditService; public UpdateEmailHandler() { this(ConfigurationService.getInstance()); @@ -76,13 +82,15 @@ public UpdateEmailHandler( AwsSqsClient sqsClient, CodeStorageService codeStorageService, AuditService auditService, - ConfigurationService configurationService) { + ConfigurationService configurationService, + StructuredAuditService structuredAuditService) { this.dynamoService = dynamoService; this.dynamoEmailCheckResultService = dynamoEmailCheckResultService; this.sqsClient = sqsClient; this.codeStorageService = codeStorageService; this.auditService = auditService; this.configurationService = configurationService; + this.structuredAuditService = structuredAuditService; } public UpdateEmailHandler(ConfigurationService configurationService) { @@ -98,6 +106,7 @@ public UpdateEmailHandler(ConfigurationService configurationService) { new CodeStorageService(new RedisConnectionService(configurationService)); this.auditService = new AuditService(configurationService); this.configurationService = configurationService; + this.structuredAuditService = new StructuredAuditService(configurationService); } @Override @@ -156,19 +165,24 @@ public APIGatewayProxyResponseEvent updateEmailRequestHandler( new UserNotFoundException( "User not found with given email")); - AtomicReference emailCheckResultStatus = - new AtomicReference<>(EmailCheckResultStatus.PENDING); - dynamoEmailCheckResultService - .getEmailCheckStore(updateInfoRequest.getReplacementEmailAddress()) - .ifPresent(result -> emailCheckResultStatus.set(result.getStatus())); + var emailCheckResult = + dynamoEmailCheckResultService.getEmailCheckStore( + updateInfoRequest.getReplacementEmailAddress()); + + var emailCheckResultStatus = + emailCheckResult + .map(EmailCheckResultStore::getStatus) + .orElse(EmailCheckResultStatus.PENDING); LOG.info( "UpdateEmailHandler: Experian email verification status: {}", - emailCheckResultStatus.get()); + emailCheckResultStatus); - if (emailCheckResultStatus.get() == EmailCheckResultStatus.DENY) { - return generateApiGatewayProxyErrorResponse( - 403, ErrorResponse.EMAIL_ADDRESS_DENIED); - } + LOG.info("Calculating internal common subject identifier"); + var internalCommonSubjectIdentifier = + ClientSubjectHelper.getSubjectWithSectorIdentifier( + userProfile, + configurationService.getInternalSectorUri(), + dynamoService); var auditContext = new AuditContext( @@ -178,7 +192,7 @@ public APIGatewayProxyResponseEvent updateEmailRequestHandler( .toString(), ClientSessionIdHelper.extractSessionIdFromHeaders(input.getHeaders()), sessionId, - AuditService.UNKNOWN, + internalCommonSubjectIdentifier.getValue(), updateInfoRequest.getReplacementEmailAddress(), IpAddressHelper.extractIpAddress(input), userProfile.getPhoneNumber(), @@ -186,15 +200,17 @@ public APIGatewayProxyResponseEvent updateEmailRequestHandler( AuditHelper.getTxmaAuditEncoded(input.getHeaders()), new ArrayList<>()); - if (emailCheckResultStatus.get().equals(EmailCheckResultStatus.PENDING)) { - auditService.submitAuditEvent( - AccountManagementAuditableEvent.AUTH_EMAIL_FRAUD_CHECK_BYPASSED, - auditContext.withSubjectId(userProfile.getSubjectID()), - AUDIT_EVENT_COMPONENT_ID_AUTH, - pair("journey_type", JourneyType.ACCOUNT_MANAGEMENT.getValue()), - pair( - "assessment_checked_at_timestamp", - NowHelper.toUnixTimestamp(NowHelper.now()))); + if (emailCheckResultStatus.equals(EmailCheckResultStatus.PENDING)) { + submitEmailFraudCheckBypassedAuditEvent(auditContext); + } else { + emailCheckResult.ifPresent( + result -> + submitEmailFraudCheckDecisionUsedAuditEvent(auditContext, result)); + } + + if (emailCheckResultStatus == EmailCheckResultStatus.DENY) { + return generateApiGatewayProxyErrorResponse( + 403, ErrorResponse.EMAIL_ADDRESS_DENIED); } Map authorizerParams = input.getRequestContext().getAuthorizer(); @@ -222,13 +238,6 @@ public APIGatewayProxyResponseEvent updateEmailRequestHandler( sqsClient.send(objectMapper.writeValueAsString((notifyEmailAddressUpdateRequest))); } - LOG.info("Calculating internal common subject identifier"); - var internalCommonSubjectIdentifier = - ClientSubjectHelper.getSubjectWithSectorIdentifier( - userProfile, - configurationService.getInternalSectorUri(), - dynamoService); - auditService.submitAuditEvent( AccountManagementAuditableEvent.AUTH_UPDATE_EMAIL, auditContext.withSubjectId(internalCommonSubjectIdentifier.getValue()), @@ -244,4 +253,48 @@ public APIGatewayProxyResponseEvent updateEmailRequestHandler( return generateApiGatewayProxyErrorResponse(400, ErrorResponse.REQUEST_MISSING_PARAMS); } } + + private void submitEmailFraudCheckBypassedAuditEvent(AuditContext auditContext) { + var newAuditEvent = + AuthEmailFraudCheckBypassed.create( + auditContext.clientId(), + new AuthEmailFraudCheckBypassed.User( + auditContext.subjectId(), + auditContext.email(), + auditContext.ipAddress(), + auditContext.persistentSessionId(), + auditContext.sessionId()), + new AuthEmailFraudCheckBypassed.Extensions( + JourneyType.REGISTRATION.getValue(), + NowHelper.toUnixTimestamp(NowHelper.now()))); + + structuredAuditService.submitAuditEvent(newAuditEvent); + } + + private void submitEmailFraudCheckDecisionUsedAuditEvent( + AuditContext auditContext, EmailCheckResultStore emailCheckResult) { + var decision_reused = + !Objects.equals( + auditContext.sessionId(), emailCheckResult.getGovukSigninJourneyId()); + var newAuditEvent = + AuthEmailFraudCheckDecisionUsed.create( + auditContext.clientId(), + new AuthEmailFraudCheckDecisionUsed.User( + auditContext.subjectId(), + auditContext.email(), + auditContext.ipAddress(), + auditContext.persistentSessionId(), + auditContext.sessionId()), + new AuthEmailFraudCheckDecisionUsed.Extensions( + JourneyType.REGISTRATION.getValue(), + decision_reused ? emailCheckResult.getReferenceNumber() : null, + emailCheckResult.getStatus().name(), + decision_reused, + decision_reused + ? getEmailFraudCheckResponseJsonFromResult(emailCheckResult) + : null), + decision_reused ? getRestrictedJsonFromResult(emailCheckResult) : null); + + structuredAuditService.submitAuditEvent(newAuditEvent); + } } diff --git a/account-management-api/src/test/java/uk/gov/di/accountmanagement/lambda/UpdateEmailHandlerTest.java b/account-management-api/src/test/java/uk/gov/di/accountmanagement/lambda/UpdateEmailHandlerTest.java index 45b85b4c1d3..b10f0bfac03 100644 --- a/account-management-api/src/test/java/uk/gov/di/accountmanagement/lambda/UpdateEmailHandlerTest.java +++ b/account-management-api/src/test/java/uk/gov/di/accountmanagement/lambda/UpdateEmailHandlerTest.java @@ -3,10 +3,16 @@ import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; +import com.google.gson.Gson; +import com.google.gson.JsonElement; import com.nimbusds.oauth2.sdk.id.Subject; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.ArgumentCaptor; import org.mockito.MockedStatic; import org.mockito.Mockito; import uk.gov.di.accountmanagement.domain.AccountManagementAuditableEvent; @@ -17,6 +23,9 @@ import uk.gov.di.accountmanagement.services.AwsSqsClient; import uk.gov.di.accountmanagement.services.CodeStorageService; import uk.gov.di.audit.AuditContext; +import uk.gov.di.authentication.auditevents.entity.AuthEmailFraudCheckBypassed; +import uk.gov.di.authentication.auditevents.entity.AuthEmailFraudCheckDecisionUsed; +import uk.gov.di.authentication.auditevents.services.StructuredAuditService; import uk.gov.di.authentication.shared.entity.EmailCheckResultStatus; import uk.gov.di.authentication.shared.entity.EmailCheckResultStore; import uk.gov.di.authentication.shared.entity.ErrorResponse; @@ -24,6 +33,7 @@ import uk.gov.di.authentication.shared.entity.UserProfile; import uk.gov.di.authentication.shared.helpers.ClientSessionIdHelper; import uk.gov.di.authentication.shared.helpers.ClientSubjectHelper; +import uk.gov.di.authentication.shared.helpers.CommonTestVariables; import uk.gov.di.authentication.shared.helpers.LocaleHelper.SupportedLanguage; import uk.gov.di.authentication.shared.helpers.NowHelper; import uk.gov.di.authentication.shared.helpers.PersistentIdHelper; @@ -40,11 +50,13 @@ import java.util.HashMap; import java.util.Map; import java.util.Optional; +import java.util.stream.Stream; import static java.lang.String.format; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; @@ -70,6 +82,8 @@ class UpdateEmailHandlerTest { private final AwsSqsClient sqsClient = mock(AwsSqsClient.class); private final CodeStorageService codeStorageService = mock(CodeStorageService.class); private final ConfigurationService configurationService = mock(ConfigurationService.class); + private final StructuredAuditService structuredAuditService = + mock(StructuredAuditService.class); private UpdateEmailHandler handler; private static final String EXISTING_EMAIL_ADDRESS = "joe.bloggs@digital.cabinet-office.gov.uk"; private static final String NEW_EMAIL_ADDRESS = "bloggs.joe@digital.cabinet-office.gov.uk"; @@ -113,7 +127,8 @@ void setUp() { sqsClient, codeStorageService, auditService, - configurationService); + configurationService, + structuredAuditService); when(configurationService.getInternalSectorUri()).thenReturn("https://test.account.gov.uk"); when(dynamoService.getOrGenerateSalt(any(UserProfile.class))).thenReturn(SALT); } @@ -158,7 +173,7 @@ void shouldReturn204WhenPrincipalContainsInternalPairwiseSubjectId() throws Json } @Test - void shouldSubmitAuditEventWhenEmailCheckResultRecordDoesNotExist() { + void shouldSubmitEmailFraudCheckBypassedAuditEventWhenEmailCheckResultRecordDoesNotExist() { var userProfile = new UserProfile().withSubjectID(INTERNAL_SUBJECT.getValue()); when(dynamoService.getUserProfileByEmailMaybe(EXISTING_EMAIL_ADDRESS)) .thenReturn(Optional.of(userProfile)); @@ -181,15 +196,93 @@ void shouldSubmitAuditEventWhenEmailCheckResultRecordDoesNotExist() { withMessageContaining( "UpdateEmailHandler: Experian email verification status: PENDING"))); - verify(auditService) - .submitAuditEvent( - AccountManagementAuditableEvent.AUTH_EMAIL_FRAUD_CHECK_BYPASSED, - auditContext.withSubjectId(INTERNAL_SUBJECT.getValue()), - AUDIT_EVENT_COMPONENT_ID_AUTH, - AuditService.MetadataPair.pair( - "journey_type", JourneyType.ACCOUNT_MANAGEMENT.getValue()), - AuditService.MetadataPair.pair( - "assessment_checked_at_timestamp", mockedTimestamp)); + ArgumentCaptor auditEventCaptor = + ArgumentCaptor.forClass(AuthEmailFraudCheckBypassed.class); + verify(structuredAuditService).submitAuditEvent(auditEventCaptor.capture()); + + AuthEmailFraudCheckBypassed capturedEvent = auditEventCaptor.getValue(); + assertThat(capturedEvent.eventName(), is("AUTH_EMAIL_FRAUD_CHECK_BYPASSED")); + assertThat(capturedEvent.clientId(), is(auditContext.clientId())); + assertThat(capturedEvent.user().email(), is(NEW_EMAIL_ADDRESS)); + assertThat(capturedEvent.user().ipAddress(), is(auditContext.ipAddress())); + assertThat( + capturedEvent.user().persistentSessionId(), + is(auditContext.persistentSessionId())); + assertThat(capturedEvent.user().govukSigninJourneyId(), is(auditContext.sessionId())); + assertThat(capturedEvent.user().userId(), is(expectedCommonSubject)); + assertThat( + capturedEvent.extensions().journeyType(), + is(JourneyType.REGISTRATION.getValue())); + assertThat( + capturedEvent.extensions().assessmentCheckedAtTimestamp(), is(mockedTimestamp)); + } + } + + private static Stream successfulEmailCheckResultStatus() { + return Stream.of( + Arguments.of(EmailCheckResultStatus.ALLOW), + Arguments.of(EmailCheckResultStatus.DENY)); + } + + @ParameterizedTest + @MethodSource("successfulEmailCheckResultStatus") + void shouldSubmitEmailCheckDecisionUsedAuditEventWhenEmailCheckIsPresent( + EmailCheckResultStatus status) { + Gson gson = new Gson(); + var userProfile = new UserProfile().withSubjectID(INTERNAL_SUBJECT.getValue()); + when(dynamoService.getUserProfileByEmailMaybe(EXISTING_EMAIL_ADDRESS)) + .thenReturn(Optional.of(userProfile)); + when(codeStorageService.isValidOtpCode(NEW_EMAIL_ADDRESS, OTP, VERIFY_EMAIL)) + .thenReturn(true); + var resultStore = new EmailCheckResultStore(); + var mockEmailCheckResponse = CommonTestVariables.TEST_EMAIL_CHECK_RESPONSE; + AuthEmailFraudCheckDecisionUsed.Extensions expectedExtensions = + new AuthEmailFraudCheckDecisionUsed.Extensions( + JourneyType.REGISTRATION.getValue(), + "some-reference-number", + status.getValue(), + true, + gson.toJsonTree(Map.of("type", "EMAIL_FRAUD_CHECK"))); + JsonElement expectedRestricted = + gson.toJsonTree(Map.of("domain_name", "digital.cabinet-office.gov.uk")); + resultStore.setStatus(status); + resultStore.setReferenceNumber("some-reference-number"); + resultStore.setEmailCheckResponse(mockEmailCheckResponse); + when(dynamoEmailCheckResultService.getEmailCheckStore(NEW_EMAIL_ADDRESS)) + .thenReturn(Optional.of(resultStore)); + + long mockedTimestamp = 1719376320; + try (MockedStatic mockedNowHelperClass = Mockito.mockStatic(NowHelper.class)) { + mockedNowHelperClass + .when(() -> NowHelper.toUnixTimestamp(NowHelper.now())) + .thenReturn(mockedTimestamp); + var event = generateApiGatewayEvent(NEW_EMAIL_ADDRESS, expectedCommonSubject); + handler.handleRequest(event, context); + + assertThat( + logging.events(), + hasItem( + withMessageContaining( + String.format( + "UpdateEmailHandler: Experian email verification status: %s", + status)))); + + ArgumentCaptor auditEventCaptor = + ArgumentCaptor.forClass(AuthEmailFraudCheckDecisionUsed.class); + verify(structuredAuditService).submitAuditEvent(auditEventCaptor.capture()); + + AuthEmailFraudCheckDecisionUsed capturedEvent = auditEventCaptor.getValue(); + assertThat(capturedEvent.eventName(), is("AUTH_EMAIL_FRAUD_CHECK_DECISION_USED")); + assertThat(capturedEvent.clientId(), is(CLIENT_ID)); + assertThat(capturedEvent.user().email(), is(NEW_EMAIL_ADDRESS)); + assertThat(capturedEvent.user().ipAddress(), is(auditContext.ipAddress())); + assertThat( + capturedEvent.user().persistentSessionId(), + is(auditContext.persistentSessionId())); + assertThat(capturedEvent.user().govukSigninJourneyId(), is(auditContext.sessionId())); + assertThat(capturedEvent.user().userId(), is(expectedCommonSubject)); + assertThat(capturedEvent.extensions(), is(expectedExtensions)); + assertThat(capturedEvent.restricted(), is(expectedRestricted)); } } diff --git a/account-management-integration-tests/build.gradle b/account-management-integration-tests/build.gradle index 1e2472774ae..6f4265a8a1f 100644 --- a/account-management-integration-tests/build.gradle +++ b/account-management-integration-tests/build.gradle @@ -20,6 +20,8 @@ dependencies { libs.bundles.lettuce, libs.libphonenumber + implementation project(':audit-events') + testImplementation project(":shared"), noXray testImplementation project(":account-management-api"), noXray testImplementation project(":shared-test"), noXray diff --git a/account-management-integration-tests/src/test/java/uk/gov/di/accountmanagement/api/SendOtpNotificationIntegrationTest.java b/account-management-integration-tests/src/test/java/uk/gov/di/accountmanagement/api/SendOtpNotificationIntegrationTest.java index 28f24ca4533..84ede367700 100644 --- a/account-management-integration-tests/src/test/java/uk/gov/di/accountmanagement/api/SendOtpNotificationIntegrationTest.java +++ b/account-management-integration-tests/src/test/java/uk/gov/di/accountmanagement/api/SendOtpNotificationIntegrationTest.java @@ -85,7 +85,7 @@ void shouldSendNotificationAndReturn204ForVerifyEmailRequest() { unixTimePlusNDays(), "test-reference", CommonTestVariables.JOURNEY_ID, - CommonTestVariables.EMAIL_CHECK_RESPONSE_TEST_DATA); + CommonTestVariables.TEST_EMAIL_CHECK_RESPONSE); Map headers = new HashMap<>(); headers.put(TXMA_AUDIT_ENCODED_HEADER, "ENCODED_DEVICE_DETAILS"); diff --git a/account-management-integration-tests/src/test/java/uk/gov/di/accountmanagement/api/UpdateEmailIntegrationTest.java b/account-management-integration-tests/src/test/java/uk/gov/di/accountmanagement/api/UpdateEmailIntegrationTest.java index 15bd5c1cf92..22cb56f1c09 100644 --- a/account-management-integration-tests/src/test/java/uk/gov/di/accountmanagement/api/UpdateEmailIntegrationTest.java +++ b/account-management-integration-tests/src/test/java/uk/gov/di/accountmanagement/api/UpdateEmailIntegrationTest.java @@ -9,8 +9,10 @@ import uk.gov.di.accountmanagement.entity.NotifyRequest; import uk.gov.di.accountmanagement.entity.UpdateEmailRequest; import uk.gov.di.accountmanagement.lambda.UpdateEmailHandler; +import uk.gov.di.authentication.shared.domain.AuditableEvent; import uk.gov.di.authentication.shared.entity.EmailCheckResultStatus; import uk.gov.di.authentication.shared.entity.ErrorResponse; +import uk.gov.di.authentication.shared.entity.JourneyType; import uk.gov.di.authentication.shared.entity.PriorityIdentifier; import uk.gov.di.authentication.shared.entity.mfa.MFAMethod; import uk.gov.di.authentication.shared.helpers.ClientSubjectHelper; @@ -20,8 +22,10 @@ import uk.gov.di.authentication.sharedtest.basetest.ApiGatewayHandlerIntegrationTest; import uk.gov.di.authentication.sharedtest.extensions.EmailCheckResultExtension; import uk.gov.di.authentication.sharedtest.helper.AuditAssertionsHelper; +import uk.gov.di.authentication.sharedtest.helper.AuditEventExpectation; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -30,6 +34,7 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertThrows; +import static uk.gov.di.accountmanagement.domain.AccountManagementAuditableEvent.AUTH_EMAIL_FRAUD_CHECK_DECISION_USED; import static uk.gov.di.accountmanagement.domain.AccountManagementAuditableEvent.AUTH_UPDATE_EMAIL; import static uk.gov.di.accountmanagement.entity.NotificationType.EMAIL_UPDATED; import static uk.gov.di.accountmanagement.testsupport.helpers.NotificationAssertionHelper.assertNoNotificationsReceived; @@ -37,6 +42,8 @@ import static uk.gov.di.authentication.shared.entity.mfa.MFAMethodType.AUTH_APP; import static uk.gov.di.authentication.shared.entity.mfa.MFAMethodType.SMS; import static uk.gov.di.authentication.shared.helpers.NowHelper.unixTimePlusNDays; +import static uk.gov.di.authentication.sharedtest.helper.AuditAssertionsHelper.assertNoTxmaAuditEventsReceived; +import static uk.gov.di.authentication.sharedtest.helper.AuditAssertionsHelper.assertTxmaAuditEventsReceived; import static uk.gov.di.authentication.sharedtest.helper.AuditAssertionsHelper.assertTxmaAuditEventsSubmittedWithMatchingNames; import static uk.gov.di.authentication.sharedtest.matchers.APIGatewayProxyResponseEventMatcher.hasBody; import static uk.gov.di.authentication.sharedtest.matchers.APIGatewayProxyResponseEventMatcher.hasStatus; @@ -49,6 +56,9 @@ class UpdateEmailIntegrationTest extends ApiGatewayHandlerIntegrationTest { private static final Subject SUBJECT = new Subject(); private static final String INTERNAl_SECTOR_HOST = "test.account.gov.uk"; private static final String CLIENT_ID = "some-client-id"; + private static final String EXTENSIONS_JOURNEY_TYPE = "extensions.journey_type"; + private static final String EXTENSIONS_COMPONENT_ID = "component_id"; + DynamoEmailCheckResultService dynamoEmailCheckResultService = new DynamoEmailCheckResultService(TEST_CONFIGURATION_SERVICE); @@ -60,6 +70,7 @@ class UpdateEmailIntegrationTest extends ApiGatewayHandlerIntegrationTest { void setup() { handler = new UpdateEmailHandler(TXMA_ENABLED_CONFIGURATION_SERVICE); txmaAuditQueue.clear(); + notificationsQueue.clear(); } @Test @@ -282,14 +293,14 @@ void shouldThrowExceptionWhenSubjectIdMissing() { } @Test - void shouldReturn404IfEmailHasFailedExperianCheck() { + void shouldReturn404IfEmailHasDeniedExperianCheck() { dynamoEmailCheckResultService.saveEmailCheckResult( NEW_EMAIL_ADDRESS, EmailCheckResultStatus.DENY, unixTimePlusNDays(1), "test-reference", CommonTestVariables.JOURNEY_ID, - CommonTestVariables.EMAIL_CHECK_RESPONSE_TEST_DATA); + CommonTestVariables.TEST_EMAIL_CHECK_RESPONSE); var internalCommonSubId = setupUserAndRetrieveInternalCommonSubId(); var otp = redis.generateAndSaveEmailCode(NEW_EMAIL_ADDRESS, 300); @@ -307,6 +318,90 @@ void shouldReturn404IfEmailHasFailedExperianCheck() { requestParams); assertThat(response, hasStatus(HttpStatus.SC_FORBIDDEN)); + + List expectedEvents = List.of(AUTH_EMAIL_FRAUD_CHECK_DECISION_USED); + Map> eventExpectations = new HashMap<>(); + Map fraudCheckDecisionUsedAttributes = new HashMap<>(); + fraudCheckDecisionUsedAttributes.put( + EXTENSIONS_JOURNEY_TYPE, JourneyType.REGISTRATION.getValue()); + fraudCheckDecisionUsedAttributes.put("extensions.decision", "DENY"); + fraudCheckDecisionUsedAttributes.put( + "extensions.emailFraudCheckResponse.type", "EMAIL_FRAUD_CHECK"); + fraudCheckDecisionUsedAttributes.put( + "restricted.domain_name", "digital.cabinet-office.gov.uk"); + eventExpectations.put( + AUTH_EMAIL_FRAUD_CHECK_DECISION_USED.name(), fraudCheckDecisionUsedAttributes); + + verifyAuditEvents(expectedEvents, eventExpectations); + } + + @Test + void shouldReturn204AndSendFraudCheckDecisionUsedAuditEventIfEmailHasPassedExperianCheck() { + var internalCommonSubId = setupUserAndRetrieveInternalCommonSubId(); + dynamoEmailCheckResultService.saveEmailCheckResult( + NEW_EMAIL_ADDRESS, + EmailCheckResultStatus.ALLOW, + unixTimePlusNDays(1), + "test-reference", + CommonTestVariables.JOURNEY_ID, + CommonTestVariables.TEST_EMAIL_CHECK_RESPONSE); + var otp = redis.generateAndSaveEmailCode(NEW_EMAIL_ADDRESS, 300); + + Map requestParams = + Map.of("principalId", internalCommonSubId, "clientId", CLIENT_ID); + + var response = + makeRequest( + Optional.of( + new UpdateEmailRequest( + EXISTING_EMAIL_ADDRESS, NEW_EMAIL_ADDRESS, otp)), + Collections.emptyMap(), + Collections.emptyMap(), + Collections.emptyMap(), + requestParams); + + assertThat(response, hasStatus(HttpStatus.SC_NO_CONTENT)); + assertThat(userStore.getEmailForUser(SUBJECT), is(NEW_EMAIL_ADDRESS)); + + List expectedEvents = + List.of(AUTH_UPDATE_EMAIL, AUTH_EMAIL_FRAUD_CHECK_DECISION_USED); + Map> eventExpectations = new HashMap<>(); + Map fraudCheckDecisionUsedAttributes = new HashMap<>(); + fraudCheckDecisionUsedAttributes.put( + EXTENSIONS_JOURNEY_TYPE, JourneyType.REGISTRATION.getValue()); + fraudCheckDecisionUsedAttributes.put(EXTENSIONS_COMPONENT_ID, "AUTH"); + fraudCheckDecisionUsedAttributes.put( + "extensions.emailFraudCheckResponse.type", "EMAIL_FRAUD_CHECK"); + fraudCheckDecisionUsedAttributes.put( + "restricted.domain_name", "digital.cabinet-office.gov.uk"); + fraudCheckDecisionUsedAttributes.put( + "restricted.domain_name", "digital.cabinet-office.gov.uk"); + eventExpectations.put( + AUTH_EMAIL_FRAUD_CHECK_DECISION_USED.name(), fraudCheckDecisionUsedAttributes); + + verifyAuditEvents(expectedEvents, eventExpectations); + } + + private void verifyAuditEvents( + List expectedEvents, + Map> eventExpectations) { + List receivedEvents = + assertTxmaAuditEventsReceived(txmaAuditQueue, expectedEvents, false); + + for (Map.Entry> eventEntry : eventExpectations.entrySet()) { + String eventName = eventEntry.getKey(); + Map attributes = eventEntry.getValue(); + + AuditEventExpectation expectation = + new AuditEventExpectation(AccountManagementAuditableEvent.valueOf(eventName)); + + for (Map.Entry attributeEntry : attributes.entrySet()) { + expectation.withAttribute(attributeEntry.getKey(), attributeEntry.getValue()); + } + + expectation.assertPublished(receivedEvents); + assertNoTxmaAuditEventsReceived(txmaAuditQueue); + } } private String setupUserAndRetrieveInternalCommonSubId() { diff --git a/audit-events/src/main/java/uk/gov/di/authentication/auditevents/entity/AuthEmailFraudCheckDecisionUsed.java b/audit-events/src/main/java/uk/gov/di/authentication/auditevents/entity/AuthEmailFraudCheckDecisionUsed.java new file mode 100644 index 00000000000..43d8fa7d320 --- /dev/null +++ b/audit-events/src/main/java/uk/gov/di/authentication/auditevents/entity/AuthEmailFraudCheckDecisionUsed.java @@ -0,0 +1,72 @@ +package uk.gov.di.authentication.auditevents.entity; + +import com.google.gson.JsonElement; +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +import java.time.Instant; +import java.util.Objects; + +public record AuthEmailFraudCheckDecisionUsed( + String eventName, + long timestamp, + long eventTimestampMs, + String clientId, + String componentId, + User user, + Extensions extensions, + JsonElement restricted) + implements StructuredAuditEvent { + + private static final String EVENT_NAME = "AUTH_EMAIL_FRAUD_CHECK_DECISION_USED"; + + public AuthEmailFraudCheckDecisionUsed { + Objects.requireNonNull(eventName); + Objects.requireNonNull(eventTimestampMs); + Objects.requireNonNull(componentId); + Objects.requireNonNull(user); + Objects.requireNonNull(extensions); + } + + public static AuthEmailFraudCheckDecisionUsed create( + String clientId, User user, Extensions extensions, JsonElement restricted) { + var now = Instant.now(); + return new AuthEmailFraudCheckDecisionUsed( + EVENT_NAME, + now.getEpochSecond(), + now.toEpochMilli(), + clientId, + ComponentId.AUTH.getValue(), + user, + extensions, + restricted); + } + + public record User( + String userId, + String email, + String ipAddress, + String persistentSessionId, + String govukSigninJourneyId) { + public User { + Objects.requireNonNull(email); + Objects.requireNonNull(ipAddress); + Objects.requireNonNull(persistentSessionId); + Objects.requireNonNull(govukSigninJourneyId); + } + } + + public record Extensions( + @Expose String journeyType, + @Expose String crosscoreRequestReference, + @Expose String decision, + @Expose boolean decision_reused, + @Expose @SerializedName("emailFraudCheckResponse") + JsonElement emailFraudCheckResponse) { + public Extensions { + Objects.requireNonNull(journeyType); + Objects.requireNonNull(decision_reused); + Objects.requireNonNull(decision); + } + } +} diff --git a/frontend-api/src/main/java/uk/gov/di/authentication/frontendapi/domain/FrontendAuditableEvent.java b/frontend-api/src/main/java/uk/gov/di/authentication/frontendapi/domain/FrontendAuditableEvent.java index 96b5040d300..4db17fe69f1 100644 --- a/frontend-api/src/main/java/uk/gov/di/authentication/frontendapi/domain/FrontendAuditableEvent.java +++ b/frontend-api/src/main/java/uk/gov/di/authentication/frontendapi/domain/FrontendAuditableEvent.java @@ -58,7 +58,8 @@ public enum FrontendAuditableEvent implements AuditableEvent { AUTH_REVERIFY_SUCCESSFUL_TOKEN_RECEIVED, AUTH_REVERIFY_VERIFICATION_INFO_RECEIVED, AUTH_REAUTH_FAILED, - AUTH_REAUTH_SUCCESS; + AUTH_REAUTH_SUCCESS, + AUTH_EMAIL_FRAUD_CHECK_DECISION_USED; public AuditableEvent parseFromName(String name) { return valueOf(name); diff --git a/frontend-api/src/main/java/uk/gov/di/authentication/frontendapi/lambda/CheckEmailFraudBlockHandler.java b/frontend-api/src/main/java/uk/gov/di/authentication/frontendapi/lambda/CheckEmailFraudBlockHandler.java index 832fe34b129..a848c0d1d54 100644 --- a/frontend-api/src/main/java/uk/gov/di/authentication/frontendapi/lambda/CheckEmailFraudBlockHandler.java +++ b/frontend-api/src/main/java/uk/gov/di/authentication/frontendapi/lambda/CheckEmailFraudBlockHandler.java @@ -6,10 +6,12 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import uk.gov.di.authentication.auditevents.entity.AuthEmailFraudCheckBypassed; +import uk.gov.di.authentication.auditevents.entity.AuthEmailFraudCheckDecisionUsed; import uk.gov.di.authentication.auditevents.services.StructuredAuditService; import uk.gov.di.authentication.frontendapi.entity.CheckEmailFraudBlockRequest; import uk.gov.di.authentication.frontendapi.entity.CheckEmailFraudBlockResponse; import uk.gov.di.authentication.shared.entity.EmailCheckResultStatus; +import uk.gov.di.authentication.shared.entity.EmailCheckResultStore; import uk.gov.di.authentication.shared.entity.JourneyType; import uk.gov.di.authentication.shared.helpers.ClientSessionIdHelper; import uk.gov.di.authentication.shared.helpers.IpAddressHelper; @@ -24,7 +26,11 @@ import uk.gov.di.authentication.shared.services.DynamoEmailCheckResultService; import uk.gov.di.authentication.shared.state.UserContext; +import java.util.Objects; + import static uk.gov.di.authentication.shared.helpers.ApiGatewayResponseHelper.generateApiGatewayProxyResponse; +import static uk.gov.di.authentication.shared.helpers.EmailCheckResultExtractorHelper.getEmailFraudCheckResponseJsonFromResult; +import static uk.gov.di.authentication.shared.helpers.EmailCheckResultExtractorHelper.getRestrictedJsonFromResult; public class CheckEmailFraudBlockHandler extends BaseFrontendHandler { @@ -87,7 +93,10 @@ public APIGatewayProxyResponseEvent handleRequestWithUserContext( var checkEmailFraudBlockResponse = createResponse(request.getEmail(), status); if (status.equals(EmailCheckResultStatus.PENDING)) { - submitAuditEvent(input, userContext, request); + submitEmailFraudCheckBypassedAuditEvent(input, userContext, request); + } else { + submitEmailFraudCheckDecisionUsedAuditEvent( + input, userContext, request, emailCheckResult.get()); } return generateApiGatewayProxyResponse(200, checkEmailFraudBlockResponse); @@ -102,7 +111,7 @@ private CheckEmailFraudBlockResponse createResponse( return new CheckEmailFraudBlockResponse(email, status.getValue()); } - private void submitAuditEvent( + private void submitEmailFraudCheckBypassedAuditEvent( APIGatewayProxyRequestEvent input, UserContext userContext, CheckEmailFraudBlockRequest request) { @@ -123,4 +132,37 @@ private void submitAuditEvent( auditService.submitAuditEvent(newAuditEvent); } + + private void submitEmailFraudCheckDecisionUsedAuditEvent( + APIGatewayProxyRequestEvent input, + UserContext userContext, + CheckEmailFraudBlockRequest request, + EmailCheckResultStore emailCheckResult) { + var decision_reused = + !Objects.equals( + ClientSessionIdHelper.extractSessionIdFromHeaders(input.getHeaders()), + emailCheckResult.getGovukSigninJourneyId()); + var newAuditEvent = + AuthEmailFraudCheckDecisionUsed.create( + userContext.getAuthSession().getClientId(), + new AuthEmailFraudCheckDecisionUsed.User( + StructuredAuditService.UNKNOWN, + request.getEmail(), + IpAddressHelper.extractIpAddress(input), + PersistentIdHelper.extractPersistentIdFromHeaders( + input.getHeaders()), + ClientSessionIdHelper.extractSessionIdFromHeaders( + input.getHeaders())), + new AuthEmailFraudCheckDecisionUsed.Extensions( + JourneyType.REGISTRATION.getValue(), + decision_reused ? emailCheckResult.getReferenceNumber() : null, + emailCheckResult.getStatus().name(), + decision_reused, + decision_reused + ? getEmailFraudCheckResponseJsonFromResult(emailCheckResult) + : null), + decision_reused ? getRestrictedJsonFromResult(emailCheckResult) : null); + + auditService.submitAuditEvent(newAuditEvent); + } } diff --git a/frontend-api/src/test/java/uk/gov/di/authentication/frontendapi/lambda/CheckEmailFraudBlockHandlerTest.java b/frontend-api/src/test/java/uk/gov/di/authentication/frontendapi/lambda/CheckEmailFraudBlockHandlerTest.java index 9644074acb7..b840c6437b7 100644 --- a/frontend-api/src/test/java/uk/gov/di/authentication/frontendapi/lambda/CheckEmailFraudBlockHandlerTest.java +++ b/frontend-api/src/test/java/uk/gov/di/authentication/frontendapi/lambda/CheckEmailFraudBlockHandlerTest.java @@ -2,16 +2,21 @@ import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import com.google.gson.Gson; +import com.google.gson.JsonElement; import com.nimbusds.oauth2.sdk.id.Subject; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.ArgumentCaptor; import org.mockito.MockedStatic; import org.mockito.Mockito; import uk.gov.di.authentication.auditevents.entity.AuthEmailFraudCheckBypassed; +import uk.gov.di.authentication.auditevents.entity.AuthEmailFraudCheckDecisionUsed; import uk.gov.di.authentication.auditevents.services.StructuredAuditService; import uk.gov.di.authentication.frontendapi.entity.CheckEmailFraudBlockRequest; import uk.gov.di.authentication.frontendapi.entity.CheckEmailFraudBlockResponse; @@ -21,6 +26,7 @@ import uk.gov.di.authentication.shared.entity.EmailCheckResultStore; import uk.gov.di.authentication.shared.entity.JourneyType; import uk.gov.di.authentication.shared.entity.UserProfile; +import uk.gov.di.authentication.shared.helpers.CommonTestVariables; import uk.gov.di.authentication.shared.helpers.NowHelper; import uk.gov.di.authentication.shared.helpers.SaltHelper; import uk.gov.di.authentication.shared.services.AuthSessionService; @@ -30,7 +36,9 @@ import uk.gov.di.authentication.shared.services.DynamoEmailCheckResultService; import uk.gov.di.authentication.shared.state.UserContext; +import java.util.Map; import java.util.Optional; +import java.util.stream.Stream; import static java.lang.String.format; import static org.hamcrest.MatcherAssert.assertThat; @@ -117,6 +125,7 @@ void shouldReturnCorrectStatusBasedOnDbResult(EmailCheckResultStatus status) { var resultStore = new EmailCheckResultStore(); resultStore.setStatus(status); + resultStore.setReferenceNumber("some-reference-number"); when(dbMock.getEmailCheckStore(EMAIL)).thenReturn(Optional.of(resultStore)); usingValidSession(); @@ -137,7 +146,8 @@ void shouldReturnCorrectStatusBasedOnDbResult(EmailCheckResultStatus status) { } @Test - void shouldSubmitAuditWithPendingStatusWhenEmailCheckResultStoreNotPresent() { + void + shouldSubmitEmailFraudCheckBypassedAuditEventWithPendingStatusWhenEmailCheckResultStoreNotPresent() { APIGatewayProxyRequestEvent.ProxyRequestContext proxyRequestContext = getProxyRequestContext(); @@ -192,6 +202,76 @@ void shouldSubmitAuditWithPendingStatusWhenEmailCheckResultStoreNotPresent() { } } + private static Stream successfulEmailCheckResultStatus() { + return Stream.of( + Arguments.of(EmailCheckResultStatus.ALLOW), + Arguments.of(EmailCheckResultStatus.DENY)); + } + + @ParameterizedTest + @MethodSource("successfulEmailCheckResultStatus") + void shouldSubmitEmailCheckDecisionUsedAuditEventWhenEmailCheckIsPresent( + EmailCheckResultStatus status) { + Gson gson = new Gson(); + APIGatewayProxyRequestEvent.ProxyRequestContext proxyRequestContext = + getProxyRequestContext(); + var resultStore = new EmailCheckResultStore(); + var mockEmailCheckResponse = CommonTestVariables.TEST_EMAIL_CHECK_RESPONSE; + AuthEmailFraudCheckDecisionUsed.Extensions expectedExtensions = + new AuthEmailFraudCheckDecisionUsed.Extensions( + JourneyType.REGISTRATION.getValue(), + "some-reference-number", + status.getValue(), + true, + gson.toJsonTree(Map.of("type", "EMAIL_FRAUD_CHECK"))); + JsonElement expectedRestricted = + gson.toJsonTree(Map.of("domain_name", "digital.cabinet-office.gov.uk")); + resultStore.setStatus(status); + resultStore.setReferenceNumber("some-reference-number"); + resultStore.setEmailCheckResponse(mockEmailCheckResponse); + when(dbMock.getEmailCheckStore(EMAIL)).thenReturn(Optional.of(resultStore)); + + usingValidSession(); + + long mockedTimestamp = 1719376320; + try (MockedStatic mockedNowHelperClass = Mockito.mockStatic(NowHelper.class)) { + mockedNowHelperClass + .when(() -> NowHelper.toUnixTimestamp(NowHelper.now())) + .thenReturn(mockedTimestamp); + + var event = + new APIGatewayProxyRequestEvent() + .withHeaders(VALID_HEADERS) + .withRequestContext(proxyRequestContext) + .withBody(format("{ \"email\": \"%s\" }", EMAIL)); + + var expectedResponse = new CheckEmailFraudBlockResponse(EMAIL, status.getValue()); + var result = + handler.handleRequestWithUserContext( + event, + contextMock, + new CheckEmailFraudBlockRequest(EMAIL), + userContext); + + assertThat(result, hasStatus(200)); + assertThat(result, hasJsonBody(expectedResponse)); + + ArgumentCaptor auditEventCaptor = + ArgumentCaptor.forClass(AuthEmailFraudCheckDecisionUsed.class); + verify(auditServiceMock).submitAuditEvent(auditEventCaptor.capture()); + + AuthEmailFraudCheckDecisionUsed capturedEvent = auditEventCaptor.getValue(); + assertThat(capturedEvent.eventName(), is("AUTH_EMAIL_FRAUD_CHECK_DECISION_USED")); + assertThat(capturedEvent.clientId(), is(CLIENT_ID)); + assertThat(capturedEvent.user().email(), is(EMAIL)); + assertThat(capturedEvent.user().ipAddress(), is(IP_ADDRESS)); + assertThat(capturedEvent.user().persistentSessionId(), is(DI_PERSISTENT_SESSION_ID)); + assertThat(capturedEvent.user().govukSigninJourneyId(), is(CLIENT_SESSION_ID)); + assertThat(capturedEvent.extensions(), is(expectedExtensions)); + assertThat(capturedEvent.restricted(), is(expectedRestricted)); + } + } + private void usingValidSession() { when(authSessionServiceMock.getSessionFromRequestHeaders(anyMap())) .thenReturn(Optional.of(authSession)); diff --git a/integration-tests/src/test/java/uk/gov/di/authentication/api/CheckEmailFraudBlockIntegrationTest.java b/integration-tests/src/test/java/uk/gov/di/authentication/api/CheckEmailFraudBlockIntegrationTest.java index 09d303388e2..b62dd79e086 100644 --- a/integration-tests/src/test/java/uk/gov/di/authentication/api/CheckEmailFraudBlockIntegrationTest.java +++ b/integration-tests/src/test/java/uk/gov/di/authentication/api/CheckEmailFraudBlockIntegrationTest.java @@ -27,6 +27,7 @@ import static java.lang.String.format; import static org.hamcrest.MatcherAssert.assertThat; import static uk.gov.di.authentication.frontendapi.domain.FrontendAuditableEvent.AUTH_EMAIL_FRAUD_CHECK_BYPASSED; +import static uk.gov.di.authentication.frontendapi.domain.FrontendAuditableEvent.AUTH_EMAIL_FRAUD_CHECK_DECISION_USED; import static uk.gov.di.authentication.shared.helpers.NowHelper.unixTimePlusNDays; import static uk.gov.di.authentication.sharedtest.helper.AuditAssertionsHelper.assertNoTxmaAuditEventsReceived; import static uk.gov.di.authentication.sharedtest.helper.AuditAssertionsHelper.assertTxmaAuditEventsReceived; @@ -84,9 +85,9 @@ void whenNoFraudCheckResultExists() { EmailCheckResultStatus.PENDING.getValue()))); List expectedEvents = List.of(AUTH_EMAIL_FRAUD_CHECK_BYPASSED); - Map> eventExpectations = new HashMap<>(); + Map> eventExpectations = new HashMap<>(); - Map fraudCheckBypassedAttributes = new HashMap<>(); + Map fraudCheckBypassedAttributes = new HashMap<>(); fraudCheckBypassedAttributes.put( EXTENSIONS_JOURNEY_TYPE, JourneyType.REGISTRATION.getValue()); fraudCheckBypassedAttributes.put(EXTENSIONS_COMPONENT_ID, "AUTH"); @@ -108,7 +109,7 @@ void whenEmailIsAllowed() { unixTimePlusNDays(1), "test-reference", CommonTestVariables.JOURNEY_ID, - CommonTestVariables.EMAIL_CHECK_RESPONSE_TEST_DATA); + CommonTestVariables.TEST_EMAIL_CHECK_RESPONSE); Map headers = constructFrontendHeaders(sessionId, CommonTestVariables.CLIENT_SESSION_ID); @@ -127,7 +128,21 @@ void whenEmailIsAllowed() { CommonTestVariables.EMAIL, EmailCheckResultStatus.ALLOW.getValue()))); - assertNoTxmaAuditEventsReceived(txmaAuditQueue); + List expectedEvents = List.of(AUTH_EMAIL_FRAUD_CHECK_DECISION_USED); + Map> eventExpectations = new HashMap<>(); + Map fraudCheckDecisionUsedAttributes = new HashMap<>(); + fraudCheckDecisionUsedAttributes.put( + EXTENSIONS_JOURNEY_TYPE, JourneyType.REGISTRATION.getValue()); + fraudCheckDecisionUsedAttributes.put(EXTENSIONS_COMPONENT_ID, "AUTH"); + fraudCheckDecisionUsedAttributes.put("extensions.decision", "ALLOW"); + fraudCheckDecisionUsedAttributes.put( + "extensions.emailFraudCheckResponse.type", "EMAIL_FRAUD_CHECK"); + fraudCheckDecisionUsedAttributes.put( + "restricted.domain_name", "digital.cabinet-office.gov.uk"); + eventExpectations.put( + AUTH_EMAIL_FRAUD_CHECK_DECISION_USED.name(), fraudCheckDecisionUsedAttributes); + + verifyAuditEvents(expectedEvents, eventExpectations); } @Test @@ -142,7 +157,7 @@ void whenEmailIsDenied() { unixTimePlusNDays(1), "test-reference", CommonTestVariables.JOURNEY_ID, - CommonTestVariables.EMAIL_CHECK_RESPONSE_TEST_DATA); + CommonTestVariables.TEST_EMAIL_CHECK_RESPONSE); Map headers = constructFrontendHeaders(sessionId, CommonTestVariables.CLIENT_SESSION_ID); @@ -161,24 +176,38 @@ void whenEmailIsDenied() { CommonTestVariables.EMAIL, EmailCheckResultStatus.DENY.getValue()))); - assertNoTxmaAuditEventsReceived(txmaAuditQueue); + List expectedEvents = List.of(AUTH_EMAIL_FRAUD_CHECK_DECISION_USED); + Map> eventExpectations = new HashMap<>(); + Map fraudCheckDecisionUsedAttributes = new HashMap<>(); + fraudCheckDecisionUsedAttributes.put( + EXTENSIONS_JOURNEY_TYPE, JourneyType.REGISTRATION.getValue()); + fraudCheckDecisionUsedAttributes.put(EXTENSIONS_COMPONENT_ID, "AUTH"); + fraudCheckDecisionUsedAttributes.put("extensions.decision", "DENY"); + fraudCheckDecisionUsedAttributes.put( + "extensions.emailFraudCheckResponse.type", "EMAIL_FRAUD_CHECK"); + fraudCheckDecisionUsedAttributes.put( + "restricted.domain_name", "digital.cabinet-office.gov.uk"); + eventExpectations.put( + AUTH_EMAIL_FRAUD_CHECK_DECISION_USED.name(), fraudCheckDecisionUsedAttributes); + + verifyAuditEvents(expectedEvents, eventExpectations); } } private void verifyAuditEvents( List expectedEvents, - Map> eventExpectations) { + Map> eventExpectations) { List receivedEvents = assertTxmaAuditEventsReceived(txmaAuditQueue, expectedEvents, false); - for (Map.Entry> eventEntry : eventExpectations.entrySet()) { + for (Map.Entry> eventEntry : eventExpectations.entrySet()) { String eventName = eventEntry.getKey(); - Map attributes = eventEntry.getValue(); + Map attributes = eventEntry.getValue(); AuditEventExpectation expectation = new AuditEventExpectation(FrontendAuditableEvent.valueOf(eventName)); - for (Map.Entry attributeEntry : attributes.entrySet()) { + for (Map.Entry attributeEntry : attributes.entrySet()) { expectation.withAttribute(attributeEntry.getKey(), attributeEntry.getValue()); } diff --git a/integration-tests/src/test/java/uk/gov/di/authentication/services/DynamoEmailCheckResultServiceIntegrationTest.java b/integration-tests/src/test/java/uk/gov/di/authentication/services/DynamoEmailCheckResultServiceIntegrationTest.java index d68da734d8c..565b2b514a9 100644 --- a/integration-tests/src/test/java/uk/gov/di/authentication/services/DynamoEmailCheckResultServiceIntegrationTest.java +++ b/integration-tests/src/test/java/uk/gov/di/authentication/services/DynamoEmailCheckResultServiceIntegrationTest.java @@ -1,7 +1,9 @@ package uk.gov.di.authentication.services; +import com.google.gson.JsonParser; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import uk.gov.di.authentication.shared.entity.EmailCheckResponse; import uk.gov.di.authentication.shared.entity.EmailCheckResultStatus; import uk.gov.di.authentication.shared.helpers.CommonTestVariables; import uk.gov.di.authentication.shared.helpers.NowHelper; @@ -10,8 +12,6 @@ import uk.gov.di.authentication.sharedtest.extensions.EmailCheckResultExtension; import java.time.temporal.ChronoUnit; -import java.util.List; -import java.util.Map; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; @@ -35,7 +35,7 @@ class DynamoEmailCheckResultServiceIntegrationTest { @Test void shouldSaveAndReadAnEmailCheckResult() { - var testResponseData = CommonTestVariables.EMAIL_CHECK_RESPONSE_TEST_DATA; + var testResponseData = CommonTestVariables.TEST_EMAIL_CHECK_RESPONSE; dynamoEmailCheckResultService.saveEmailCheckResult( email, @@ -55,19 +55,13 @@ void shouldSaveAndReadAnEmailCheckResult() { var responseSection = result.get().getEmailCheckResponse(); assertNotNull(responseSection); - var responseMap = (Map) responseSection; - assertThat(responseMap.get("testString"), equalTo("testValue1")); - assertThat(((Number) responseMap.get("testNumber")).intValue(), equalTo(456)); - assertThat(responseMap.get("testBoolean"), equalTo(true)); - - var testArray = (List) responseMap.get("testArray"); - assertThat(testArray.size(), equalTo(2)); - assertThat(testArray.get(0), equalTo("testItem1")); - assertThat(testArray.get(1), equalTo("testItem2")); - - var testObject = (Map) responseMap.get("testObject"); - assertThat(testObject.get("testNestedString"), equalTo("testNestedValue")); - assertThat(((Number) testObject.get("testNestedNumber")).intValue(), equalTo(789)); + EmailCheckResponse responseMap = responseSection; + assertThat( + responseMap.extensions(), + equalTo(JsonParser.parseString(CommonTestVariables.extensionsJsonString))); + assertThat( + responseMap.restricted(), + equalTo(JsonParser.parseString(CommonTestVariables.restrictedJsonString))); } @Test @@ -80,7 +74,7 @@ void shouldNotReturnAnEmailCheckResultWhenTimeToLiveHasExpired() { unixTimeInThePast, referenceNumber, CommonTestVariables.JOURNEY_ID, - CommonTestVariables.EMAIL_CHECK_RESPONSE_TEST_DATA); + CommonTestVariables.TEST_EMAIL_CHECK_RESPONSE); var result = dynamoEmailCheckResultService.getEmailCheckStore(email); diff --git a/shared/src/main/java/uk/gov/di/authentication/shared/converters/EmailCheckResponseConverter.java b/shared/src/main/java/uk/gov/di/authentication/shared/converters/EmailCheckResponseConverter.java new file mode 100644 index 00000000000..522d40cc782 --- /dev/null +++ b/shared/src/main/java/uk/gov/di/authentication/shared/converters/EmailCheckResponseConverter.java @@ -0,0 +1,45 @@ +package uk.gov.di.authentication.shared.converters; + +import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import uk.gov.di.authentication.shared.entity.EmailCheckResponse; +import uk.gov.di.authentication.shared.serialization.Json; +import uk.gov.di.authentication.shared.services.SerializationService; + +public class EmailCheckResponseConverter implements AttributeConverter { + + private static final SerializationService serializationService = + SerializationService.getInstance(); + + @Override + public AttributeValue transformFrom(EmailCheckResponse input) { + if (input == null) { + return AttributeValue.builder().nul(true).build(); + } + return AttributeValue.builder().s(serializationService.writeValueAsString(input)).build(); + } + + @Override + public EmailCheckResponse transformTo(AttributeValue input) { + if (input.nul() != null && input.nul()) { + return null; + } + try { + return serializationService.readValue(input.s(), EmailCheckResponse.class); + } catch (Json.JsonException e) { + throw new RuntimeException("Failed to deserialize EmailCheckResponse", e); + } + } + + @Override + public EnhancedType type() { + return EnhancedType.of(EmailCheckResponse.class); + } + + @Override + public AttributeValueType attributeValueType() { + return AttributeValueType.S; + } +} diff --git a/shared/src/main/java/uk/gov/di/authentication/shared/entity/EmailCheckResponse.java b/shared/src/main/java/uk/gov/di/authentication/shared/entity/EmailCheckResponse.java new file mode 100644 index 00000000000..5f908f09d27 --- /dev/null +++ b/shared/src/main/java/uk/gov/di/authentication/shared/entity/EmailCheckResponse.java @@ -0,0 +1,20 @@ +package uk.gov.di.authentication.shared.entity; + +import com.google.gson.JsonElement; +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public record EmailCheckResponse( + @Expose @SerializedName("extensions") JsonElement extensions, + @Expose @SerializedName("restricted") JsonElement restricted) { + + @Override + public JsonElement extensions() { + return this.extensions; + } + + @Override + public JsonElement restricted() { + return this.restricted; + } +} diff --git a/shared/src/main/java/uk/gov/di/authentication/shared/entity/EmailCheckResultSqsMessage.java b/shared/src/main/java/uk/gov/di/authentication/shared/entity/EmailCheckResultSqsMessage.java index 0ba5de20f43..05c3f158991 100644 --- a/shared/src/main/java/uk/gov/di/authentication/shared/entity/EmailCheckResultSqsMessage.java +++ b/shared/src/main/java/uk/gov/di/authentication/shared/entity/EmailCheckResultSqsMessage.java @@ -11,4 +11,4 @@ public record EmailCheckResultSqsMessage( @Expose @SerializedName("RequestReference") @Required String requestReference, @Expose @SerializedName("TimeOfInitialRequest") @Required long timeOfInitialRequest, @Expose @SerializedName("GovukSigninJourneyId") String govukSigninJourneyId, - @Expose @SerializedName("EmailCheckResponse") Object emailCheckResponse) {} + @Expose @SerializedName("EmailCheckResponse") EmailCheckResponse emailCheckResponse) {} diff --git a/shared/src/main/java/uk/gov/di/authentication/shared/entity/EmailCheckResultStore.java b/shared/src/main/java/uk/gov/di/authentication/shared/entity/EmailCheckResultStore.java index bf0b065a679..4c3d5e77e3c 100644 --- a/shared/src/main/java/uk/gov/di/authentication/shared/entity/EmailCheckResultStore.java +++ b/shared/src/main/java/uk/gov/di/authentication/shared/entity/EmailCheckResultStore.java @@ -4,7 +4,7 @@ import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbConvertedBy; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; -import uk.gov.di.authentication.shared.converters.ObjectConverter; +import uk.gov.di.authentication.shared.converters.EmailCheckResponseConverter; @DynamoDbBean public class EmailCheckResultStore { @@ -21,7 +21,7 @@ public class EmailCheckResultStore { private long timeToExist; private String referenceNumber; private String govukSigninJourneyId; - private Object emailCheckResponse; + private EmailCheckResponse emailCheckResponse; @DynamoDbPartitionKey @DynamoDbAttribute(ATTRIBUTE_EMAIL) @@ -95,16 +95,16 @@ public EmailCheckResultStore withGovukSigninJourneyId(String govukSigninJourneyI } @DynamoDbAttribute(ATTRIBUTE_EMAIL_CHECK_RESPONSE) - @DynamoDbConvertedBy(ObjectConverter.class) - public Object getEmailCheckResponse() { + @DynamoDbConvertedBy(EmailCheckResponseConverter.class) + public EmailCheckResponse getEmailCheckResponse() { return emailCheckResponse; } - public void setEmailCheckResponse(Object emailCheckResponse) { + public void setEmailCheckResponse(EmailCheckResponse emailCheckResponse) { this.emailCheckResponse = emailCheckResponse; } - public EmailCheckResultStore withEmailCheckResponse(Object emailCheckResponse) { + public EmailCheckResultStore withEmailCheckResponse(EmailCheckResponse emailCheckResponse) { this.emailCheckResponse = emailCheckResponse; return this; } diff --git a/shared/src/main/java/uk/gov/di/authentication/shared/helpers/CommonTestVariables.java b/shared/src/main/java/uk/gov/di/authentication/shared/helpers/CommonTestVariables.java index 5d28b9755bd..cf1d97f20ad 100644 --- a/shared/src/main/java/uk/gov/di/authentication/shared/helpers/CommonTestVariables.java +++ b/shared/src/main/java/uk/gov/di/authentication/shared/helpers/CommonTestVariables.java @@ -1,5 +1,7 @@ package uk.gov.di.authentication.shared.helpers; +import com.google.gson.JsonParser; +import uk.gov.di.authentication.shared.entity.EmailCheckResponse; import uk.gov.di.authentication.shared.entity.PriorityIdentifier; import uk.gov.di.authentication.shared.entity.mfa.MFAMethod; @@ -83,4 +85,23 @@ public class CommonTestVariables { List.of("testItem1", "testItem2"), "testObject", Map.of("testNestedString", "testNestedValue", "testNestedNumber", 789)); + public static final String extensionsJsonString = + """ + { + "emailFraudCheckResponse": { + "type": "EMAIL_FRAUD_CHECK" + } + } + """; + + public static final String restrictedJsonString = + """ + { + "domain_name": "digital.cabinet-office.gov.uk" + } + """; + public static final EmailCheckResponse TEST_EMAIL_CHECK_RESPONSE = + new EmailCheckResponse( + JsonParser.parseString(extensionsJsonString), + JsonParser.parseString(restrictedJsonString)); } diff --git a/shared/src/main/java/uk/gov/di/authentication/shared/helpers/EmailCheckResultExtractorHelper.java b/shared/src/main/java/uk/gov/di/authentication/shared/helpers/EmailCheckResultExtractorHelper.java new file mode 100644 index 00000000000..b50adf760ab --- /dev/null +++ b/shared/src/main/java/uk/gov/di/authentication/shared/helpers/EmailCheckResultExtractorHelper.java @@ -0,0 +1,25 @@ +package uk.gov.di.authentication.shared.helpers; + +import com.google.gson.JsonElement; +import uk.gov.di.authentication.shared.entity.EmailCheckResponse; +import uk.gov.di.authentication.shared.entity.EmailCheckResultStore; + +import java.util.Optional; + +public class EmailCheckResultExtractorHelper { + + public static JsonElement getEmailFraudCheckResponseJsonFromResult( + EmailCheckResultStore emailCheckResult) { + return Optional.ofNullable(emailCheckResult.getEmailCheckResponse()) + .map(EmailCheckResponse::extensions) + .map(res -> res.getAsJsonObject()) + .map(json -> json.get("emailFraudCheckResponse")) + .orElse(null); + } + + public static JsonElement getRestrictedJsonFromResult(EmailCheckResultStore emailCheckResult) { + return Optional.ofNullable(emailCheckResult.getEmailCheckResponse()) + .map(EmailCheckResponse::restricted) + .orElse(null); + } +} diff --git a/shared/src/main/java/uk/gov/di/authentication/shared/services/DynamoEmailCheckResultService.java b/shared/src/main/java/uk/gov/di/authentication/shared/services/DynamoEmailCheckResultService.java index 39218843c68..b4fd224ec20 100644 --- a/shared/src/main/java/uk/gov/di/authentication/shared/services/DynamoEmailCheckResultService.java +++ b/shared/src/main/java/uk/gov/di/authentication/shared/services/DynamoEmailCheckResultService.java @@ -1,5 +1,6 @@ package uk.gov.di.authentication.shared.services; +import uk.gov.di.authentication.shared.entity.EmailCheckResponse; import uk.gov.di.authentication.shared.entity.EmailCheckResultStatus; import uk.gov.di.authentication.shared.entity.EmailCheckResultStore; import uk.gov.di.authentication.shared.helpers.NowHelper; @@ -23,7 +24,7 @@ public void saveEmailCheckResult( Long timeToExist, String referenceNumber, String govukSigninJourneyId, - Object emailCheckResponse) { + EmailCheckResponse emailCheckResponse) { var emailCheckResult = new EmailCheckResultStore() .withEmail(email) diff --git a/shared/src/test/java/uk/gov/di/authentication/shared/converters/EmailCheckResponseConverterTest.java b/shared/src/test/java/uk/gov/di/authentication/shared/converters/EmailCheckResponseConverterTest.java new file mode 100644 index 00000000000..dce192edf20 --- /dev/null +++ b/shared/src/test/java/uk/gov/di/authentication/shared/converters/EmailCheckResponseConverterTest.java @@ -0,0 +1,47 @@ +package uk.gov.di.authentication.shared.converters; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import uk.gov.di.authentication.shared.entity.EmailCheckResponse; +import uk.gov.di.authentication.shared.helpers.CommonTestVariables; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class EmailCheckResponseConverterTest { + + private EmailCheckResponseConverter emailCheckResponseConverter; + + @BeforeEach + public void setUp() { + emailCheckResponseConverter = new EmailCheckResponseConverter(); + } + + @Test + void shouldTransformFromEmailCheckResponseToAttributeValue() { + EmailCheckResponse input = CommonTestVariables.TEST_EMAIL_CHECK_RESPONSE; + + AttributeValue result = emailCheckResponseConverter.transformFrom(input); + + String jsonString = result.s(); + assertTrue(jsonString.contains("\"domain_name\":\"digital.cabinet-office.gov.uk\"")); + assertTrue( + jsonString.contains( + "\"emailFraudCheckResponse\":{\"type\":\"EMAIL_FRAUD_CHECK\"}")); + } + + @Test + void shouldTransformToObjectFromAttributeValue() { + AttributeValue attributeValue = + AttributeValue.builder() + .s( + "{\"restricted\":{\"domain_name\":\"digital.cabinet-office.gov.uk\"},\"extensions\":{\"emailFraudCheckResponse\":{\"type\":\"EMAIL_FRAUD_CHECK\"}}}") + .build(); + + Object result = emailCheckResponseConverter.transformTo(attributeValue); + + assertTrue(result instanceof EmailCheckResponse); + assertEquals(CommonTestVariables.TEST_EMAIL_CHECK_RESPONSE, result); + } +} diff --git a/utils/build.gradle b/utils/build.gradle index 2a2ea7c246f..fbb798b87b3 100644 --- a/utils/build.gradle +++ b/utils/build.gradle @@ -20,7 +20,8 @@ dependencies { libs.bundles.xray, platform(libs.openTelemetryBom), libs.openTelemetryAwsSdk, - libs.awsCloudwatch + libs.awsCloudwatch, + libs.gson runtimeOnly libs.bundles.loggingRuntime diff --git a/utils/src/test/java/uk/gov/di/authentication/utils/lambda/EmailCheckResultWriterHandlerTest.java b/utils/src/test/java/uk/gov/di/authentication/utils/lambda/EmailCheckResultWriterHandlerTest.java index 4b470243b78..022e7e8cd7a 100644 --- a/utils/src/test/java/uk/gov/di/authentication/utils/lambda/EmailCheckResultWriterHandlerTest.java +++ b/utils/src/test/java/uk/gov/di/authentication/utils/lambda/EmailCheckResultWriterHandlerTest.java @@ -9,6 +9,7 @@ import org.mockito.MockedStatic; import org.mockito.Mockito; import software.amazon.awssdk.annotations.NotNull; +import uk.gov.di.authentication.shared.entity.EmailCheckResponse; import uk.gov.di.authentication.shared.entity.EmailCheckResultStatus; import uk.gov.di.authentication.shared.helpers.NowHelper; import uk.gov.di.authentication.shared.services.CloudwatchMetricsService; @@ -17,7 +18,6 @@ import java.time.Instant; import java.util.Date; import java.util.List; -import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -40,7 +40,7 @@ class EmailCheckResultWriterHandlerTest { private static ArgumentCaptor timeToExistCaptor; private static ArgumentCaptor referenceNumberCaptor; private static ArgumentCaptor govukSigninJourneyIdCaptor; - private static ArgumentCaptor emailCheckResponseCaptor; + private static ArgumentCaptor emailCheckResponseCaptor; private EmailCheckResultWriterHandler handler; @BeforeAll @@ -52,7 +52,7 @@ static void init() { timeToExistCaptor = ArgumentCaptor.forClass(Long.class); referenceNumberCaptor = ArgumentCaptor.forClass(String.class); govukSigninJourneyIdCaptor = ArgumentCaptor.forClass(String.class); - emailCheckResponseCaptor = ArgumentCaptor.forClass(Object.class); + emailCheckResponseCaptor = ArgumentCaptor.forClass(EmailCheckResponse.class); } @BeforeEach @@ -84,26 +84,27 @@ void shouldProcessValidSQSEventWithSingleMessageAndSaveToDatabase() { assertEquals(TEST_MSG_REF_NUMBER, referenceNumberCaptor.getValue()); assertEquals("test-journey-id", govukSigninJourneyIdCaptor.getValue()); - var capturedResponseSection = emailCheckResponseCaptor.getValue(); - assertNotNull(capturedResponseSection); + var capturedEmailCheckResponse = emailCheckResponseCaptor.getValue(); + assertNotNull(capturedEmailCheckResponse); - var responseMap = (Map) capturedResponseSection; - assertEquals("testValue1", responseMap.get("testString")); - assertEquals(123, ((Number) responseMap.get("testNumber")).intValue()); - assertEquals(true, responseMap.get("testBoolean")); + var extensions = capturedEmailCheckResponse.extensions().getAsJsonObject(); + assertEquals("testValue1", extensions.get("extensionsTestString").getAsString()); + assertEquals(123, extensions.get("extensionsTestNumber").getAsNumber().intValue()); + assertEquals(true, extensions.get("extensionsTestBoolean").getAsBoolean()); - var testArray = (List) responseMap.get("testArray"); - assertEquals(2, testArray.size()); - assertEquals("testItem1", testArray.get(0)); - assertEquals("testItem2", testArray.get(1)); + var testObject = extensions.get("extensionsTestObject").getAsJsonObject(); + assertEquals( + "testNestedValue", testObject.get("extensionsTestNestedString").getAsString()); + assertEquals( + 456, testObject.get("extensionsTestNestedNumber").getAsNumber().intValue()); - var testObject = (Map) responseMap.get("testObject"); - assertEquals("testNestedValue", testObject.get("testNestedString")); - assertEquals(456, ((Number) testObject.get("testNestedNumber")).intValue()); + var testChildObject = testObject.get("extensionsTestChildObject").getAsJsonObject(); + assertEquals( + "testDeepValue", testChildObject.get("extensionsTestDeepString").getAsString()); + assertEquals(false, testChildObject.get("extensionsTestDeepBoolean").getAsBoolean()); - var testChildObject = (Map) testObject.get("testChildObject"); - assertEquals("testDeepValue", testChildObject.get("testDeepString")); - assertEquals(false, testChildObject.get("testDeepBoolean")); + var restricted = capturedEmailCheckResponse.restricted().getAsJsonObject(); + assertEquals("testValue2", restricted.get("restrictedTestString").getAsString()); verify(cloudWatchMock).logEmailCheckDuration(REQUEST_DURATION); } @@ -169,17 +170,22 @@ private static SQSEvent getSqsEventWithSingleMessage( "TimeOfInitialRequest": %d, "GovukSigninJourneyId": "test-journey-id", "EmailCheckResponse": { - "testString": "testValue1", - "testNumber": 123, - "testBoolean": true, - "testArray": ["testItem1", "testItem2"], - "testObject": { - "testNestedString": "testNestedValue", - "testNestedNumber": 456, - "testChildObject": { - "testDeepString": "testDeepValue", - "testDeepBoolean": false - } + "extensions": { + "extensionsTestString": "testValue1", + "extensionsTestNumber": 123, + "extensionsTestBoolean": true, + "extensionsTestArray": ["testItem1", "testItem2"], + "extensionsTestObject": { + "extensionsTestNestedString": "testNestedValue", + "extensionsTestNestedNumber": 456, + "extensionsTestChildObject": { + "extensionsTestDeepString": "testDeepValue", + "extensionsTestDeepBoolean": false + } + } + }, + "restricted": { + "restrictedTestString": "testValue2" } } }""",