Skip to content
39 changes: 39 additions & 0 deletions account-management-api/api-contract/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,45 @@ 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 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 \
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
#########################################################################################################
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,25 @@
//
// 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") {
def publicSubjectId = context.request.pathParams.publicSubjectId
// Handle /authenticate endpoint
if (path == "/authenticate" && method == "post") {
handleAuthenticate(request)
return
}

def publicSubjectId = context.request.pathParams?.publicSubjectId

// Skip processing if no publicSubjectId
if (publicSubjectId == null) {
return
}
def responseStatusCode
def responseBody

Expand All @@ -30,3 +44,73 @@ 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
}

// 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"]
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
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package uk.gov.di.accountmanagement.entity;

public enum ActionSource {
ACCOUNT_MANAGEMENT,
ACCOUNT_COMPONENTS;

public String getValue() {
return name();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ public class AuthenticateRequest {

@Expose @Required private String password;

@Expose private PostAuthAction postAuthAction;

@Expose private ActionSource actionSource;

public AuthenticateRequest() {}

public AuthenticateRequest(String email, String password) {
Expand All @@ -23,4 +27,12 @@ public String getEmail() {
public String getPassword() {
return password;
}

public PostAuthAction getPostAuthAction() {
return postAuthAction;
}

public ActionSource getActionSource() {
return actionSource;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package uk.gov.di.accountmanagement.entity;

public enum PostAuthAction {
UPDATE_EMAIL,
UPDATE_PASSWORD,
DELETE_ACCOUNT;

public String getValue() {
return name();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
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;
import uk.gov.di.audit.AuditContext;
import uk.gov.di.authentication.shared.entity.AccountInterventionsInboundResponse;
Expand All @@ -30,11 +32,14 @@
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<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {

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();
Expand Down Expand Up @@ -166,7 +171,17 @@ 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)),
pair(
ACTION_SOURCE,
Optional.ofNullable(loginRequest.getActionSource())
.map(ActionSource::getValue)
.orElse(AuditService.UNKNOWN)));

return generateEmptySuccessApiGatewayResponse();
} catch (JsonException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
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;
import uk.gov.di.authentication.shared.entity.*;
Expand All @@ -34,6 +36,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;
Expand Down Expand Up @@ -111,21 +114,55 @@ void setUp() throws UnsuccessfulAccountInterventionsResponseException {
new Intervention(1L), new State(false, false, false, false)));
}

@Test
void shouldReturn204IfLoginIsSuccessful() {
private static Stream<Arguments> 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("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\", \"action_source\": \"%s\" }",
PASSWORD, EMAIL, postAuthAction, actionSource));

APIGatewayProxyResponseEvent result = handler.handleRequest(event, context);

assertThat(result, hasStatus(204));

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);
AUDIT_EVENT_COMPONENT_ID_AUTH,
pair("post_auth_action", expectedPostAuthAction),
pair("action_source", expectedActionSource));
}

@Test
Expand All @@ -147,7 +184,9 @@ 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),
pair("action_source", AuditService.UNKNOWN));
}

@Test
Expand Down Expand Up @@ -289,7 +328,9 @@ 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),
pair("action_source", AuditService.UNKNOWN));
}

@Test
Expand Down
Loading
Loading