From 24ef58320f3b2c4c660901249c6eb5f7947533b4 Mon Sep 17 00:00:00 2001 From: Aidan Comer Date: Tue, 7 Oct 2025 13:50:31 +0100 Subject: [PATCH 1/8] BAU: Add missing UserAccountSuspended schema --- ci/terraform/account-management/openapi_v2.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ci/terraform/account-management/openapi_v2.yaml b/ci/terraform/account-management/openapi_v2.yaml index 108b64c1f31..21d3fbf74db 100644 --- a/ci/terraform/account-management/openapi_v2.yaml +++ b/ci/terraform/account-management/openapi_v2.yaml @@ -1092,6 +1092,14 @@ components: example: code: 1080 message: "Default method already exists, new one cannot be created." + UserAccountSuspended: + allOf: + - $ref: "#/components/schemas/SimpleError" + - type: object + properties: + code: + type: integer + enum: [ 1083 ] UserAccountBlocked: allOf: - $ref: "#/components/schemas/SimpleError" From 007248e1e0c4b745d3031cb651b3d732fbe1c267 Mon Sep 17 00:00:00 2001 From: Aidan Comer Date: Tue, 7 Oct 2025 13:57:45 +0100 Subject: [PATCH 2/8] AUT-4506: Add new /authenticate request body with optional field - Optional targetAction field added. To be used in the AUTH_ACCOUNT_MANAGEMENT_AUTHENTICATE audit event for a target_action extension. - Added missing request body information for /authenticate spec - Bumped openapi spec minor version --- .../account-management/openapi_v2.yaml | 49 ++++++++++++++++++- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/ci/terraform/account-management/openapi_v2.yaml b/ci/terraform/account-management/openapi_v2.yaml index 21d3fbf74db..02473f79e91 100644 --- a/ci/terraform/account-management/openapi_v2.yaml +++ b/ci/terraform/account-management/openapi_v2.yaml @@ -1,7 +1,7 @@ openapi: "3.0.1" info: title: "auth-account-management-method-management-api" - version: 1.0.2 + version: 1.1.0 description: Auth Account Management API servers: - url: "https://localhost:8080/" @@ -16,6 +16,24 @@ paths: uri: "${endpoint_modules.authenticate.integration_uri}" passthroughBehavior: "when_no_match" timeoutInMillis: 29000 + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/AuthenticateRequest" + examples: + authenticate-request: + summary: Authenticate user with optional target action + value: + email: "user@example.gov.uk" + password: "password123" + postAuthAction: "UPDATE_EMAIL" + authenticate-request-no-target-action: + summary: Authenticate user request with no target action for backwards compatability + value: + email: "user@example.gov.uk" + password: "password123" + required: true responses: "204": description: Successful Operation. The password is correct for this user. @@ -1099,7 +1117,7 @@ components: properties: code: type: integer - enum: [ 1083 ] + enum: [1083] UserAccountBlocked: allOf: - $ref: "#/components/schemas/SimpleError" @@ -1190,6 +1208,33 @@ components: example: code: 1089 message: "Email address is denied" + AuthenticateRequest: + required: + - email + - password + type: object + properties: + email: + type: string + format: email + description: The user's email address + example: "user@example.gov.uk" + password: + type: string + description: The user's password + example: "password123" + postAuthAction: + type: string + enum: + - UPDATE_EMAIL + - UPDATE_PASSWORD + - DELETE_ACCOUNT + description: Optional field indicating the user's intended action + example: "UPDATE_EMAIL" + example: + email: "user@example.gov.uk" + password: "password123" + postAuthAction: "UPDATE_EMAIL" securitySchemes: authorise-access-token: From 7af6e2c5cdaa013577e276907e2e45a3a4266ef0 Mon Sep 17 00:00:00 2001 From: Aidan Comer Date: Tue, 7 Oct 2025 13:59:37 +0100 Subject: [PATCH 3/8] AUT-4506: Use target action to extend audit event - Data requested this extension so that they can identify drop out rate between authenticating and performing the intended action. - The field is optional to maintain backwards compatability. --- .../entity/AuthenticateRequest.java | 6 ++++ .../entity/PostAuthAction.java | 11 +++++++ .../lambda/AuthenticateHandler.java | 10 +++++- .../lambda/AuthenticateHandlerTest.java | 33 ++++++++++++++++--- 4 files changed, 54 insertions(+), 6 deletions(-) create mode 100644 account-management-api/src/main/java/uk/gov/di/accountmanagement/entity/PostAuthAction.java diff --git a/account-management-api/src/main/java/uk/gov/di/accountmanagement/entity/AuthenticateRequest.java b/account-management-api/src/main/java/uk/gov/di/accountmanagement/entity/AuthenticateRequest.java index 5f2f6ba0724..0a2e0e2d558 100644 --- a/account-management-api/src/main/java/uk/gov/di/accountmanagement/entity/AuthenticateRequest.java +++ b/account-management-api/src/main/java/uk/gov/di/accountmanagement/entity/AuthenticateRequest.java @@ -9,6 +9,8 @@ public class AuthenticateRequest { @Expose @Required private String password; + @Expose private PostAuthAction postAuthAction; + public AuthenticateRequest() {} public AuthenticateRequest(String email, String password) { @@ -23,4 +25,8 @@ public String getEmail() { public String getPassword() { return password; } + + public PostAuthAction getPostAuthAction() { + return postAuthAction; + } } diff --git a/account-management-api/src/main/java/uk/gov/di/accountmanagement/entity/PostAuthAction.java b/account-management-api/src/main/java/uk/gov/di/accountmanagement/entity/PostAuthAction.java new file mode 100644 index 00000000000..0509e57f7c0 --- /dev/null +++ b/account-management-api/src/main/java/uk/gov/di/accountmanagement/entity/PostAuthAction.java @@ -0,0 +1,11 @@ +package uk.gov.di.accountmanagement.entity; + +public enum PostAuthAction { + UPDATE_EMAIL, + UPDATE_PASSWORD, + DELETE_ACCOUNT; + + public String getValue() { + return name(); + } +} diff --git a/account-management-api/src/main/java/uk/gov/di/accountmanagement/lambda/AuthenticateHandler.java b/account-management-api/src/main/java/uk/gov/di/accountmanagement/lambda/AuthenticateHandler.java index b9611a6a2d0..228f674209d 100644 --- a/account-management-api/src/main/java/uk/gov/di/accountmanagement/lambda/AuthenticateHandler.java +++ b/account-management-api/src/main/java/uk/gov/di/accountmanagement/lambda/AuthenticateHandler.java @@ -8,6 +8,7 @@ import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.ThreadContext; import uk.gov.di.accountmanagement.entity.AuthenticateRequest; +import uk.gov.di.accountmanagement.entity.PostAuthAction; import uk.gov.di.accountmanagement.helpers.AuditHelper; import uk.gov.di.audit.AuditContext; import uk.gov.di.authentication.shared.entity.AccountInterventionsInboundResponse; @@ -30,11 +31,13 @@ import static uk.gov.di.authentication.shared.helpers.InstrumentationHelper.segmentedFunctionCall; 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 AuthenticateHandler implements RequestHandler { private static final Logger LOG = LogManager.getLogger(AuthenticateHandler.class); + public static final String POST_AUTH_ACTION = "post_auth_action"; private final AuthenticationService authenticationService; private final Json objectMapper = SerializationService.getInstance(); @@ -166,7 +169,12 @@ public APIGatewayProxyResponseEvent authenticateRequestHandler( auditService.submitAuditEvent( AUTH_ACCOUNT_MANAGEMENT_AUTHENTICATE, auditContext, - AUDIT_EVENT_COMPONENT_ID_AUTH); + AUDIT_EVENT_COMPONENT_ID_AUTH, + pair( + POST_AUTH_ACTION, + Optional.ofNullable(loginRequest.getPostAuthAction()) + .map(PostAuthAction::getValue) + .orElse(AuditService.UNKNOWN))); return generateEmptySuccessApiGatewayResponse(); } catch (JsonException e) { diff --git a/account-management-api/src/test/java/uk/gov/di/accountmanagement/lambda/AuthenticateHandlerTest.java b/account-management-api/src/test/java/uk/gov/di/accountmanagement/lambda/AuthenticateHandlerTest.java index bfc545b31d3..be37e31be82 100644 --- a/account-management-api/src/test/java/uk/gov/di/accountmanagement/lambda/AuthenticateHandlerTest.java +++ b/account-management-api/src/test/java/uk/gov/di/accountmanagement/lambda/AuthenticateHandlerTest.java @@ -9,6 +9,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import uk.gov.di.accountmanagement.entity.PostAuthAction; import uk.gov.di.accountmanagement.helpers.AuditHelper; import uk.gov.di.audit.AuditContext; import uk.gov.di.authentication.shared.entity.*; @@ -34,6 +35,7 @@ import static org.mockito.Mockito.when; import static uk.gov.di.accountmanagement.constants.AccountManagementConstants.AUDIT_EVENT_COMPONENT_ID_AUTH; import static uk.gov.di.accountmanagement.domain.AccountManagementAuditableEvent.*; +import static uk.gov.di.authentication.shared.services.AuditService.MetadataPair.pair; import static uk.gov.di.authentication.sharedtest.helper.RequestEventHelper.contextWithSourceIp; import static uk.gov.di.authentication.sharedtest.matchers.APIGatewayProxyResponseEventMatcher.hasJsonBody; import static uk.gov.di.authentication.sharedtest.matchers.APIGatewayProxyResponseEventMatcher.hasStatus; @@ -111,21 +113,40 @@ void setUp() throws UnsuccessfulAccountInterventionsResponseException { new Intervention(1L), new State(false, false, false, false))); } - @Test - void shouldReturn204IfLoginIsSuccessful() { + private static Stream postAuthAction() { + return Stream.of( + Arguments.of("not-a-valid-target-action"), + Arguments.of(PostAuthAction.DELETE_ACCOUNT.getValue()), + Arguments.of(PostAuthAction.UPDATE_EMAIL.getValue()), + Arguments.of(PostAuthAction.UPDATE_PASSWORD.getValue())); + } + + @ParameterizedTest + @MethodSource("postAuthAction") + void shouldReturn204IfLoginIsSuccessful(String postAuthAction) { when(authenticationService.getUserProfileByEmailMaybe(EMAIL)) .thenReturn(Optional.of(USER_PROFILE)); when(authenticationService.login(EMAIL, PASSWORD)).thenReturn(true); when(authenticationService.getPhoneNumber(EMAIL)).thenReturn(Optional.of(PHONE_NUMBER)); + event.setBody( + format( + "{ \"password\": \"%s\", \"email\": \"%s\", \"post_auth_action\": \"%s\" }", + PASSWORD, EMAIL, postAuthAction)); + APIGatewayProxyResponseEvent result = handler.handleRequest(event, context); assertThat(result, hasStatus(204)); + String expectedAuditValue = + "not-a-valid-target-action".equals(postAuthAction) + ? AuditService.UNKNOWN + : postAuthAction; verify(auditService) .submitAuditEvent( AUTH_ACCOUNT_MANAGEMENT_AUTHENTICATE, auditContext.withSubjectId(clientSubjectId), - AUDIT_EVENT_COMPONENT_ID_AUTH); + AUDIT_EVENT_COMPONENT_ID_AUTH, + pair("post_auth_action", expectedAuditValue)); } @Test @@ -147,7 +168,8 @@ void shouldNotSendEncodedAuditDataIfHeaderNotPresent() { .withClientSessionId("unknown") .withSubjectId(clientSubjectId) .withTxmaAuditEncoded(Optional.empty()), - AUDIT_EVENT_COMPONENT_ID_AUTH); + AUDIT_EVENT_COMPONENT_ID_AUTH, + pair("post_auth_action", AuditService.UNKNOWN)); } @Test @@ -289,7 +311,8 @@ void shouldReturn204IfAisCallEnabledAndUserIsSuspendedWithInterventions( .submitAuditEvent( AUTH_ACCOUNT_MANAGEMENT_AUTHENTICATE, auditContext.withSubjectId(clientSubjectId), - AUDIT_EVENT_COMPONENT_ID_AUTH); + AUDIT_EVENT_COMPONENT_ID_AUTH, + pair("post_auth_action", AuditService.UNKNOWN)); } @Test From cecff6b08cbac09eddad5d274dc7f35b0a979dff Mon Sep 17 00:00:00 2001 From: Aidan Comer Date: Tue, 7 Oct 2025 14:41:19 +0100 Subject: [PATCH 4/8] BAU: replace high entropy credential value with clear placeholder value The high entropy value was being flagged by checkov in GHA --- .../account-management/openapi_v2.yaml | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/ci/terraform/account-management/openapi_v2.yaml b/ci/terraform/account-management/openapi_v2.yaml index 02473f79e91..f79b46200be 100644 --- a/ci/terraform/account-management/openapi_v2.yaml +++ b/ci/terraform/account-management/openapi_v2.yaml @@ -26,13 +26,13 @@ paths: summary: Authenticate user with optional target action value: email: "user@example.gov.uk" - password: "password123" + password: "" postAuthAction: "UPDATE_EMAIL" authenticate-request-no-target-action: summary: Authenticate user request with no target action for backwards compatability value: email: "user@example.gov.uk" - password: "password123" + password: "" required: true responses: "204": @@ -226,7 +226,7 @@ paths: priorityIdentifier: DEFAULT method: mfaMethodType: AUTH_APP - credential: "AAAABBBBCCCCCDDDDD55551111EEEE2222FFFF3333GGGG4444" + credential: "" methodVerified: true get-when-user-with-multiple-mfa-types-default-sms: summary: a user with a default mfa method of SMS, and a backup auth app method @@ -241,7 +241,7 @@ paths: priorityIdentifier: BACKUP method: mfaMethodType: AUTH_APP - credential: "AAAABBBBCCCCCDDDDD55551111EEEE2222FFFF3333GGGG4444" + credential: "" methodVerified: true "400": description: Bad Request @@ -349,7 +349,7 @@ paths: "method": { "mfaMethodType": "AUTH_APP", - "credential": "AAAABBBBCCCCCDDDDD55551111EEEE2222FFFF3333GGGG4444", + "credential": "", }, }, } @@ -388,7 +388,7 @@ paths: priorityIdentifier: BACKUP method: mfaMethodType: AUTH_APP - credential: "postAAAABBBBCCCCCDDDDD55551111EEEE2222FFFF3333GGGG4444" + credential: "post" methodVerified: true "400": description: Bad Request @@ -553,7 +553,7 @@ paths: priorityIdentifier: BACKUP method: mfaMethodType: AUTH_APP - credential: "AAAABBBBCCCCCDDDDD55551111EEEE2222FFFF3333GGGG4444" + credential: "" methodVerified: true put-when-user-with-multiple-mfa-types-default-sms: summary: a user with a default mfa method of SMS, and a backup auth app method after update @@ -568,7 +568,7 @@ paths: priorityIdentifier: BACKUP method: mfaMethodType: AUTH_APP - credential: "AAAABBBBCCCCCDDDDD55551111EEEE2222FFFF3333GGGG4444" + credential: "" methodVerified: true "400": description: Bad Request @@ -830,7 +830,7 @@ components: priorityIdentifier: DEFAULT method: mfaMethodType: AUTH_APP - credential: "AAAABBBBCCCCCDDDDD55551111EEEE2222FFFF3333GGGG4444" + credential: "" MfaMethodUpdate: required: @@ -861,7 +861,7 @@ components: priorityIdentifier: DEFAULT method: mfaMethodType: AUTH_APP - credential: "AAAABBBBCCCCCDDDDD55551111EEEE2222FFFF3333GGGG4444" + credential: "" MfaMethodUpdateRequest: required: @@ -875,7 +875,7 @@ components: priorityIdentifier: DEFAULT method: mfaMethodType: AUTH_APP - credential: "AAAABBBBCCCCCDDDDD55551111EEEE2222FFFF3333GGGG4444" + credential: "" SimpleError: type: object @@ -1222,7 +1222,7 @@ components: password: type: string description: The user's password - example: "password123" + example: "" postAuthAction: type: string enum: @@ -1233,7 +1233,7 @@ components: example: "UPDATE_EMAIL" example: email: "user@example.gov.uk" - password: "password123" + password: "" postAuthAction: "UPDATE_EMAIL" securitySchemes: From 2484077c8ecd7b530bdbeb6557d5c574ec3f432c Mon Sep 17 00:00:00 2001 From: Aidan Comer Date: Tue, 7 Oct 2025 15:57:31 +0100 Subject: [PATCH 5/8] AUT-4506: Add action_source extension + openapi spec --- .../entity/ActionSource.java | 10 ++++ .../entity/AuthenticateRequest.java | 6 +++ .../lambda/AuthenticateHandler.java | 7 +++ .../lambda/AuthenticateHandlerTest.java | 46 +++++++++++++------ .../account-management/openapi_v2.yaml | 11 ++++- 5 files changed, 65 insertions(+), 15 deletions(-) create mode 100644 account-management-api/src/main/java/uk/gov/di/accountmanagement/entity/ActionSource.java diff --git a/account-management-api/src/main/java/uk/gov/di/accountmanagement/entity/ActionSource.java b/account-management-api/src/main/java/uk/gov/di/accountmanagement/entity/ActionSource.java new file mode 100644 index 00000000000..3b5bf0680d7 --- /dev/null +++ b/account-management-api/src/main/java/uk/gov/di/accountmanagement/entity/ActionSource.java @@ -0,0 +1,10 @@ +package uk.gov.di.accountmanagement.entity; + +public enum ActionSource { + ACCOUNT_MANAGEMENT, + ACCOUNT_COMPONENTS; + + public String getValue() { + return name(); + } +} diff --git a/account-management-api/src/main/java/uk/gov/di/accountmanagement/entity/AuthenticateRequest.java b/account-management-api/src/main/java/uk/gov/di/accountmanagement/entity/AuthenticateRequest.java index 0a2e0e2d558..71d38b8e467 100644 --- a/account-management-api/src/main/java/uk/gov/di/accountmanagement/entity/AuthenticateRequest.java +++ b/account-management-api/src/main/java/uk/gov/di/accountmanagement/entity/AuthenticateRequest.java @@ -11,6 +11,8 @@ public class AuthenticateRequest { @Expose private PostAuthAction postAuthAction; + @Expose private ActionSource actionSource; + public AuthenticateRequest() {} public AuthenticateRequest(String email, String password) { @@ -29,4 +31,8 @@ public String getPassword() { public PostAuthAction getPostAuthAction() { return postAuthAction; } + + public ActionSource getActionSource() { + return actionSource; + } } diff --git a/account-management-api/src/main/java/uk/gov/di/accountmanagement/lambda/AuthenticateHandler.java b/account-management-api/src/main/java/uk/gov/di/accountmanagement/lambda/AuthenticateHandler.java index 228f674209d..ed7285fca3c 100644 --- a/account-management-api/src/main/java/uk/gov/di/accountmanagement/lambda/AuthenticateHandler.java +++ b/account-management-api/src/main/java/uk/gov/di/accountmanagement/lambda/AuthenticateHandler.java @@ -7,6 +7,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.ThreadContext; +import uk.gov.di.accountmanagement.entity.ActionSource; import uk.gov.di.accountmanagement.entity.AuthenticateRequest; import uk.gov.di.accountmanagement.entity.PostAuthAction; import uk.gov.di.accountmanagement.helpers.AuditHelper; @@ -38,6 +39,7 @@ public class AuthenticateHandler private static final Logger LOG = LogManager.getLogger(AuthenticateHandler.class); public static final String POST_AUTH_ACTION = "post_auth_action"; + public static final String ACTION_SOURCE = "action_source"; private final AuthenticationService authenticationService; private final Json objectMapper = SerializationService.getInstance(); @@ -174,6 +176,11 @@ public APIGatewayProxyResponseEvent authenticateRequestHandler( POST_AUTH_ACTION, Optional.ofNullable(loginRequest.getPostAuthAction()) .map(PostAuthAction::getValue) + .orElse(AuditService.UNKNOWN)), + pair( + ACTION_SOURCE, + Optional.ofNullable(loginRequest.getActionSource()) + .map(ActionSource::getValue) .orElse(AuditService.UNKNOWN))); return generateEmptySuccessApiGatewayResponse(); diff --git a/account-management-api/src/test/java/uk/gov/di/accountmanagement/lambda/AuthenticateHandlerTest.java b/account-management-api/src/test/java/uk/gov/di/accountmanagement/lambda/AuthenticateHandlerTest.java index be37e31be82..e87c8b4a507 100644 --- a/account-management-api/src/test/java/uk/gov/di/accountmanagement/lambda/AuthenticateHandlerTest.java +++ b/account-management-api/src/test/java/uk/gov/di/accountmanagement/lambda/AuthenticateHandlerTest.java @@ -9,6 +9,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import uk.gov.di.accountmanagement.entity.ActionSource; import uk.gov.di.accountmanagement.entity.PostAuthAction; import uk.gov.di.accountmanagement.helpers.AuditHelper; import uk.gov.di.audit.AuditContext; @@ -113,40 +114,55 @@ void setUp() throws UnsuccessfulAccountInterventionsResponseException { new Intervention(1L), new State(false, false, false, false))); } - private static Stream postAuthAction() { - return Stream.of( - Arguments.of("not-a-valid-target-action"), - Arguments.of(PostAuthAction.DELETE_ACCOUNT.getValue()), - Arguments.of(PostAuthAction.UPDATE_EMAIL.getValue()), - Arguments.of(PostAuthAction.UPDATE_PASSWORD.getValue())); + private static Stream postAuthActionAndSourceValues() { + var postAuthActions = + java.util.List.of( + "not-a-valid-target-action", + PostAuthAction.DELETE_ACCOUNT.getValue(), + PostAuthAction.UPDATE_EMAIL.getValue(), + PostAuthAction.UPDATE_PASSWORD.getValue()); + var actionSources = + java.util.List.of( + "not-a-valid-action-source", + ActionSource.ACCOUNT_MANAGEMENT.getValue(), + ActionSource.ACCOUNT_COMPONENTS.getValue()); + return postAuthActions.stream() + .flatMap( + target -> + actionSources.stream().map(source -> Arguments.of(target, source))); } @ParameterizedTest - @MethodSource("postAuthAction") - void shouldReturn204IfLoginIsSuccessful(String postAuthAction) { + @MethodSource("postAuthActionAndSourceValues") + void shouldReturn204IfLoginIsSuccessful(String postAuthAction, String actionSource) { when(authenticationService.getUserProfileByEmailMaybe(EMAIL)) .thenReturn(Optional.of(USER_PROFILE)); when(authenticationService.login(EMAIL, PASSWORD)).thenReturn(true); when(authenticationService.getPhoneNumber(EMAIL)).thenReturn(Optional.of(PHONE_NUMBER)); event.setBody( format( - "{ \"password\": \"%s\", \"email\": \"%s\", \"post_auth_action\": \"%s\" }", - PASSWORD, EMAIL, postAuthAction)); + "{ \"password\": \"%s\", \"email\": \"%s\", \"post_auth_action\": \"%s\", \"action_source\": \"%s\" }", + PASSWORD, EMAIL, postAuthAction, actionSource)); APIGatewayProxyResponseEvent result = handler.handleRequest(event, context); assertThat(result, hasStatus(204)); - String expectedAuditValue = + String expectedPostAuthAction = "not-a-valid-target-action".equals(postAuthAction) ? AuditService.UNKNOWN : postAuthAction; + String expectedActionSource = + "not-a-valid-action-source".equals(actionSource) + ? AuditService.UNKNOWN + : actionSource; verify(auditService) .submitAuditEvent( AUTH_ACCOUNT_MANAGEMENT_AUTHENTICATE, auditContext.withSubjectId(clientSubjectId), AUDIT_EVENT_COMPONENT_ID_AUTH, - pair("post_auth_action", expectedAuditValue)); + pair("post_auth_action", expectedPostAuthAction), + pair("action_source", expectedActionSource)); } @Test @@ -169,7 +185,8 @@ void shouldNotSendEncodedAuditDataIfHeaderNotPresent() { .withSubjectId(clientSubjectId) .withTxmaAuditEncoded(Optional.empty()), AUDIT_EVENT_COMPONENT_ID_AUTH, - pair("post_auth_action", AuditService.UNKNOWN)); + pair("post_auth_action", AuditService.UNKNOWN), + pair("action_source", AuditService.UNKNOWN)); } @Test @@ -312,7 +329,8 @@ void shouldReturn204IfAisCallEnabledAndUserIsSuspendedWithInterventions( AUTH_ACCOUNT_MANAGEMENT_AUTHENTICATE, auditContext.withSubjectId(clientSubjectId), AUDIT_EVENT_COMPONENT_ID_AUTH, - pair("post_auth_action", AuditService.UNKNOWN)); + pair("post_auth_action", AuditService.UNKNOWN), + pair("action_source", AuditService.UNKNOWN)); } @Test diff --git a/ci/terraform/account-management/openapi_v2.yaml b/ci/terraform/account-management/openapi_v2.yaml index f79b46200be..ba28e8eb680 100644 --- a/ci/terraform/account-management/openapi_v2.yaml +++ b/ci/terraform/account-management/openapi_v2.yaml @@ -23,11 +23,12 @@ paths: $ref: "#/components/schemas/AuthenticateRequest" examples: authenticate-request: - summary: Authenticate user with optional target action + summary: Authenticate user with optional target action and action source value: email: "user@example.gov.uk" password: "" postAuthAction: "UPDATE_EMAIL" + actionSource: "ACCOUNT_MANAGEMENT" authenticate-request-no-target-action: summary: Authenticate user request with no target action for backwards compatability value: @@ -1231,10 +1232,18 @@ components: - DELETE_ACCOUNT description: Optional field indicating the user's intended action example: "UPDATE_EMAIL" + actionSource: + type: string + enum: + - ACCOUNT_MANAGEMENT + - ACCOUNT_COMPONENTS + description: Optional field indicating the source of the action + example: "ACCOUNT_MANAGEMENT" example: email: "user@example.gov.uk" password: "" postAuthAction: "UPDATE_EMAIL" + actionSource: "ACCOUNT_MANAGEMENT" securitySchemes: authorise-access-token: From a13d5f90b1f8b1397fb3cd6bdafb9aae9260734e Mon Sep 17 00:00:00 2001 From: Aidan Comer Date: Wed, 15 Oct 2025 08:45:27 +0100 Subject: [PATCH 6/8] AUT-4506: Update groovy script to handle request without public subject ID --- .../mock/respond-with-examples-from-spec.groovy | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/account-management-api/api-contract/mock/respond-with-examples-from-spec.groovy b/account-management-api/api-contract/mock/respond-with-examples-from-spec.groovy index 3b3697ead7e..c23a1fcddeb 100644 --- a/account-management-api/api-contract/mock/respond-with-examples-from-spec.groovy +++ b/account-management-api/api-contract/mock/respond-with-examples-from-spec.groovy @@ -10,7 +10,12 @@ def request = context.request def method = request.method.toLowerCase() if (method == "get" || method == "post" || method == "put" || method == "delete") { - def publicSubjectId = context.request.pathParams.publicSubjectId + def publicSubjectId = context.request.pathParams?.publicSubjectId + + // Skip processing if no publicSubjectId (e.g., for /authenticate endpoint) + if (publicSubjectId == null) { + return + } def responseStatusCode def responseBody From 22f8fc1c2f7055b7f02ca91d81e02e33c3939c3b Mon Sep 17 00:00:00 2001 From: Andrew Moores Date: Fri, 17 Oct 2025 17:18:24 +0100 Subject: [PATCH 7/8] AUT-4506: Add response examples for authenticate Add missing 400, 401, and 403 response examples to /authenticate endpoint to support comprehensive mock server testing scenarios. --- account-management-api/api-contract/README.md | 33 +++++++++ .../respond-with-examples-from-spec.groovy | 70 ++++++++++++++++++- .../account-management/openapi_v2.yaml | 18 +++++ 3 files changed, 120 insertions(+), 1 deletion(-) diff --git a/account-management-api/api-contract/README.md b/account-management-api/api-contract/README.md index 2ffc489219e..096b5a54367 100644 --- a/account-management-api/api-contract/README.md +++ b/account-management-api/api-contract/README.md @@ -137,6 +137,39 @@ http DELETE :8080/v1/mfa-methods/delete-when-mfa-method-not-found/id http DELETE :8080/v1/mfa-methods/delete-when-cannot-delete-default-priority-mfa-method/id +######################################################################################################### +# AUTHENTICATE +######################################################################################################### + +# POST 204 success response (any valid email) +http POST :8080/authenticate \ + Content-Type:application/json \ + email="user@example.gov.uk" \ + password="test" + +# POST 400 error response (missing email) +http POST :8080/authenticate \ + Content-Type:application/json \ + password="test" + +# POST 401 error response (invalid credentials) +http POST :8080/authenticate \ + Content-Type:application/json \ + email="invalid@example.gov.uk" \ + password="test" + +# POST 403 error response (blocked account) +http POST :8080/authenticate \ + Content-Type:application/json \ + email="blocked@example.gov.uk" \ + password="test" + +# POST 403 error response (suspended account) +http POST :8080/authenticate \ + Content-Type:application/json \ + email="suspended@example.gov.uk" \ + password="test" + ######################################################################################################### # UPDATE EMAIL ######################################################################################################### diff --git a/account-management-api/api-contract/mock/respond-with-examples-from-spec.groovy b/account-management-api/api-contract/mock/respond-with-examples-from-spec.groovy index c23a1fcddeb..a0465e79fba 100644 --- a/account-management-api/api-contract/mock/respond-with-examples-from-spec.groovy +++ b/account-management-api/api-contract/mock/respond-with-examples-from-spec.groovy @@ -6,13 +6,22 @@ // // context.operation.responses["200"].content["application/json"].examples["user-with-single-mfa-type-app"]) +import groovy.json.JsonSlurper + def request = context.request def method = request.method.toLowerCase() +def path = request.path if (method == "get" || method == "post" || method == "put" || method == "delete") { + // Handle /authenticate endpoint + if (path == "/authenticate" && method == "post") { + handleAuthenticate(request) + return + } + def publicSubjectId = context.request.pathParams?.publicSubjectId - // Skip processing if no publicSubjectId (e.g., for /authenticate endpoint) + // Skip processing if no publicSubjectId if (publicSubjectId == null) { return } @@ -35,3 +44,62 @@ if (method == "get" || method == "post" || method == "put" || method == "delete" } } } + +def handleAuthenticate(request) { + def requestBody = [:] + try { + if (request.body) { + requestBody = new JsonSlurper().parseText(request.body) + } + } catch (Exception e) { + // Invalid JSON, treat as empty + } + + def email = requestBody?.email + def missingEmail = !email + + if (missingEmail) { + def response400 = context.operation.responses["400"] + def example = response400.content["application/json"].examples["post-when-request-is-missing-parameters"] + respond() + .withContent(example.value.toString()) + .withHeader("Content-Type", "application/json") + .withStatusCode(400) + return + } + + // 401 - Invalid credentials + if (email == "invalid@example.gov.uk") { + def response401 = context.operation.responses["401"] + def example = response401.content["application/json"].examples["post-when-invalid-credentials"] + respond() + .withContent(example.value.toString()) + .withHeader("Content-Type", "application/json") + .withStatusCode(401) + return + } + + // 403 - Blocked account + if (email == "blocked@example.gov.uk") { + def response403 = context.operation.responses["403"] + def example = response403.content["application/json"].examples["post-when-user-has-blocked-intervention"] + respond() + .withContent(example.value.toString()) + .withHeader("Content-Type", "application/json") + .withStatusCode(403) + return + } + + // 403 - Suspended account + if (email == "suspended@example.gov.uk") { + def response403 = context.operation.responses["403"] + def example = response403.content["application/json"].examples["post-when-user-has-suspended-intervention"] + respond() + .withContent(example.value.toString()) + .withHeader("Content-Type", "application/json") + .withStatusCode(403) + return + } + + // Default to 204 success for any other valid email +} diff --git a/ci/terraform/account-management/openapi_v2.yaml b/ci/terraform/account-management/openapi_v2.yaml index ba28e8eb680..a8c472f779a 100644 --- a/ci/terraform/account-management/openapi_v2.yaml +++ b/ci/terraform/account-management/openapi_v2.yaml @@ -40,8 +40,26 @@ paths: description: Successful Operation. The password is correct for this user. "400": description: One of the required fields is missing. + content: + application/json: + schema: + $ref: "#/components/schemas/RequestIsMissingParameters" + examples: + post-when-request-is-missing-parameters: + value: + code: 1001 + message: "Request is missing parameters" "401": description: This user does not have an account or the password is wrong. + content: + application/json: + schema: + $ref: "#/components/schemas/SimpleError" + examples: + post-when-invalid-credentials: + value: + code: 1002 + message: "Invalid email or password" "403": description: This user cannot authenticate because of a suspension. content: From fea03c480d81a687b6b702afe55040e32448ca26 Mon Sep 17 00:00:00 2001 From: Andrew Moores Date: Mon, 20 Oct 2025 08:59:42 +0100 Subject: [PATCH 8/8] AUT-4506: Add response examples for authenticate Add missing 400, 401, and 403 response examples to /authenticate endpoint to support mock server testing scenarios. --- account-management-api/api-contract/README.md | 6 ++++++ .../mock/respond-with-examples-from-spec.groovy | 11 +++++++++++ ci/terraform/account-management/openapi_v2.yaml | 13 +++++++++++-- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/account-management-api/api-contract/README.md b/account-management-api/api-contract/README.md index 096b5a54367..c0fe5034da0 100644 --- a/account-management-api/api-contract/README.md +++ b/account-management-api/api-contract/README.md @@ -152,6 +152,12 @@ http POST :8080/authenticate \ Content-Type:application/json \ password="test" +# POST 400 error response (account does not exist) +http POST :8080/authenticate \ + Content-Type:application/json \ + email="nonexistent@example.gov.uk" \ + password="test" + # POST 401 error response (invalid credentials) http POST :8080/authenticate \ Content-Type:application/json \ diff --git a/account-management-api/api-contract/mock/respond-with-examples-from-spec.groovy b/account-management-api/api-contract/mock/respond-with-examples-from-spec.groovy index a0465e79fba..d98f1e6c425 100644 --- a/account-management-api/api-contract/mock/respond-with-examples-from-spec.groovy +++ b/account-management-api/api-contract/mock/respond-with-examples-from-spec.groovy @@ -68,6 +68,17 @@ def handleAuthenticate(request) { return } + // 400 - Account does not exist + if (email == "nonexistent@example.gov.uk") { + def response400 = context.operation.responses["400"] + def example = response400.content["application/json"].examples["post-when-account-does-not-exist"] + respond() + .withContent(example.value.toString()) + .withHeader("Content-Type", "application/json") + .withStatusCode(400) + return + } + // 401 - Invalid credentials if (email == "invalid@example.gov.uk") { def response401 = context.operation.responses["401"] diff --git a/ci/terraform/account-management/openapi_v2.yaml b/ci/terraform/account-management/openapi_v2.yaml index a8c472f779a..13a0da80697 100644 --- a/ci/terraform/account-management/openapi_v2.yaml +++ b/ci/terraform/account-management/openapi_v2.yaml @@ -16,6 +16,9 @@ paths: uri: "${endpoint_modules.authenticate.integration_uri}" passthroughBehavior: "when_no_match" timeoutInMillis: 29000 + description: "Validates user credentials (email and password) for authentication. Optionally accepts post-authentication action context to indicate the user's intended next action after successful authentication." + summary: Authenticate user credentials + operationId: "authenticate" requestBody: content: application/json: @@ -39,16 +42,22 @@ paths: "204": description: Successful Operation. The password is correct for this user. "400": - description: One of the required fields is missing. + description: Bad Request - Missing parameters or account does not exist. content: application/json: schema: - $ref: "#/components/schemas/RequestIsMissingParameters" + oneOf: + - $ref: "#/components/schemas/RequestIsMissingParameters" + - $ref: "#/components/schemas/AccountDoesNotExist" examples: post-when-request-is-missing-parameters: value: code: 1001 message: "Request is missing parameters" + post-when-account-does-not-exist: + value: + code: 1010 + message: "An account with this email address does not exist" "401": description: This user does not have an account or the password is wrong. content: