Repository: govuk-one-login/authentication-api
Pull request #6198: Aut 3486/implement update mfa method for default method
Author: login: BeckaL
Created: 2025-03-24T17:44:23Z | Merged: 2025-03-27T13:32:51Z

## What

Implements updating an mfa method when the method to update is a default method.

In this case, the only legitimate update is to change phone number or credential value - all other updates are not allowed and will return an appropriate error.

## How to review

1. Code Review -probably easiest to look at the overall diff first, and then commit by commit



Diff:
diff --git a/account-management-api/src/main/java/uk/gov/di/accountmanagement/lambda/MFAMethodsCreateHandler.java b/account-management-api/src/main/java/uk/gov/di/accountmanagement/lambda/MFAMethodsCreateHandler.java
index 4b429410de0..32d5cb47180 100644
--- a/account-management-api/src/main/java/uk/gov/di/accountmanagement/lambda/MFAMethodsCreateHandler.java
+++ b/account-management-api/src/main/java/uk/gov/di/accountmanagement/lambda/MFAMethodsCreateHandler.java
@@ -9,7 +9,7 @@
 import org.apache.logging.log4j.Logger;
 import org.apache.logging.log4j.ThreadContext;
 import uk.gov.di.authentication.shared.entity.ErrorResponse;
-import uk.gov.di.authentication.shared.entity.mfa.MfaMethodCreateRequest;
+import uk.gov.di.authentication.shared.entity.mfa.MfaMethodCreateOrUpdateRequest;
 import uk.gov.di.authentication.shared.entity.mfa.MfaMethodData;
 import uk.gov.di.authentication.shared.serialization.Json;
 import uk.gov.di.authentication.shared.services.ConfigurationService;
@@ -82,7 +82,8 @@ public APIGatewayProxyResponseEvent mfaMethodsHandler(
         String email = maybeUserProfile.get().getEmail();
 
         try {
-            MfaMethodCreateRequest mfaMethodCreateRequest = readMfaMethodCreateRequest(input);
+            MfaMethodCreateOrUpdateRequest mfaMethodCreateRequest =
+                    readMfaMethodCreateRequest(input);
 
             LOG.info("Update MFA POST called with: {}", mfaMethodCreateRequest.mfaMethod());
 
@@ -113,17 +114,19 @@ public APIGatewayProxyResponseEvent mfaMethodsHandler(
         }
     }
 
-    private MfaMethodCreateRequest readMfaMethodCreateRequest(APIGatewayProxyRequestEvent input)
-            throws Json.JsonException {
+    private MfaMethodCreateOrUpdateRequest readMfaMethodCreateRequest(
+            APIGatewayProxyRequestEvent input) throws Json.JsonException {
 
-        MfaMethodCreateRequest mfaMethodCreateRequest;
+        MfaMethodCreateOrUpdateRequest mfaMethodCreateRequest;
         try {
             mfaMethodCreateRequest =
                     segmentedFunctionCall(
                             "SerializationService::GSON::fromJson",
                             () ->
                                     objectMapper.readValue(
-                                            input.getBody(), MfaMethodCreateRequest.class, true));
+                                            input.getBody(),
+                                            MfaMethodCreateOrUpdateRequest.class,
+                                            true));
 
         } catch (RuntimeException e) {
             LOG.error("Error during JSON deserialization", e);
diff --git a/account-management-api/src/main/java/uk/gov/di/accountmanagement/lambda/MFAMethodsDeleteHandler.java b/account-management-api/src/main/java/uk/gov/di/accountmanagement/lambda/MFAMethodsDeleteHandler.java
index b980b07ca45..de1b318e864 100644
--- a/account-management-api/src/main/java/uk/gov/di/accountmanagement/lambda/MFAMethodsDeleteHandler.java
+++ b/account-management-api/src/main/java/uk/gov/di/accountmanagement/lambda/MFAMethodsDeleteHandler.java
@@ -68,12 +68,12 @@ public APIGatewayProxyResponseEvent deleteMFAMethodHandler(
 
         if (publicSubjectId.isEmpty()) {
             LOG.error("Request does not include public subject id");
-            return generateApiGatewayProxyErrorResponse(400, ErrorResponse.ERROR_1056);
+            return generateApiGatewayProxyErrorResponse(400, ErrorResponse.ERROR_1001);
         }
 
         if (mfaIdentifier.isEmpty()) {
             LOG.error("Request does not include mfa identifier");
-            return generateApiGatewayProxyErrorResponse(400, ErrorResponse.ERROR_1064);
+            return generateApiGatewayProxyErrorResponse(400, ErrorResponse.ERROR_1001);
         }
 
         var deleteResult = mfaMethodsService.deleteMfaMethod(publicSubjectId, mfaIdentifier);
diff --git a/account-management-api/src/main/java/uk/gov/di/accountmanagement/lambda/MFAMethodsPutHandler.java b/account-management-api/src/main/java/uk/gov/di/accountmanagement/lambda/MFAMethodsPutHandler.java
index 485d21872da..25885581c90 100644
--- a/account-management-api/src/main/java/uk/gov/di/accountmanagement/lambda/MFAMethodsPutHandler.java
+++ b/account-management-api/src/main/java/uk/gov/di/accountmanagement/lambda/MFAMethodsPutHandler.java
@@ -8,14 +8,22 @@
 import org.apache.logging.log4j.Logger;
 import org.apache.logging.log4j.ThreadContext;
 import uk.gov.di.authentication.shared.entity.ErrorResponse;
+import uk.gov.di.authentication.shared.entity.mfa.MfaMethodCreateOrUpdateRequest;
 import uk.gov.di.authentication.shared.helpers.RequestHeaderHelper;
+import uk.gov.di.authentication.shared.serialization.Json;
+import uk.gov.di.authentication.shared.services.AuthenticationService;
 import uk.gov.di.authentication.shared.services.ConfigurationService;
+import uk.gov.di.authentication.shared.services.DynamoService;
+import uk.gov.di.authentication.shared.services.SerializationService;
+import uk.gov.di.authentication.shared.services.mfa.MfaMethodsService;
+import uk.gov.di.authentication.shared.services.mfa.MfaUpdateFailureReason;
 
 import java.util.Map;
 
 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.generateApiGatewayProxyResponse;
+import static uk.gov.di.authentication.shared.helpers.ApiGatewayResponseHelper.generateEmptySuccessApiGatewayResponse;
 import static uk.gov.di.authentication.shared.helpers.InstrumentationHelper.segmentedFunctionCall;
 import static uk.gov.di.authentication.shared.helpers.LogLineHelper.attachSessionIdToLogs;
 
@@ -24,6 +32,10 @@ public class MFAMethodsPutHandler
 
     private static final Logger LOG = LogManager.getLogger(MFAMethodsPutHandler.class);
     private final ConfigurationService configurationService;
+    private final MfaMethodsService mfaMethodsService;
+    private final AuthenticationService authenticationService;
+
+    private final Json objectMapper = SerializationService.getInstance();
 
     public MFAMethodsPutHandler() {
         this(ConfigurationService.getInstance());
@@ -31,6 +43,17 @@ public MFAMethodsPutHandler() {
 
     public MFAMethodsPutHandler(ConfigurationService configurationService) {
         this.configurationService = configurationService;
+        this.mfaMethodsService = new MfaMethodsService(configurationService);
+        this.authenticationService = new DynamoService(configurationService);
+    }
+
+    public MFAMethodsPutHandler(
+            ConfigurationService configurationService,
+            MfaMethodsService mfaMethodsService,
+            AuthenticationService authenticationService) {
+        this.configurationService = configurationService;
+        this.mfaMethodsService = mfaMethodsService;
+        this.authenticationService = authenticationService;
     }
 
     @Override
@@ -54,7 +77,62 @@ public APIGatewayProxyResponseEvent updateMFAMethodsHandler(
             return generateApiGatewayProxyErrorResponse(400, ErrorResponse.ERROR_1063);
         }
 
-        return generateApiGatewayProxyResponse(200, "{}");
+        var publicSubjectId = input.getPathParameters().get("publicSubjectId");
+        var mfaIdentifier = input.getPathParameters().get("mfaIdentifier");
+
+        if (publicSubjectId.isEmpty()) {
+            LOG.error("Request does not include public subject id");
+            return generateApiGatewayProxyErrorResponse(400, ErrorResponse.ERROR_1001);
+        }
+
+        if (mfaIdentifier.isEmpty()) {
+            LOG.error("Request does not include mfa identifier");
+            return generateApiGatewayProxyErrorResponse(400, ErrorResponse.ERROR_1001);
+        }
+
+        var maybeUserProfile =
+                authenticationService.getOptionalUserProfileFromPublicSubject(publicSubjectId);
+        if (maybeUserProfile.isEmpty()) {
+            return generateApiGatewayProxyErrorResponse(404, ErrorResponse.ERROR_1056);
+        }
+        var userProfile = maybeUserProfile.get();
+
+        try {
+            var mfaMethodUpdateRequest =
+                    objectMapper.readValue(
+                            input.getBody(), MfaMethodCreateOrUpdateRequest.class, true);
+            var result =
+                    mfaMethodsService.updateMfaMethod(
+                            userProfile.getEmail(), mfaIdentifier, mfaMethodUpdateRequest);
+
+            if (result.isLeft()) {
+                var failureReason = result.getLeft();
+                return handleUpdateMfaFailureReason(failureReason);
+            }
+
+            var successfulUpdate = result.get();
+
+            return generateApiGatewayProxyResponse(200, successfulUpdate, true);
+        } catch (Json.JsonException e) {
+            return generateApiGatewayProxyErrorResponse(400, ErrorResponse.ERROR_1001);
+        }
+    }
+
+    private static APIGatewayProxyResponseEvent handleUpdateMfaFailureReason(
+            MfaUpdateFailureReason failureReason) {
+        return switch (failureReason) {
+            case CANNOT_CHANGE_TYPE_OF_MFA_METHOD -> generateApiGatewayProxyErrorResponse(
+                    400, ErrorResponse.ERROR_1072);
+            case UNEXPECTED_ERROR -> generateApiGatewayProxyErrorResponse(
+                    500, ErrorResponse.ERROR_1071);
+            case UNKOWN_MFA_IDENTIFIER -> generateApiGatewayProxyErrorResponse(
+                    404, ErrorResponse.ERROR_1065);
+            case CANNOT_CHANGE_PRIORITY_OF_DEFAULT_METHOD -> generateApiGatewayProxyErrorResponse(
+                    400, ErrorResponse.ERROR_1073);
+            case REQUEST_TO_UPDATE_MFA_METHOD_WITH_NO_CHANGE -> generateEmptySuccessApiGatewayResponse();
+            case ATTEMPT_TO_UPDATE_PHONE_NUMBER_WITH_BACKUP_NUMBER -> generateApiGatewayProxyErrorResponse(
+                    400, ErrorResponse.ERROR_1074);
+        };
     }
 
     private void addSessionIdToLogs(APIGatewayProxyRequestEvent input) {
diff --git a/account-management-api/src/test/java/uk/gov/di/accountmanagement/lambda/MFAMethodsCreateHandlerTest.java b/account-management-api/src/test/java/uk/gov/di/accountmanagement/lambda/MFAMethodsCreateHandlerTest.java
index 606e8d0bdc5..af04c2d489e 100644
--- a/account-management-api/src/test/java/uk/gov/di/accountmanagement/lambda/MFAMethodsCreateHandlerTest.java
+++ b/account-management-api/src/test/java/uk/gov/di/accountmanagement/lambda/MFAMethodsCreateHandlerTest.java
@@ -15,7 +15,7 @@
 import uk.gov.di.authentication.shared.entity.mfa.AuthAppMfaDetail;
 import uk.gov.di.authentication.shared.entity.mfa.MFAMethodType;
 import uk.gov.di.authentication.shared.entity.mfa.MfaDetail;
-import uk.gov.di.authentication.shared.entity.mfa.MfaMethodCreateRequest;
+import uk.gov.di.authentication.shared.entity.mfa.MfaMethodCreateOrUpdateRequest;
 import uk.gov.di.authentication.shared.entity.mfa.MfaMethodData;
 import uk.gov.di.authentication.shared.entity.mfa.SmsMfaDetail;
 import uk.gov.di.authentication.shared.helpers.ClientSessionIdHelper;
@@ -98,8 +98,8 @@ void shouldReturn200AndCreateMfaSmsMfaMethod() {
 
         var result = handler.handleRequest(event, context);
 
-        ArgumentCaptor<MfaMethodCreateRequest.MfaMethod> mfaMethodCaptor =
-                ArgumentCaptor.forClass(MfaMethodCreateRequest.MfaMethod.class);
+        ArgumentCaptor<MfaMethodCreateOrUpdateRequest.MfaMethod> mfaMethodCaptor =
+                ArgumentCaptor.forClass(MfaMethodCreateOrUpdateRequest.MfaMethod.class);
 
         verify(mfaMethodsService).addBackupMfa(eq(TEST_EMAIL), mfaMethodCaptor.capture());
         var capturedRequest = mfaMethodCaptor.getValue();
@@ -151,8 +151,8 @@ void shouldReturn200AndCreateAuthAppMfa() {
 
         var result = handler.handleRequest(event, context);
 
-        ArgumentCaptor<MfaMethodCreateRequest.MfaMethod> mfaMethodCaptor =
-                ArgumentCaptor.forClass(MfaMethodCreateRequest.MfaMethod.class);
+        ArgumentCaptor<MfaMethodCreateOrUpdateRequest.MfaMethod> mfaMethodCaptor =
+                ArgumentCaptor.forClass(MfaMethodCreateOrUpdateRequest.MfaMethod.class);
 
         verify(mfaMethodsService).addBackupMfa(eq(TEST_EMAIL), mfaMethodCaptor.capture());
         var capturedRequest = mfaMethodCaptor.getValue();
diff --git a/account-management-api/src/test/java/uk/gov/di/accountmanagement/lambda/MFAMethodsPutHandlerTest.java b/account-management-api/src/test/java/uk/gov/di/accountmanagement/lambda/MFAMethodsPutHandlerTest.java
index 7a4ca2b5b11..59c847d075f 100644
--- a/account-management-api/src/test/java/uk/gov/di/accountmanagement/lambda/MFAMethodsPutHandlerTest.java
+++ b/account-management-api/src/test/java/uk/gov/di/accountmanagement/lambda/MFAMethodsPutHandlerTest.java
@@ -2,22 +2,50 @@
 
 import com.amazonaws.services.lambda.runtime.Context;
 import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
+import com.google.gson.JsonParser;
+import io.vavr.control.Either;
 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.MethodSource;
+import uk.gov.di.authentication.shared.entity.ErrorResponse;
+import uk.gov.di.authentication.shared.entity.PriorityIdentifier;
+import uk.gov.di.authentication.shared.entity.UserProfile;
+import uk.gov.di.authentication.shared.entity.mfa.MFAMethodType;
+import uk.gov.di.authentication.shared.entity.mfa.MfaMethodCreateOrUpdateRequest;
+import uk.gov.di.authentication.shared.entity.mfa.MfaMethodData;
+import uk.gov.di.authentication.shared.entity.mfa.SmsMfaDetail;
+import uk.gov.di.authentication.shared.services.AuthenticationService;
 import uk.gov.di.authentication.shared.services.ConfigurationService;
+import uk.gov.di.authentication.shared.services.mfa.MfaMethodsService;
+import uk.gov.di.authentication.shared.services.mfa.MfaUpdateFailureReason;
 
 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.junit.jupiter.api.Assertions.assertEquals;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 import static uk.gov.di.accountmanagement.helpers.CommonTestVariables.VALID_HEADERS;
+import static uk.gov.di.authentication.sharedtest.matchers.APIGatewayProxyResponseEventMatcher.hasJsonBody;
+import static uk.gov.di.authentication.sharedtest.matchers.APIGatewayProxyResponseEventMatcher.hasStatus;
 
 class MFAMethodsPutHandlerTest {
 
     private static final ConfigurationService configurationService =
             mock(ConfigurationService.class);
+    private static final MfaMethodsService mfaMethodsService = mock(MfaMethodsService.class);
+    private static final AuthenticationService authenticationService =
+            mock(AuthenticationService.class);
+    private static final UserProfile userProfile = mock(UserProfile.class);
     private static final Context context = mock(Context.class);
+    private static final String MFA_IDENTIFIER = "some-mfa-identifier";
+    private static final String PUBLIC_SUBJECT_ID = "some-subject-id";
+    private static final String EMAIL = "joe.bloggs@digital.cabinet-office.gov.uk";
 
     private MFAMethodsPutHandler handler;
 
@@ -25,20 +53,158 @@ class MFAMethodsPutHandlerTest {
             new APIGatewayProxyRequestEvent()
                     .withPathParameters(
                             Map.ofEntries(
-                                    Map.entry("publicSubjectId", "some-subject-id"),
-                                    Map.entry("mfaIdentifier", "some-mfa-identifier")))
+                                    Map.entry("publicSubjectId", PUBLIC_SUBJECT_ID),
+                                    Map.entry("mfaIdentifier", MFA_IDENTIFIER)))
                     .withHeaders(VALID_HEADERS);
 
     @BeforeEach
     void setUp() {
         when(configurationService.isMfaMethodManagementApiEnabled()).thenReturn(true);
-        handler = new MFAMethodsPutHandler(configurationService);
+        handler =
+                new MFAMethodsPutHandler(
+                        configurationService, mfaMethodsService, authenticationService);
     }
 
     @Test
-    void shouldReturn204WhenFeatureFlagEnabled() {
-        var result = handler.handleRequest(event, context);
+    void shouldReturn200WithUpdatedMethodWhenFeatureFlagEnabled() {
+        var phoneNumber = "123456789";
+        var updateRequest =
+                MfaMethodCreateOrUpdateRequest.from(
+                        PriorityIdentifier.DEFAULT,
+                        new SmsMfaDetail(MFAMethodType.SMS, phoneNumber));
+        var eventWithUpdateRequest = event.withBody(updateSmsRequest(phoneNumber));
+
+        when(userProfile.getEmail()).thenReturn(EMAIL);
+        when(authenticationService.getOptionalUserProfileFromPublicSubject(PUBLIC_SUBJECT_ID))
+                .thenReturn(Optional.of(userProfile));
+
+        var updatedMfaMethod =
+                MfaMethodData.smsMethodData(
+                        MFA_IDENTIFIER, PriorityIdentifier.DEFAULT, true, phoneNumber);
+        when(mfaMethodsService.updateMfaMethod(EMAIL, MFA_IDENTIFIER, updateRequest))
+                .thenReturn(Either.right(updatedMfaMethod));
+
+        var result = handler.handleRequest(eventWithUpdateRequest, context);
+
         assertEquals(200, result.getStatusCode());
+        var expectedResponse =
+                format(
+                        """
+                {
+                  "mfaIdentifier": "%s",
+                  "priorityIdentifier": "DEFAULT",
+                  "methodVerified": true,
+                  "method": {
+                    "mfaMethodType": "SMS",
+                    "phoneNumber": "%s"
+                  }
+                }
+                """,
+                        MFA_IDENTIFIER, phoneNumber);
+        var expectedResponseParsedToString =
+                JsonParser.parseString(expectedResponse).getAsJsonObject().toString();
+        assertEquals(expectedResponseParsedToString, result.getBody());
+    }
+
+    private static Stream<Arguments> updateFailureReasonsToExpectedResponses() {
+        return Stream.of(
+                Arguments.of(
+                        MfaUpdateFailureReason.CANNOT_CHANGE_TYPE_OF_MFA_METHOD,
+                        400,
+                        Optional.of(ErrorResponse.ERROR_1072)),
+                Arguments.of(
+                        MfaUpdateFailureReason.REQUEST_TO_UPDATE_MFA_METHOD_WITH_NO_CHANGE,
+                        204,
+                        Optional.empty()),
+                Arguments.of(
+                        MfaUpdateFailureReason.UNEXPECTED_ERROR,
+                        500,
+                        Optional.of(ErrorResponse.ERROR_1071)),
+                Arguments.of(
+                        MfaUpdateFailureReason.ATTEMPT_TO_UPDATE_PHONE_NUMBER_WITH_BACKUP_NUMBER,
+                        400,
+                        Optional.of(ErrorResponse.ERROR_1074)),
+                Arguments.of(
+                        MfaUpdateFailureReason.CANNOT_CHANGE_PRIORITY_OF_DEFAULT_METHOD,
+                        400,
+                        Optional.of(ErrorResponse.ERROR_1073)),
+                Arguments.of(
+                        MfaUpdateFailureReason.UNKOWN_MFA_IDENTIFIER,
+                        404,
+                        Optional.of(ErrorResponse.ERROR_1065)));
+    }
+
+    @ParameterizedTest
+    @MethodSource("updateFailureReasonsToExpectedResponses")
+    void shouldReturnAppropriateResponseWhenMfaMethodsServiceReturnsError(
+            MfaUpdateFailureReason failureReason,
+            int expectedStatus,
+            Optional<ErrorResponse> maybeErrorResponse) {
+        when(userProfile.getEmail()).thenReturn(EMAIL);
+        when(authenticationService.getOptionalUserProfileFromPublicSubject(PUBLIC_SUBJECT_ID))
+                .thenReturn(Optional.of(userProfile));
+
+        var phoneNumber = "123456789";
+        var updateRequest =
+                MfaMethodCreateOrUpdateRequest.from(
+                        PriorityIdentifier.DEFAULT,
+                        new SmsMfaDetail(MFAMethodType.SMS, phoneNumber));
+
+        var eventWithUpdateRequest = event.withBody(updateSmsRequest(phoneNumber));
+        when(mfaMethodsService.updateMfaMethod(EMAIL, MFA_IDENTIFIER, updateRequest))
+                .thenReturn(Either.left(failureReason));
+        var result = handler.handleRequest(eventWithUpdateRequest, context);
+
+        assertThat(result, hasStatus(expectedStatus));
+        maybeErrorResponse.ifPresent(
+                expectedError -> assertThat(result, hasJsonBody(expectedError)));
+    }
+
+    @Test
+    void shouldReturn400WhenJsonIsInvalid() {
+        event.withBody("Invalid JSON");
+        when(authenticationService.getOptionalUserProfileFromPublicSubject(PUBLIC_SUBJECT_ID))
+                .thenReturn(Optional.of(userProfile));
+
+        var result = handler.handleRequest(event, context);
+
+        assertThat(result, hasStatus(400));
+        assertThat(result, hasJsonBody(ErrorResponse.ERROR_1001));
+    }
+
+    @Test
+    void shouldReturn400WhenPathParameterIsEmpty() {
+        event.withPathParameters(
+                Map.of("mfaIdentifier", "some-mfa-identifier", "publicSubjectId", ""));
+
+        var result = handler.handleRequest(event, context);
+
+        assertThat(result, hasStatus(400));
+        assertThat(result, hasJsonBody(ErrorResponse.ERROR_1001));
+    }
+
+    @Test
+    void shouldReturn400WhenMfaIdentifierParameterIsEmpty() {
+        event.withPathParameters(
+                Map.of("publicSubjectId", "some-public-subject-id", "mfaIdentifier", ""));
+
+        var result = handler.handleRequest(event, context);
+
+        assertThat(result, hasStatus(400));
+        assertThat(result, hasJsonBody(ErrorResponse.ERROR_1001));
+    }
+
+    @Test
+    void shouldReturn404WhenUserProfileNotFoundForPublicSubject() {
+        when(authenticationService.getOptionalUserProfileFromPublicSubject(PUBLIC_SUBJECT_ID))
+                .thenReturn(Optional.empty());
+
+        event.withBody(updateSmsRequest("123456789"));
+
+        var result = handler.handleRequest(event, context);
+
+        assertThat(result, hasStatus(404));
+        assertThat(result, hasJsonBody(ErrorResponse.ERROR_1056));
     }
 
     @Test
@@ -48,4 +214,20 @@ void shouldReturn400WhenFeatureFlagDisabled() {
         var result = handler.handleRequest(event, context);
         assertEquals(400, result.getStatusCode());
     }
+
+    private String updateSmsRequest(String phoneNumber) {
+        return format(
+                """
+        {
+          "mfaMethod": {
+            "priorityIdentifier": "DEFAULT",
+            "method": {
+                "mfaMethodType": "SMS",
+                "phoneNumber": "%s"
+            }
+          }
+        }
+        """,
+                phoneNumber);
+    }
 }
diff --git a/account-management-integration-tests/src/test/java/uk/gov/di/accountmanagement/api/MFAMethodsPutHandlerIntegrationTest.java b/account-management-integration-tests/src/test/java/uk/gov/di/accountmanagement/api/MFAMethodsPutHandlerIntegrationTest.java
new file mode 100644
index 00000000000..a5d8cd8b839
--- /dev/null
+++ b/account-management-integration-tests/src/test/java/uk/gov/di/accountmanagement/api/MFAMethodsPutHandlerIntegrationTest.java
@@ -0,0 +1,268 @@
+package uk.gov.di.accountmanagement.api;
+
+import com.google.gson.JsonParser;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import uk.gov.di.accountmanagement.lambda.MFAMethodsPutHandler;
+import uk.gov.di.authentication.shared.entity.PriorityIdentifier;
+import uk.gov.di.authentication.shared.entity.mfa.MFAMethod;
+import uk.gov.di.authentication.shared.services.ConfigurationService;
+import uk.gov.di.authentication.sharedtest.basetest.ApiGatewayHandlerIntegrationTest;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+
+import static java.lang.String.format;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+class MFAMethodsPutHandlerIntegrationTest extends ApiGatewayHandlerIntegrationTest {
+
+    private static final String TEST_EMAIL = "test@email.com";
+    private static final String TEST_PASSWORD = "test-password";
+    private static final String TEST_PHONE_NUMBER = "07123123123";
+    private static final String TEST_CREDENTIAL = "ZZ11BB22CC33DD44EE55FF66GG77HH88II99JJ00";
+    private static String testPublicSubject;
+    private static final MFAMethod defaultPrioritySms =
+            MFAMethod.smsMfaMethod(
+                    true,
+                    true,
+                    TEST_PHONE_NUMBER,
+                    PriorityIdentifier.DEFAULT,
+                    UUID.randomUUID().toString());
+    private static final MFAMethod defaultPriorityAuthApp =
+            MFAMethod.authAppMfaMethod(
+                    TEST_CREDENTIAL,
+                    true,
+                    true,
+                    PriorityIdentifier.DEFAULT,
+                    UUID.randomUUID().toString());
+
+    @BeforeEach
+    void setUp() {
+        ConfigurationService mfaMethodEnabledConfigurationService =
+                new ConfigurationService() {
+                    @Override
+                    public boolean isMfaMethodManagementApiEnabled() {
+                        return true;
+                    }
+                };
+
+        handler = new MFAMethodsPutHandler(mfaMethodEnabledConfigurationService);
+        userStore.signUp(TEST_EMAIL, TEST_PASSWORD);
+        testPublicSubject =
+                userStore.getUserProfileFromEmail(TEST_EMAIL).get().getPublicSubjectID();
+    }
+
+    @Test
+    void shouldReturn200AndMfaMethodDataWhenAuthAppUserUpdatesTheirCredential() {
+        userStore.addMfaMethodSupportingMultiple(TEST_EMAIL, defaultPriorityAuthApp);
+        var mfaIdentifier = defaultPriorityAuthApp.getMfaIdentifier();
+        var updatedCredential = "some-new-credential";
+        var updateRequest =
+                format(
+                        """
+                        {
+                          "mfaMethod": {
+                            "priorityIdentifier": "DEFAULT",
+                            "method": {
+                                "mfaMethodType": "AUTH_APP",
+                                "credential": "%s"
+                            }
+                          }
+                        }
+                        """,
+                        updatedCredential);
+
+        var response =
+                makeRequest(
+                        Optional.of(updateRequest),
+                        Collections.emptyMap(),
+                        Collections.emptyMap(),
+                        Map.of(
+                                "publicSubjectId",
+                                testPublicSubject,
+                                "mfaIdentifier",
+                                mfaIdentifier));
+
+        var expectedResponseBody =
+                format(
+                        """
+            {
+                "mfaIdentifier":"%s",
+                "priorityIdentifier":"DEFAULT",
+                "methodVerified":true,
+                "method": {
+                  "mfaMethodType":"AUTH_APP",
+                  "credential":"%s"
+                }
+            }
+            """,
+                        mfaIdentifier, updatedCredential);
+        assertEquals(200, response.getStatusCode());
+
+        var expectedResponse =
+                JsonParser.parseString(expectedResponseBody).getAsJsonObject().toString();
+        assertEquals(expectedResponse, response.getBody());
+
+        var retrievedMfaMethods = userStore.getMfaMethod(TEST_EMAIL);
+
+        assertEquals(1, retrievedMfaMethods.size());
+
+        var retrievedMethod = retrievedMfaMethods.get(0);
+
+        assertRetrievedMethodHasSameBasicFields(defaultPriorityAuthApp, retrievedMethod);
+        assertMfaCredentialUpdated(retrievedMethod, updatedCredential);
+    }
+
+    @Test
+    void shouldReturn200AndMfaMethodDataWhenSmsUserUpdatesTheirPhoneNumber() {
+        userStore.addMfaMethodSupportingMultiple(TEST_EMAIL, defaultPrioritySms);
+        var mfaIdentifier = defaultPrioritySms.getMfaIdentifier();
+        var updatedPhoneNumber = "111222333";
+        var updateRequest =
+                format(
+                        """
+                        {
+                          "mfaMethod": {
+                            "priorityIdentifier": "DEFAULT",
+                            "method": {
+                                "mfaMethodType": "SMS",
+                                "phoneNumber": "%s"
+                            }
+                          }
+                        }
+                        """,
+                        updatedPhoneNumber);
+
+        var response =
+                makeRequest(
+                        Optional.of(updateRequest),
+                        Collections.emptyMap(),
+                        Collections.emptyMap(),
+                        Map.of(
+                                "publicSubjectId",
+                                testPublicSubject,
+                                "mfaIdentifier",
+                                mfaIdentifier));
+
+        var expectedResponseBody =
+                format(
+                        """
+            {
+                "mfaIdentifier":"%s",
+                "priorityIdentifier":"DEFAULT",
+                "methodVerified":true,
+                "method": {
+                  "mfaMethodType":"SMS",
+                  "phoneNumber":"%s"
+                }
+            }
+            """,
+                        mfaIdentifier, updatedPhoneNumber);
+        assertEquals(200, response.getStatusCode());
+
+        var expectedResponse =
+                JsonParser.parseString(expectedResponseBody).getAsJsonObject().toString();
+        assertEquals(expectedResponse, response.getBody());
+
+        var retrievedMfaMethods = userStore.getMfaMethod(TEST_EMAIL);
+
+        assertEquals(1, retrievedMfaMethods.size());
+
+        var retrievedMethod = retrievedMfaMethods.get(0);
+
+        assertRetrievedMethodHasSameBasicFields(defaultPrioritySms, retrievedMethod);
+        assertMfaPhoneNumberUpdated(retrievedMethod, updatedPhoneNumber);
+    }
+
+    @Test
+    void duplicateUpdatesShouldBeIdempotent() {
+        userStore.addMfaMethodSupportingMultiple(TEST_EMAIL, defaultPriorityAuthApp);
+        var mfaIdentifier = defaultPriorityAuthApp.getMfaIdentifier();
+        var updatedCredential = "some-new-credential";
+        var updateRequest =
+                format(
+                        """
+                        {
+                          "mfaMethod": {
+                            "priorityIdentifier": "DEFAULT",
+                            "method": {
+                                "mfaMethodType": "AUTH_APP",
+                                "credential": "%s"
+                            }
+                          }
+                        }
+                        """,
+                        updatedCredential);
+
+        var firstResponse =
+                makeRequest(
+                        Optional.of(updateRequest),
+                        Collections.emptyMap(),
+                        Collections.emptyMap(),
+                        Map.of(
+                                "publicSubjectId",
+                                testPublicSubject,
+                                "mfaIdentifier",
+                                mfaIdentifier));
+
+        assertEquals(200, firstResponse.getStatusCode());
+
+        var retrievedMfaMethods = userStore.getMfaMethod(TEST_EMAIL);
+
+        assertEquals(1, retrievedMfaMethods.size());
+
+        var retrievedMethodAfterFirstRequest = retrievedMfaMethods.get(0);
+
+        assertRetrievedMethodHasSameBasicFields(
+                defaultPriorityAuthApp, retrievedMethodAfterFirstRequest);
+        assertMfaCredentialUpdated(retrievedMethodAfterFirstRequest, updatedCredential);
+
+        for (int i = 0; i < 5; i++) {
+            var response =
+                    makeRequest(
+                            Optional.of(updateRequest),
+                            Collections.emptyMap(),
+                            Collections.emptyMap(),
+                            Map.of(
+                                    "publicSubjectId",
+                                    testPublicSubject,
+                                    "mfaIdentifier",
+                                    mfaIdentifier));
+
+            assertEquals(204, response.getStatusCode());
+
+            var retrievedMethodsAfterSubsequentUpdates = userStore.getMfaMethod(TEST_EMAIL);
+
+            assertEquals(1, retrievedMethodsAfterSubsequentUpdates.size());
+
+            var retrievedMethod = retrievedMethodsAfterSubsequentUpdates.get(0);
+
+            assertRetrievedMethodHasSameBasicFields(defaultPriorityAuthApp, retrievedMethod);
+            assertEquals(
+                    retrievedMethodAfterFirstRequest.getCredentialValue(),
+                    retrievedMethod.getCredentialValue());
+        }
+    }
+
+    private void assertRetrievedMethodHasSameBasicFields(MFAMethod expected, MFAMethod retrieved) {
+        assertEquals(expected.getMfaMethodType(), retrieved.getMfaMethodType());
+        assertEquals(expected.getPriority(), retrieved.getPriority());
+        assertEquals(expected.getMfaIdentifier(), retrieved.getMfaIdentifier());
+        assertEquals(expected.isEnabled(), retrieved.isEnabled());
+        assertEquals(expected.isMethodVerified(), retrieved.isMethodVerified());
+    }
+
+    private void assertMfaCredentialUpdated(MFAMethod retrieved, String updatedCredential) {
+        assertEquals(retrieved.getCredentialValue(), updatedCredential);
+        assertNull(retrieved.getDestination());
+    }
+
+    private void assertMfaPhoneNumberUpdated(MFAMethod retrieved, String updatedPhone) {
+        assertEquals(retrieved.getDestination(), updatedPhone);
+        assertNull(retrieved.getCredentialValue());
+    }
+}
diff --git a/integration-tests/src/test/java/uk/gov/di/authentication/services/DynamoServiceIntegrationTest.java b/integration-tests/src/test/java/uk/gov/di/authentication/services/DynamoServiceIntegrationTest.java
index ab9ee54d9ff..39fcc20ba69 100644
--- a/integration-tests/src/test/java/uk/gov/di/authentication/services/DynamoServiceIntegrationTest.java
+++ b/integration-tests/src/test/java/uk/gov/di/authentication/services/DynamoServiceIntegrationTest.java
@@ -21,9 +21,11 @@
 import java.util.Optional;
 import java.util.function.Predicate;
 
+import static java.lang.String.format;
 import static java.util.Collections.emptyList;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.equalTo;
+import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 
@@ -273,6 +275,158 @@ void shouldNotDeleteAnyMfaMethodsIfNoneWithTheIdentifierExists() {
                     userCredentials, defaultPrioritySmsData, backupPrioritySmsData);
         }
 
+        @Test
+        void shouldUpdateAPhoneNumber() {
+            dynamoService.addMFAMethodSupportingMultiple(TEST_EMAIL, defaultPrioritySmsData);
+            dynamoService.addMFAMethodSupportingMultiple(TEST_EMAIL, backupPrioritySmsData);
+
+            var updatedPhoneNumber = "111222333";
+
+            var result =
+                    dynamoService.updateMigratedMethodPhoneNumber(
+                            TEST_EMAIL,
+                            updatedPhoneNumber,
+                            defaultPrioritySmsData.getMfaIdentifier());
+
+            var expectedUpdatedDefaultSmsMethod =
+                    MFAMethod.smsMfaMethod(
+                            defaultPrioritySmsData.isMethodVerified(),
+                            defaultPrioritySmsData.isEnabled(),
+                            updatedPhoneNumber,
+                            PriorityIdentifier.valueOf(defaultPrioritySmsData.getPriority()),
+                            defaultPrioritySmsData.getMfaIdentifier());
+
+            assertRetrievedMethodHasData(expectedUpdatedDefaultSmsMethod, result.get());
+
+            var userCredentials = dynamoService.getUserCredentialsFromEmail(TEST_EMAIL);
+
+            assertBackupAndDefaultMfaMethodsWithData(
+                    userCredentials, expectedUpdatedDefaultSmsMethod, backupPrioritySmsData);
+        }
+
+        @Test
+        void shouldUpdateAnAuthAppCredential() {
+            dynamoService.addMFAMethodSupportingMultiple(TEST_EMAIL, defaultPriorityAuthAppData);
+            dynamoService.addMFAMethodSupportingMultiple(TEST_EMAIL, backupPrioritySmsData);
+
+            var updatedCredential = "some-updated-credential";
+
+            var result =
+                    dynamoService.updateMigratedAuthAppCredential(
+                            TEST_EMAIL,
+                            updatedCredential,
+                            defaultPriorityAuthAppData.getMfaIdentifier());
+
+            var expectedUpdatedAuthAppMethod =
+                    MFAMethod.authAppMfaMethod(
+                            updatedCredential,
+                            defaultPriorityAuthAppData.isMethodVerified(),
+                            defaultPriorityAuthAppData.isEnabled(),
+                            PriorityIdentifier.valueOf(defaultPriorityAuthAppData.getPriority()),
+                            defaultPriorityAuthAppData.getMfaIdentifier());
+
+            assertRetrievedMethodHasData(result.get(), expectedUpdatedAuthAppMethod);
+
+            var userCredentials = dynamoService.getUserCredentialsFromEmail(TEST_EMAIL);
+
+            assertBackupAndDefaultMfaMethodsWithData(
+                    userCredentials, expectedUpdatedAuthAppMethod, backupPrioritySmsData);
+        }
+
+        @Test
+        void
+                shouldReturnAnErrorAndDoNoUpdatesForUpdatePhoneNumberIfMfaMethodByIdentifierDoesNotExist() {
+            dynamoService.addMFAMethodSupportingMultiple(TEST_EMAIL, defaultPrioritySmsData);
+            dynamoService.addMFAMethodSupportingMultiple(TEST_EMAIL, backupPrioritySmsData);
+
+            var updatedPhoneNumber = "111222333";
+
+            var result =
+                    dynamoService.updateMigratedMethodPhoneNumber(
+                            TEST_EMAIL, updatedPhoneNumber, "some-other-identifier");
+
+            assertEquals(
+                    "Mfa method with identifier some-other-identifier does not exist",
+                    result.getLeft());
+
+            var userCredentials = dynamoService.getUserCredentialsFromEmail(TEST_EMAIL);
+
+            assertBackupAndDefaultMfaMethodsWithData(
+                    userCredentials, defaultPrioritySmsData, backupPrioritySmsData);
+        }
+
+        @Test
+        void
+                shouldReturnAnErrorAndDoNoUpdatesForUpdateAuthAppCredentialIfMfaMethodByIdentifierDoesNotExist() {
+            dynamoService.addMFAMethodSupportingMultiple(TEST_EMAIL, defaultPriorityAuthAppData);
+            dynamoService.addMFAMethodSupportingMultiple(TEST_EMAIL, backupPrioritySmsData);
+
+            var updatedCredential = "some-updated-credential";
+
+            var result =
+                    dynamoService.updateMigratedAuthAppCredential(
+                            TEST_EMAIL, updatedCredential, "some-other-identifier");
+
+            assertEquals(
+                    "Mfa method with identifier some-other-identifier does not exist",
+                    result.getLeft());
+
+            var userCredentials = dynamoService.getUserCredentialsFromEmail(TEST_EMAIL);
+
+            assertBackupAndDefaultMfaMethodsWithData(
+                    userCredentials, defaultPriorityAuthAppData, backupPrioritySmsData);
+        }
+
+        @Test
+        void shouldReturnAnErrorAndDoNoUpdatesWhenCallingUpdatePhoneNumberIfMfaMethodIsNotSms() {
+            dynamoService.addMFAMethodSupportingMultiple(TEST_EMAIL, defaultPriorityAuthAppData);
+            dynamoService.addMFAMethodSupportingMultiple(TEST_EMAIL, backupPrioritySmsData);
+
+            var updatedPhoneNumber = "111222333";
+
+            var result =
+                    dynamoService.updateMigratedMethodPhoneNumber(
+                            TEST_EMAIL,
+                            updatedPhoneNumber,
+                            defaultPriorityAuthAppData.getMfaIdentifier());
+
+            assertEquals(
+                    format(
+                            "Attempted to update phone number for non sms method with identifier %s",
+                            defaultPriorityAuthAppData.getMfaIdentifier()),
+                    result.getLeft());
+
+            var userCredentials = dynamoService.getUserCredentialsFromEmail(TEST_EMAIL);
+
+            assertBackupAndDefaultMfaMethodsWithData(
+                    userCredentials, defaultPriorityAuthAppData, backupPrioritySmsData);
+        }
+
+        @Test
+        void shouldReturnAnErrorAndDoNoUpdatesWhenCallingUpdateAuthAppIfMfaMethodIsNotAuthApp() {
+            dynamoService.addMFAMethodSupportingMultiple(TEST_EMAIL, defaultPrioritySmsData);
+            dynamoService.addMFAMethodSupportingMultiple(TEST_EMAIL, backupPrioritySmsData);
+
+            var updatedCredential = "some-other-credential";
+
+            var result =
+                    dynamoService.updateMigratedAuthAppCredential(
+                            TEST_EMAIL,
+                            updatedCredential,
+                            defaultPrioritySmsData.getMfaIdentifier());
+
+            assertEquals(
+                    format(
+                            "Attempted to update auth app credential for non auth app method with identifier %s",
+                            defaultPrioritySmsData.getMfaIdentifier()),
+                    result.getLeft());
+
+            var userCredentials = dynamoService.getUserCredentialsFromEmail(TEST_EMAIL);
+
+            assertBackupAndDefaultMfaMethodsWithData(
+                    userCredentials, defaultPrioritySmsData, backupPrioritySmsData);
+        }
+
         private MFAMethod findMethodWithPriority(
                 String priority, List<MFAMethod> retrievedMethods) {
             Predicate<MFAMethod> findCondition =
diff --git a/integration-tests/src/test/java/uk/gov/di/authentication/services/MfaMethodsServiceIntegrationTest.java b/integration-tests/src/test/java/uk/gov/di/authentication/services/MfaMethodsServiceIntegrationTest.java
index 3d02dc013ef..d2c7cb0d6f1 100644
--- a/integration-tests/src/test/java/uk/gov/di/authentication/services/MfaMethodsServiceIntegrationTest.java
+++ b/integration-tests/src/test/java/uk/gov/di/authentication/services/MfaMethodsServiceIntegrationTest.java
@@ -7,6 +7,7 @@
 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.junit.jupiter.params.provider.ValueSource;
 import uk.gov.di.authentication.shared.entity.PriorityIdentifier;
@@ -14,13 +15,14 @@
 import uk.gov.di.authentication.shared.entity.mfa.MFAMethod;
 import uk.gov.di.authentication.shared.entity.mfa.MFAMethodType;
 import uk.gov.di.authentication.shared.entity.mfa.MfaDetail;
-import uk.gov.di.authentication.shared.entity.mfa.MfaMethodCreateRequest;
+import uk.gov.di.authentication.shared.entity.mfa.MfaMethodCreateOrUpdateRequest;
 import uk.gov.di.authentication.shared.entity.mfa.MfaMethodData;
 import uk.gov.di.authentication.shared.entity.mfa.SmsMfaDetail;
 import uk.gov.di.authentication.shared.services.ConfigurationService;
 import uk.gov.di.authentication.shared.services.mfa.MfaCreateFailureReason;
 import uk.gov.di.authentication.shared.services.mfa.MfaDeleteFailureReason;
 import uk.gov.di.authentication.shared.services.mfa.MfaMethodsService;
+import uk.gov.di.authentication.shared.services.mfa.MfaUpdateFailureReason;
 import uk.gov.di.authentication.sharedtest.extensions.UserStoreExtension;
 
 import java.util.List;
@@ -35,18 +37,42 @@
 
 class MfaMethodsServiceIntegrationTest {
 
-    private static final String TEST_EMAIL = "joe.bloggs@example.com";
-    private static final String TEST_PHONE_NUMBER = "07123123123";
+    private static final String EMAIL = "joe.bloggs@example.com";
     private static final String PHONE_NUMBER = "+44123456789";
+    private static final String PHONE_NUMBER_TWO = "987654321";
     private static final String AUTH_APP_CREDENTIAL = "some-credential";
+    private static final String SMS_MFA_IDENTIFIER_1 = "ea83592f-b9bf-436f-b4f4-ee33f610ee05";
+    private static final String SMS_MFA_IDENTIFIER_2 = "3634a5e3-dac8-4804-8d40-181722b48ae1";
+    private static final String APP_MFA_IDENTIFIER_1 = "a87e57e5-6175-4be7-af7d-547a390b36c1";
+    private static final String APP_MFA_IDENTIFIER_2 = "898a7e13-c354-430a-a3ca-8cc6c6391057";
+    private static final MFAMethod defaultPriorityAuthApp =
+            MFAMethod.authAppMfaMethod(
+                    AUTH_APP_CREDENTIAL,
+                    true,
+                    true,
+                    PriorityIdentifier.DEFAULT,
+                    APP_MFA_IDENTIFIER_1);
+    private static final String AUTH_APP_CREDENTIAL_TWO = "another-credential";
+    private static final MFAMethod backupPriorityAuthApp =
+            MFAMethod.authAppMfaMethod(
+                    AUTH_APP_CREDENTIAL_TWO,
+                    true,
+                    true,
+                    PriorityIdentifier.BACKUP,
+                    APP_MFA_IDENTIFIER_2);
+    private static final MFAMethod defaultPrioritySms =
+            MFAMethod.smsMfaMethod(
+                    true, true, PHONE_NUMBER, PriorityIdentifier.DEFAULT, SMS_MFA_IDENTIFIER_1);
+    private static final MFAMethod backupPrioritySms =
+            MFAMethod.smsMfaMethod(
+                    true, true, PHONE_NUMBER_TWO, PriorityIdentifier.BACKUP, SMS_MFA_IDENTIFIER_2);
     MfaMethodsService mfaMethodsService = new MfaMethodsService(ConfigurationService.getInstance());
 
     @RegisterExtension static UserStoreExtension userStoreExtension = new UserStoreExtension();
 
     @Nested
-    class WhenAUserIsNotMigrated {
+    class RetrieveWhenAUserIsNotMigrated {
 
-        private static final String EMAIL = "joe.bloggs@example.com";
         private static final String EXPLICITLY_NON_MIGRATED_USER_EMAIL = "not-migrated@example.com";
 
         @BeforeEach
@@ -123,42 +149,7 @@ void shouldReturnNoMethodsWhenSmsMethodNotVerified(String email) {
     }
 
     @Nested
-    class WhenAUserIsMigrated {
-
-        private static final String EMAIL = "joe.bloggs@example.com";
-
-        private static final String SMS_MFA_IDENTIFIER_1 = "ea83592f-b9bf-436f-b4f4-ee33f610ee05";
-        private static final String SMS_MFA_IDENTIFIER_2 = "3634a5e3-dac8-4804-8d40-181722b48ae1";
-        private static final String APP_MFA_IDENTIFIER_1 = "a87e57e5-6175-4be7-af7d-547a390b36c1";
-        private static final String APP_MFA_IDENTIFIER_2 = "898a7e13-c354-430a-a3ca-8cc6c6391057";
-        private static final String PHONE_NUMBER_TWO = "987654321";
-
-        private static final MFAMethod defaultPriorityAuthApp =
-                MFAMethod.authAppMfaMethod(
-                        AUTH_APP_CREDENTIAL,
-                        true,
-                        true,
-                        PriorityIdentifier.DEFAULT,
-                        APP_MFA_IDENTIFIER_1);
-        private static final String AUTH_APP_CREDENTIAL_TWO = "another-credential";
-        private static final MFAMethod backupPriorityAuthApp =
-                MFAMethod.authAppMfaMethod(
-                        AUTH_APP_CREDENTIAL_TWO,
-                        true,
-                        true,
-                        PriorityIdentifier.BACKUP,
-                        APP_MFA_IDENTIFIER_2);
-        private static final MFAMethod defaultPrioritySms =
-                MFAMethod.smsMfaMethod(
-                        true, true, PHONE_NUMBER, PriorityIdentifier.DEFAULT, SMS_MFA_IDENTIFIER_1);
-        private static final MFAMethod backupPrioritySms =
-                MFAMethod.smsMfaMethod(
-                        true,
-                        true,
-                        PHONE_NUMBER_TWO,
-                        PriorityIdentifier.BACKUP,
-                        SMS_MFA_IDENTIFIER_2);
-
+    class RetrieveWhenAUserIsMigrated {
         @BeforeEach
         void setUp() {
             userStoreExtension.signUp(EMAIL, "password-1", new Subject());
@@ -213,165 +204,360 @@ void shouldReturnMultipleMethodsWhenTheyExist(List<MFAMethod> mfaMethods) {
                             .toList();
             assertEquals(expectedData, result);
         }
+    }
+
+    @Nested
+    class AddBackupMfaTests {
+        @BeforeEach
+        void setUp() {
+            userStoreExtension.signUp(EMAIL, "password-1", new Subject());
+            userStoreExtension.setMfaMethodsMigrated(EMAIL, true);
+        }
+
+        @Test
+        void authAppUserShouldSuccessfullyAddSmsMfaInPost() {
+            userStoreExtension.addMfaMethodSupportingMultiple(
+                    MfaMethodsServiceIntegrationTest.EMAIL, defaultPriorityAuthApp);
+            SmsMfaDetail smsMfaDetail = new SmsMfaDetail(MFAMethodType.SMS, PHONE_NUMBER);
+
+            MfaMethodCreateOrUpdateRequest.MfaMethod mfaMethod =
+                    new MfaMethodCreateOrUpdateRequest.MfaMethod(
+                            PriorityIdentifier.BACKUP, smsMfaDetail);
+
+            var result =
+                    mfaMethodsService
+                            .addBackupMfa(MfaMethodsServiceIntegrationTest.EMAIL, mfaMethod)
+                            .get();
+
+            List<MFAMethod> mfaMethods =
+                    userStoreExtension.getMfaMethod(MfaMethodsServiceIntegrationTest.EMAIL);
+            boolean smsMethodExists =
+                    mfaMethods.stream()
+                            .anyMatch(
+                                    method ->
+                                            method.getMfaMethodType()
+                                                    .equals(MFAMethodType.SMS.getValue()));
+
+            assertTrue(smsMethodExists);
+            assertDoesNotThrow(() -> UUID.fromString(result.mfaIdentifier()));
+            assertEquals(PriorityIdentifier.BACKUP, result.priorityIdentifier());
+            assertTrue(result.methodVerified());
+            assertEquals(smsMfaDetail, result.method());
+        }
+
+        @Test
+        void smsUserShouldSuccessfullyAddAuthAppMfa() {
+            userStoreExtension.addMfaMethodSupportingMultiple(
+                    MfaMethodsServiceIntegrationTest.EMAIL, defaultPrioritySms);
+
+            AuthAppMfaDetail authAppMfaDetail =
+                    new AuthAppMfaDetail(MFAMethodType.AUTH_APP, AUTH_APP_CREDENTIAL);
+
+            MfaMethodCreateOrUpdateRequest.MfaMethod mfaMethod =
+                    new MfaMethodCreateOrUpdateRequest.MfaMethod(
+                            PriorityIdentifier.BACKUP, authAppMfaDetail);
+
+            var result =
+                    mfaMethodsService
+                            .addBackupMfa(MfaMethodsServiceIntegrationTest.EMAIL, mfaMethod)
+                            .get();
+
+            List<MFAMethod> mfaMethods =
+                    userStoreExtension.getMfaMethod(MfaMethodsServiceIntegrationTest.EMAIL);
+            boolean authAppMethodExists =
+                    mfaMethods.stream()
+                            .anyMatch(
+                                    method ->
+                                            method.getMfaMethodType()
+                                                    .equals(MFAMethodType.AUTH_APP.getValue()));
+
+            assertTrue(authAppMethodExists);
+            assertDoesNotThrow(() -> UUID.fromString(result.mfaIdentifier()));
+            assertEquals(PriorityIdentifier.BACKUP, result.priorityIdentifier());
+            assertTrue(result.methodVerified());
+            assertEquals(authAppMfaDetail, result.method());
+        }
+
+        @Test
+        void shouldErrorWhenPriorityIdentifierIsDefault() {
+            userStoreExtension.addAuthAppMethod(
+                    MfaMethodsServiceIntegrationTest.EMAIL, true, true, AUTH_APP_CREDENTIAL);
+            SmsMfaDetail smsMfaDetail = new SmsMfaDetail(MFAMethodType.SMS, PHONE_NUMBER);
+
+            MfaMethodCreateOrUpdateRequest request =
+                    new MfaMethodCreateOrUpdateRequest(
+                            new MfaMethodCreateOrUpdateRequest.MfaMethod(
+                                    PriorityIdentifier.DEFAULT, smsMfaDetail));
+
+            var result =
+                    mfaMethodsService.addBackupMfa(
+                            MfaMethodsServiceIntegrationTest.EMAIL, request.mfaMethod());
+
+            assertEquals(MfaCreateFailureReason.INVALID_PRIORITY_IDENTIFIER, result.getLeft());
+        }
+
+        @Test
+        void shouldReturnAtMaximumMfaErrorWhenAddingBackupWithTwoExistingMfaMethods() {
+            userStoreExtension.addMfaMethodSupportingMultiple(EMAIL, defaultPriorityAuthApp);
+            userStoreExtension.addMfaMethodSupportingMultiple(EMAIL, backupPrioritySms);
+
+            MfaMethodCreateOrUpdateRequest request =
+                    new MfaMethodCreateOrUpdateRequest(
+                            new MfaMethodCreateOrUpdateRequest.MfaMethod(
+                                    PriorityIdentifier.BACKUP,
+                                    new SmsMfaDetail(MFAMethodType.SMS, PHONE_NUMBER)));
+
+            var result =
+                    mfaMethodsService.addBackupMfa(
+                            MfaMethodsServiceIntegrationTest.EMAIL, request.mfaMethod());
+
+            assertEquals(
+                    MfaCreateFailureReason.BACKUP_AND_DEFAULT_METHOD_ALREADY_EXIST,
+                    result.getLeft());
+        }
+
+        @Test
+        void shouldReturnPhoneNumberAlreadyExistsErrorWhenSmsMfaUserAddsBackupWithSameNumber() {
+            userStoreExtension.addMfaMethodSupportingMultiple(EMAIL, defaultPrioritySms);
+
+            MfaMethodCreateOrUpdateRequest request =
+                    new MfaMethodCreateOrUpdateRequest(
+                            new MfaMethodCreateOrUpdateRequest.MfaMethod(
+                                    PriorityIdentifier.BACKUP,
+                                    new SmsMfaDetail(MFAMethodType.SMS, PHONE_NUMBER)));
+
+            var result =
+                    mfaMethodsService.addBackupMfa(
+                            MfaMethodsServiceIntegrationTest.EMAIL, request.mfaMethod());
+
+            assertEquals(MfaCreateFailureReason.PHONE_NUMBER_ALREADY_EXISTS, result.getLeft());
+        }
+
+        @Test
+        void shouldReturnAuthAppAlreadyExistsErrorWhenAuthAppMfaUserAddsSecondAuthAppMfa() {
+            userStoreExtension.addMfaMethodSupportingMultiple(EMAIL, defaultPriorityAuthApp);
+
+            MfaMethodCreateOrUpdateRequest request =
+                    new MfaMethodCreateOrUpdateRequest(
+                            new MfaMethodCreateOrUpdateRequest.MfaMethod(
+                                    PriorityIdentifier.BACKUP,
+                                    new AuthAppMfaDetail(
+                                            MFAMethodType.AUTH_APP, AUTH_APP_CREDENTIAL)));
+
+            var result =
+                    mfaMethodsService.addBackupMfa(
+                            MfaMethodsServiceIntegrationTest.EMAIL, request.mfaMethod());
+
+            assertEquals(MfaCreateFailureReason.AUTH_APP_EXISTS, result.getLeft());
+        }
+    }
+
+    @Nested
+    class UpdateMfaMethod {
+
+        private static final AuthAppMfaDetail authAppDetail =
+                new AuthAppMfaDetail(MFAMethodType.AUTH_APP, AUTH_APP_CREDENTIAL);
+        private static final SmsMfaDetail smsMfaDetail =
+                new SmsMfaDetail(MFAMethodType.SMS, PHONE_NUMBER);
+
+        @BeforeEach
+        void setUp() {
+            userStoreExtension.signUp(EMAIL, "password-1", new Subject());
+            userStoreExtension.setMfaMethodsMigrated(EMAIL, true);
+        }
+
+        @Test
+        void returnsAnErrorWhenTheMfaIdentifierIsNotFound() {
+            userStoreExtension.addMfaMethodSupportingMultiple(EMAIL, defaultPriorityAuthApp);
+
+            var request =
+                    MfaMethodCreateOrUpdateRequest.from(PriorityIdentifier.BACKUP, authAppDetail);
+
+            var result = mfaMethodsService.updateMfaMethod(EMAIL, "some-other-identifier", request);
+
+            assertEquals(MfaUpdateFailureReason.UNKOWN_MFA_IDENTIFIER, result.getLeft());
+
+            var remainingMfaMethods = mfaMethodsService.getMfaMethods(EMAIL);
+            assertEquals(List.of(mfaMethodDataFrom(defaultPriorityAuthApp)), remainingMfaMethods);
+        }
 
         @Nested
-        class AddBackupMfaTests {
+        class WhenUpdatingADefaultMethod {
             @Test
-            void authAppUserShouldSuccessfullyAddSmsMfaInPost() {
-                userStoreExtension.addMfaMethodSupportingMultiple(
-                        TEST_EMAIL, defaultPriorityAuthApp);
-                SmsMfaDetail smsMfaDetail = new SmsMfaDetail(MFAMethodType.SMS, PHONE_NUMBER);
-
-                MfaMethodCreateRequest.MfaMethod mfaMethod =
-                        new MfaMethodCreateRequest.MfaMethod(
-                                PriorityIdentifier.BACKUP, smsMfaDetail);
-
-                var result = mfaMethodsService.addBackupMfa(TEST_EMAIL, mfaMethod).get();
-
-                List<MFAMethod> mfaMethods = userStoreExtension.getMfaMethod(TEST_EMAIL);
-                boolean smsMethodExists =
-                        mfaMethods.stream()
-                                .anyMatch(
-                                        method ->
-                                                method.getMfaMethodType()
-                                                        .equals(MFAMethodType.SMS.getValue()));
-
-                assertTrue(smsMethodExists);
-                assertDoesNotThrow(() -> UUID.fromString(result.mfaIdentifier()));
-                assertEquals(PriorityIdentifier.BACKUP, result.priorityIdentifier());
-                assertTrue(result.methodVerified());
-                assertEquals(smsMfaDetail, result.method());
+            void returnsSuccessAndUpdatesMethodWhenAttemptingToUpdateAnAuthAppCredential() {
+                userStoreExtension.addMfaMethodSupportingMultiple(EMAIL, defaultPriorityAuthApp);
+                userStoreExtension.addMfaMethodSupportingMultiple(EMAIL, backupPrioritySms);
+
+                var detailWithUpdatedCredential =
+                        new AuthAppMfaDetail(MFAMethodType.AUTH_APP, AUTH_APP_CREDENTIAL_TWO);
+                var request =
+                        MfaMethodCreateOrUpdateRequest.from(
+                                PriorityIdentifier.DEFAULT, detailWithUpdatedCredential);
+
+                var result =
+                        mfaMethodsService.updateMfaMethod(
+                                EMAIL, defaultPriorityAuthApp.getMfaIdentifier(), request);
+
+                var expectedMfaData =
+                        MfaMethodData.authAppMfaData(
+                                defaultPriorityAuthApp.getMfaIdentifier(),
+                                PriorityIdentifier.DEFAULT,
+                                true,
+                                AUTH_APP_CREDENTIAL_TWO);
+
+                assertEquals(expectedMfaData, result.get());
+
+                var remainingMfaMethods = mfaMethodsService.getMfaMethods(EMAIL);
+                var expectedRemainingMethods =
+                        List.of(expectedMfaData, mfaMethodDataFrom(backupPrioritySms));
+                assertEquals(
+                        expectedRemainingMethods.stream().sorted().toList(),
+                        remainingMfaMethods.stream().sorted().toList());
             }
 
             @Test
-            void smsUserShouldSuccessfullyAddAuthAppMfa() {
-                userStoreExtension.addMfaMethodSupportingMultiple(TEST_EMAIL, defaultPrioritySms);
-
-                AuthAppMfaDetail authAppMfaDetail =
-                        new AuthAppMfaDetail(MFAMethodType.AUTH_APP, AUTH_APP_CREDENTIAL);
-
-                MfaMethodCreateRequest.MfaMethod mfaMethod =
-                        new MfaMethodCreateRequest.MfaMethod(
-                                PriorityIdentifier.BACKUP, authAppMfaDetail);
-
-                var result = mfaMethodsService.addBackupMfa(TEST_EMAIL, mfaMethod).get();
-
-                List<MFAMethod> mfaMethods = userStoreExtension.getMfaMethod(TEST_EMAIL);
-                boolean authAppMethodExists =
-                        mfaMethods.stream()
-                                .anyMatch(
-                                        method ->
-                                                method.getMfaMethodType()
-                                                        .equals(MFAMethodType.AUTH_APP.getValue()));
-
-                assertTrue(authAppMethodExists);
-                assertDoesNotThrow(() -> UUID.fromString(result.mfaIdentifier()));
-                assertEquals(PriorityIdentifier.BACKUP, result.priorityIdentifier());
-                assertTrue(result.methodVerified());
-                assertEquals(authAppMfaDetail, result.method());
+            void returnsSuccessWhenAttemptingToUpdateAnSmsNumber() {
+                userStoreExtension.addMfaMethodSupportingMultiple(EMAIL, defaultPrioritySms);
+                userStoreExtension.addMfaMethodSupportingMultiple(EMAIL, backupPrioritySms);
+
+                var aThirdPhoneNumber = "111222333";
+
+                var detailWithUpdatedNumber =
+                        new SmsMfaDetail(MFAMethodType.SMS, aThirdPhoneNumber);
+                var request =
+                        MfaMethodCreateOrUpdateRequest.from(
+                                PriorityIdentifier.DEFAULT, detailWithUpdatedNumber);
+
+                var result =
+                        mfaMethodsService.updateMfaMethod(
+                                EMAIL, defaultPrioritySms.getMfaIdentifier(), request);
+
+                var expectedMfaData =
+                        MfaMethodData.smsMethodData(
+                                defaultPrioritySms.getMfaIdentifier(),
+                                PriorityIdentifier.DEFAULT,
+                                true,
+                                aThirdPhoneNumber);
+
+                assertEquals(expectedMfaData, result.get());
+
+                var methodsInDatabase =
+                        mfaMethodsService.getMfaMethods(EMAIL).stream().sorted().toList();
+                var expectedMethods =
+                        List.of(mfaMethodDataFrom(backupPrioritySms), expectedMfaData).stream()
+                                .sorted()
+                                .toList();
+                assertEquals(expectedMethods, methodsInDatabase);
             }
 
             @Test
-            void shouldErrorWhenPriorityIdentifierIsDefault() {
-                userStoreExtension.addAuthAppMethod(TEST_EMAIL, true, true, AUTH_APP_CREDENTIAL);
-                SmsMfaDetail smsMfaDetail = new SmsMfaDetail(MFAMethodType.SMS, PHONE_NUMBER);
+            void returnsFailureWhenAttemptingToUpdateAnSmsNumberToTheBackupNumber() {
+                userStoreExtension.addMfaMethodSupportingMultiple(EMAIL, defaultPrioritySms);
+                userStoreExtension.addMfaMethodSupportingMultiple(EMAIL, backupPrioritySms);
 
-                MfaMethodCreateRequest request =
-                        new MfaMethodCreateRequest(
-                                new MfaMethodCreateRequest.MfaMethod(
-                                        PriorityIdentifier.DEFAULT, smsMfaDetail));
+                var detailWithUpdatedNumber =
+                        new SmsMfaDetail(MFAMethodType.SMS, backupPrioritySms.getDestination());
+                var request =
+                        MfaMethodCreateOrUpdateRequest.from(
+                                PriorityIdentifier.DEFAULT, detailWithUpdatedNumber);
 
-                var result = mfaMethodsService.addBackupMfa(TEST_EMAIL, request.mfaMethod());
+                var result =
+                        mfaMethodsService.updateMfaMethod(
+                                EMAIL, defaultPrioritySms.getMfaIdentifier(), request);
 
-                assertEquals(MfaCreateFailureReason.INVALID_PRIORITY_IDENTIFIER, result.getLeft());
+                assertEquals(
+                        MfaUpdateFailureReason.ATTEMPT_TO_UPDATE_PHONE_NUMBER_WITH_BACKUP_NUMBER,
+                        result.getLeft());
+
+                var methodsInDatabase =
+                        mfaMethodsService.getMfaMethods(EMAIL).stream().sorted().toList();
+                var expectedMethods =
+                        List.of(
+                                        mfaMethodDataFrom(backupPrioritySms),
+                                        mfaMethodDataFrom(defaultPrioritySms))
+                                .stream()
+                                .sorted()
+                                .toList();
+                assertEquals(expectedMethods, methodsInDatabase);
             }
 
             @Test
-            void shouldReturnAtMaximumMfaErrorWhenAddingBackupWithTwoExistingMfaMethods() {
+            void returnsAnErrorWhenAttemptingToChangePriorityOfDefaultMethod() {
                 userStoreExtension.addMfaMethodSupportingMultiple(EMAIL, defaultPriorityAuthApp);
-                userStoreExtension.addMfaMethodSupportingMultiple(EMAIL, backupPrioritySms);
-
-                MfaMethodCreateRequest request =
-                        new MfaMethodCreateRequest(
-                                new MfaMethodCreateRequest.MfaMethod(
-                                        PriorityIdentifier.BACKUP,
-                                        new SmsMfaDetail(MFAMethodType.SMS, PHONE_NUMBER)));
+                var request =
+                        MfaMethodCreateOrUpdateRequest.from(
+                                PriorityIdentifier.BACKUP, authAppDetail);
 
-                var result = mfaMethodsService.addBackupMfa(TEST_EMAIL, request.mfaMethod());
+                var result =
+                        mfaMethodsService.updateMfaMethod(
+                                EMAIL, defaultPriorityAuthApp.getMfaIdentifier(), request);
 
                 assertEquals(
-                        MfaCreateFailureReason.BACKUP_AND_DEFAULT_METHOD_ALREADY_EXIST,
+                        MfaUpdateFailureReason.CANNOT_CHANGE_PRIORITY_OF_DEFAULT_METHOD,
                         result.getLeft());
+
+                var remainingMfaMethods = mfaMethodsService.getMfaMethods(EMAIL);
+                assertEquals(
+                        List.of(mfaMethodDataFrom(defaultPriorityAuthApp)), remainingMfaMethods);
             }
 
-            @Test
-            void shouldReturnPhoneNumberAlreadyExistsErrorWhenSmsMfaUserAddsBackupWithSameNumber() {
-                userStoreExtension.addMfaMethodSupportingMultiple(EMAIL, defaultPrioritySms);
+            private static Stream<Arguments> existingMethodsAndRequestedUpdates() {
+                return Stream.of(
+                        Arguments.of(defaultPriorityAuthApp, smsMfaDetail),
+                        Arguments.of(defaultPrioritySms, authAppDetail));
+            }
+
+            @ParameterizedTest
+            @MethodSource("existingMethodsAndRequestedUpdates")
+            void returnsAFailureWhenAttemptingToChangeTypeOfExistingDefaultMethod(
+                    MFAMethod existingMethod, MfaDetail requestedUpdate) {
+                userStoreExtension.addMfaMethodSupportingMultiple(EMAIL, existingMethod);
+                var request =
+                        MfaMethodCreateOrUpdateRequest.from(
+                                PriorityIdentifier.DEFAULT, requestedUpdate);
+
+                var result =
+                        mfaMethodsService.updateMfaMethod(
+                                EMAIL, existingMethod.getMfaIdentifier(), request);
 
-                MfaMethodCreateRequest request =
-                        new MfaMethodCreateRequest(
-                                new MfaMethodCreateRequest.MfaMethod(
-                                        PriorityIdentifier.BACKUP,
-                                        new SmsMfaDetail(MFAMethodType.SMS, PHONE_NUMBER)));
+                assertEquals(
+                        MfaUpdateFailureReason.CANNOT_CHANGE_TYPE_OF_MFA_METHOD, result.getLeft());
 
-                var result = mfaMethodsService.addBackupMfa(TEST_EMAIL, request.mfaMethod());
+                var remainingMfaMethods = mfaMethodsService.getMfaMethods(EMAIL);
+                assertEquals(List.of(mfaMethodDataFrom(existingMethod)), remainingMfaMethods);
+            }
 
-                assertEquals(MfaCreateFailureReason.PHONE_NUMBER_ALREADY_EXISTS, result.getLeft());
+            private static Stream<Arguments> existingMethodsAndNoChangeUpdates() {
+                return Stream.of(
+                        Arguments.of(defaultPriorityAuthApp, authAppDetail),
+                        Arguments.of(defaultPrioritySms, smsMfaDetail));
             }
 
-            @Test
-            void shouldReturnAuthAppAlreadyExistsErrorWhenAuthAppMfaUserAddsSecondAuthAppMfa() {
-                userStoreExtension.addMfaMethodSupportingMultiple(EMAIL, defaultPriorityAuthApp);
+            @ParameterizedTest
+            @MethodSource("existingMethodsAndNoChangeUpdates")
+            void returnsAFailureWhenNoChangeDetected(
+                    MFAMethod existingMethod, MfaDetail requestedUpdate) {
+                userStoreExtension.addMfaMethodSupportingMultiple(EMAIL, existingMethod);
+                var request =
+                        MfaMethodCreateOrUpdateRequest.from(
+                                PriorityIdentifier.DEFAULT, requestedUpdate);
 
-                MfaMethodCreateRequest request =
-                        new MfaMethodCreateRequest(
-                                new MfaMethodCreateRequest.MfaMethod(
-                                        PriorityIdentifier.BACKUP,
-                                        new AuthAppMfaDetail(
-                                                MFAMethodType.AUTH_APP, AUTH_APP_CREDENTIAL)));
+                var result =
+                        mfaMethodsService.updateMfaMethod(
+                                EMAIL, existingMethod.getMfaIdentifier(), request);
 
-                var result = mfaMethodsService.addBackupMfa(TEST_EMAIL, request.mfaMethod());
+                assertEquals(
+                        MfaUpdateFailureReason.REQUEST_TO_UPDATE_MFA_METHOD_WITH_NO_CHANGE,
+                        result.getLeft());
 
-                assertEquals(MfaCreateFailureReason.AUTH_APP_EXISTS, result.getLeft());
+                var remainingMfaMethods = mfaMethodsService.getMfaMethods(EMAIL);
+                assertEquals(List.of(mfaMethodDataFrom(existingMethod)), remainingMfaMethods);
             }
         }
     }
 
     @Nested
     class DeleteMfaMethod {
-        private static final String EMAIL = "joe.bloggs@example.com";
-
-        private static final String SMS_MFA_IDENTIFIER_1 = "ea83592f-b9bf-436f-b4f4-ee33f610ee05";
-        private static final String SMS_MFA_IDENTIFIER_2 = "3634a5e3-dac8-4804-8d40-181722b48ae1";
-        private static final String APP_MFA_IDENTIFIER_1 = "a87e57e5-6175-4be7-af7d-547a390b36c1";
-        private static final String APP_MFA_IDENTIFIER_2 = "898a7e13-c354-430a-a3ca-8cc6c6391057";
-        private static final String PHONE_NUMBER_TWO = "987654321";
-
-        private static final MFAMethod defaultPriorityAuthApp =
-                MFAMethod.authAppMfaMethod(
-                        AUTH_APP_CREDENTIAL,
-                        true,
-                        true,
-                        PriorityIdentifier.DEFAULT,
-                        APP_MFA_IDENTIFIER_1);
-        private static final String AUTH_APP_CREDENTIAL_TWO = "another-credential";
-        private static final MFAMethod backupPriorityAuthApp =
-                MFAMethod.authAppMfaMethod(
-                        AUTH_APP_CREDENTIAL_TWO,
-                        true,
-                        true,
-                        PriorityIdentifier.BACKUP,
-                        APP_MFA_IDENTIFIER_2);
-        private static final MFAMethod defaultPrioritySms =
-                MFAMethod.smsMfaMethod(
-                        true, true, PHONE_NUMBER, PriorityIdentifier.DEFAULT, SMS_MFA_IDENTIFIER_1);
-        private static final MFAMethod backupPrioritySms =
-                MFAMethod.smsMfaMethod(
-                        true,
-                        true,
-                        PHONE_NUMBER_TWO,
-                        PriorityIdentifier.BACKUP,
-                        SMS_MFA_IDENTIFIER_2);
         private String publicSubjectId;
 
         @BeforeEach
diff --git a/shared/src/main/java/uk/gov/di/authentication/shared/entity/ErrorResponse.java b/shared/src/main/java/uk/gov/di/authentication/shared/entity/ErrorResponse.java
index fad35b9cf55..de25ba93f72 100644
--- a/shared/src/main/java/uk/gov/di/authentication/shared/entity/ErrorResponse.java
+++ b/shared/src/main/java/uk/gov/di/authentication/shared/entity/ErrorResponse.java
@@ -81,7 +81,11 @@ public enum ErrorResponse {
     ERROR_1067(1067, "Cannot delete mfa method for non-migrated user"),
     ERROR_1068(1068, "MFA method count limit reached"),
     ERROR_1069(1069, "SMS MFA with same number already exists"),
-    ERROR_1070(1070, "AUTH APP MFA already exists");
+    ERROR_1070(1070, "AUTH APP MFA already exists"),
+    ERROR_1071(1071, "Account Management API encountered Unexpected Error"),
+    ERROR_1072(1072, "Cannot change type of mfa method"),
+    ERROR_1073(1073, "Cannot change priority of default mfa method"),
+    ERROR_1074(1074, "Cannot update primary sms number to number already in use by backup");
 
     private int code;
 
diff --git a/shared/src/main/java/uk/gov/di/authentication/shared/entity/UserCredentials.java b/shared/src/main/java/uk/gov/di/authentication/shared/entity/UserCredentials.java
index 97bcbb85332..0761df20ab6 100644
--- a/shared/src/main/java/uk/gov/di/authentication/shared/entity/UserCredentials.java
+++ b/shared/src/main/java/uk/gov/di/authentication/shared/entity/UserCredentials.java
@@ -174,6 +174,12 @@ public UserCredentials removeMfaMethodByIdentifierIfPresent(String mfaMethodIden
         }
     }
 
+    public UserCredentials withUpdatedMfaMethod(String mfaMethodIdentifier, MFAMethod mfaMethod) {
+        removeMfaMethodByIdentifierIfPresent(mfaMethodIdentifier);
+        this.mfaMethods.add(mfaMethod);
+        return this;
+    }
+
     @DynamoDbAttribute(ATTRIBUTE_TEST_USER)
     @DynamoDbSecondaryPartitionKey(indexNames = {"TestUserIndex"})
     public int getTestUser() {
diff --git a/shared/src/main/java/uk/gov/di/authentication/shared/entity/mfa/MfaMethodCreateRequest.java b/shared/src/main/java/uk/gov/di/authentication/shared/entity/mfa/MfaMethodCreateOrUpdateRequest.java
similarity index 63%
rename from shared/src/main/java/uk/gov/di/authentication/shared/entity/mfa/MfaMethodCreateRequest.java
rename to shared/src/main/java/uk/gov/di/authentication/shared/entity/mfa/MfaMethodCreateOrUpdateRequest.java
index 2fbb9191b5f..af3bbc72389 100644
--- a/shared/src/main/java/uk/gov/di/authentication/shared/entity/mfa/MfaMethodCreateRequest.java
+++ b/shared/src/main/java/uk/gov/di/authentication/shared/entity/mfa/MfaMethodCreateOrUpdateRequest.java
@@ -6,9 +6,15 @@
 import uk.gov.di.authentication.shared.entity.PriorityIdentifier;
 import uk.gov.di.authentication.shared.serialization.MfaDetailDeserializer;
 
-public record MfaMethodCreateRequest(@Expose @SerializedName("mfaMethod") MfaMethod mfaMethod) {
+public record MfaMethodCreateOrUpdateRequest(
+        @Expose @SerializedName("mfaMethod") MfaMethod mfaMethod) {
     public record MfaMethod(
             @Expose @SerializedName("priorityIdentifier") PriorityIdentifier priorityIdentifier,
             @Expose @SerializedName("method") @JsonAdapter(MfaDetailDeserializer.class)
                     MfaDetail method) {}
+
+    public static MfaMethodCreateOrUpdateRequest from(
+            PriorityIdentifier priorityIdentifier, MfaDetail detail) {
+        return new MfaMethodCreateOrUpdateRequest(new MfaMethod(priorityIdentifier, detail));
+    }
 }
diff --git a/shared/src/main/java/uk/gov/di/authentication/shared/entity/mfa/MfaMethodData.java b/shared/src/main/java/uk/gov/di/authentication/shared/entity/mfa/MfaMethodData.java
index 1d7b6414afe..05459c6e39b 100644
--- a/shared/src/main/java/uk/gov/di/authentication/shared/entity/mfa/MfaMethodData.java
+++ b/shared/src/main/java/uk/gov/di/authentication/shared/entity/mfa/MfaMethodData.java
@@ -1,6 +1,8 @@
 package uk.gov.di.authentication.shared.entity.mfa;
 
 import com.google.gson.annotations.Expose;
+import io.vavr.control.Either;
+import org.jetbrains.annotations.NotNull;
 import uk.gov.di.authentication.shared.entity.PriorityIdentifier;
 import uk.gov.di.authentication.shared.validation.Required;
 
@@ -8,7 +10,29 @@ public record MfaMethodData(
         @Expose String mfaIdentifier,
         @Expose @Required PriorityIdentifier priorityIdentifier,
         @Expose @Required boolean methodVerified,
-        @Expose @Required MfaDetail method) {
+        @Expose @Required MfaDetail method)
+        implements Comparable<MfaMethodData> {
+
+    public static Either<String, MfaMethodData> from(MFAMethod mfaMethod) {
+        if (mfaMethod.getMfaMethodType().equals(MFAMethodType.SMS.getValue())) {
+            return Either.right(
+                    smsMethodData(
+                            mfaMethod.getMfaIdentifier(),
+                            PriorityIdentifier.valueOf(mfaMethod.getPriority()),
+                            mfaMethod.isMethodVerified(),
+                            mfaMethod.getDestination()));
+        } else if (mfaMethod.getMfaMethodType().equals(MFAMethodType.AUTH_APP.getValue())) {
+            return Either.right(
+                    authAppMfaData(
+                            mfaMethod.getMfaIdentifier(),
+                            PriorityIdentifier.valueOf(mfaMethod.getPriority()),
+                            mfaMethod.isMethodVerified(),
+                            mfaMethod.getCredentialValue()));
+        } else {
+            return Either.left("Unsupported MFA method type: " + mfaMethod.getMfaMethodType());
+        }
+    }
+
     public static MfaMethodData smsMethodData(
             String mfaIdentifier,
             PriorityIdentifier priorityIdentifier,
@@ -32,4 +56,9 @@ public static MfaMethodData authAppMfaData(
                 methodVerified,
                 new AuthAppMfaDetail(MFAMethodType.AUTH_APP, credential));
     }
+
+    @Override
+    public int compareTo(@NotNull MfaMethodData other) {
+        return this.mfaIdentifier.compareTo(other.mfaIdentifier);
+    }
 }
diff --git a/shared/src/main/java/uk/gov/di/authentication/shared/services/AuthenticationService.java b/shared/src/main/java/uk/gov/di/authentication/shared/services/AuthenticationService.java
index 1af4ce02715..1e1b77c39d0 100644
--- a/shared/src/main/java/uk/gov/di/authentication/shared/services/AuthenticationService.java
+++ b/shared/src/main/java/uk/gov/di/authentication/shared/services/AuthenticationService.java
@@ -1,6 +1,7 @@
 package uk.gov.di.authentication.shared.services;
 
 import com.nimbusds.oauth2.sdk.id.Subject;
+import io.vavr.control.Either;
 import uk.gov.di.authentication.shared.entity.TermsAndConditions;
 import uk.gov.di.authentication.shared.entity.User;
 import uk.gov.di.authentication.shared.entity.UserCredentials;
@@ -82,4 +83,10 @@ void updateMFAMethod(
     void setMfaMethodsMigrated(String email, boolean mfaMethodsMigrated);
 
     void deleteMfaMethodByIdentifier(String email, String mfaMethodIdentifier);
+
+    Either<String, MFAMethod> updateMigratedMethodPhoneNumber(
+            String email, String updatedPhoneNumber, String mfaMethodIdentifier);
+
+    Either<String, MFAMethod> updateMigratedAuthAppCredential(
+            String email, String updatedCredential, String mfaMethodIdentifier);
 }
diff --git a/shared/src/main/java/uk/gov/di/authentication/shared/services/DynamoService.java b/shared/src/main/java/uk/gov/di/authentication/shared/services/DynamoService.java
index 36404d579fe..2180aa70001 100644
--- a/shared/src/main/java/uk/gov/di/authentication/shared/services/DynamoService.java
+++ b/shared/src/main/java/uk/gov/di/authentication/shared/services/DynamoService.java
@@ -1,6 +1,7 @@
 package uk.gov.di.authentication.shared.services;
 
 import com.nimbusds.oauth2.sdk.id.Subject;
+import io.vavr.control.Either;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import software.amazon.awssdk.core.SdkBytes;
@@ -736,6 +737,89 @@ public void deleteMfaMethodByIdentifier(String email, String mfaMethodIdentifier
                         .withUpdated(dateTime));
     }
 
+    @Override
+    public Either<String, MFAMethod> updateMigratedMethodPhoneNumber(
+            String email, String updatedPhoneNumber, String mfaMethodIdentifier) {
+        var userCredentials =
+                dynamoUserCredentialsTable.getItem(
+                        Key.builder().partitionValue(email.toLowerCase(Locale.ROOT)).build());
+        var maybeExistingMethod = getMfaMethodByIdentifier(userCredentials, mfaMethodIdentifier);
+        return maybeExistingMethod.flatMap(
+                existingMethod -> {
+                    if (!existingMethod.getMfaMethodType().equals(MFAMethodType.SMS.getValue())) {
+                        return Either.left(
+                                format(
+                                        "Attempted to update phone number for non sms method with identifier %s",
+                                        mfaMethodIdentifier));
+                    }
+                    return updateMigratedMfaMethod(
+                            existingMethod.withDestination(updatedPhoneNumber),
+                            mfaMethodIdentifier,
+                            userCredentials);
+                });
+    }
+
+    private Either<String, MFAMethod> updateMigratedMfaMethod(
+            MFAMethod updatedMFAMethod,
+            String mfaMethodIdentifier,
+            UserCredentials userCredentials) {
+        var dateTime = NowHelper.toTimestampString(NowHelper.now());
+        var updatedUserCredentials =
+                dynamoUserCredentialsTable.updateItem(
+                        userCredentials
+                                .withUpdated(dateTime)
+                                .withUpdatedMfaMethod(mfaMethodIdentifier, updatedMFAMethod));
+
+        return getMfaMethodByIdentifier(updatedUserCredentials, mfaMethodIdentifier)
+                .mapLeft(
+                        err ->
+                                format(
+                                        "Error when retrieving updated mfa method with identifier: %s no such identifier exists",
+                                        mfaMethodIdentifier));
+    }
+
+    private Either<String, MFAMethod> getMfaMethodByIdentifier(
+            UserCredentials userCredentials, String mfaMethodIdentifier) {
+        var maybeExistingMethod =
+                userCredentials.getMfaMethods().stream()
+                        .filter(
+                                mfaMethod ->
+                                        mfaMethod.getMfaIdentifier().equals(mfaMethodIdentifier))
+                        .findFirst();
+        return maybeExistingMethod
+                .<Either<String, MFAMethod>>map(Either::right)
+                .orElseGet(
+                        () ->
+                                Either.left(
+                                        format(
+                                                "Mfa method with identifier %s does not exist",
+                                                mfaMethodIdentifier)));
+    }
+
+    @Override
+    public Either<String, MFAMethod> updateMigratedAuthAppCredential(
+            String email, String updatedCredential, String mfaMethodIdentifier) {
+        var userCredentials =
+                dynamoUserCredentialsTable.getItem(
+                        Key.builder().partitionValue(email.toLowerCase(Locale.ROOT)).build());
+        var maybeExistingMethod = getMfaMethodByIdentifier(userCredentials, mfaMethodIdentifier);
+        return maybeExistingMethod.flatMap(
+                existingMethod -> {
+                    if (!existingMethod
+                            .getMfaMethodType()
+                            .equals(MFAMethodType.AUTH_APP.getValue())) {
+                        return Either.left(
+                                format(
+                                        "Attempted to update auth app credential for non auth app method with identifier %s",
+                                        mfaMethodIdentifier));
+                    }
+                    return updateMigratedMfaMethod(
+                            existingMethod.withCredentialValue(updatedCredential),
+                            mfaMethodIdentifier,
+                            userCredentials);
+                });
+    }
+
     public Stream<UserProfile> getBulkUserEmailAudienceStreamOnTermsAndConditionsVersion(
             Map<String, AttributeValue> exclusiveStartKey, List<String> termsAndConditionsVersion) {
 
diff --git a/shared/src/main/java/uk/gov/di/authentication/shared/services/mfa/MfaMethodsService.java b/shared/src/main/java/uk/gov/di/authentication/shared/services/mfa/MfaMethodsService.java
index e673826950b..fe9ebccc70b 100644
--- a/shared/src/main/java/uk/gov/di/authentication/shared/services/mfa/MfaMethodsService.java
+++ b/shared/src/main/java/uk/gov/di/authentication/shared/services/mfa/MfaMethodsService.java
@@ -7,7 +7,8 @@
 import uk.gov.di.authentication.shared.entity.mfa.AuthAppMfaDetail;
 import uk.gov.di.authentication.shared.entity.mfa.MFAMethod;
 import uk.gov.di.authentication.shared.entity.mfa.MFAMethodType;
-import uk.gov.di.authentication.shared.entity.mfa.MfaMethodCreateRequest;
+import uk.gov.di.authentication.shared.entity.mfa.MfaDetail;
+import uk.gov.di.authentication.shared.entity.mfa.MfaMethodCreateOrUpdateRequest;
 import uk.gov.di.authentication.shared.entity.mfa.MfaMethodData;
 import uk.gov.di.authentication.shared.entity.mfa.SmsMfaDetail;
 import uk.gov.di.authentication.shared.exceptions.UnknownMfaTypeException;
@@ -19,6 +20,7 @@
 import java.util.UUID;
 
 import static uk.gov.di.authentication.shared.conditions.MfaHelper.getPrimaryMFAMethod;
+import static uk.gov.di.authentication.shared.entity.PriorityIdentifier.BACKUP;
 
 public class MfaMethodsService {
     private static final Logger LOG = LogManager.getLogger(MfaMethodsService.class);
@@ -103,7 +105,7 @@ public Either<MfaDeleteFailureReason, String> deleteMfaMethod(
             return Either.left(MfaDeleteFailureReason.MFA_METHOD_WITH_IDENTIFIER_DOES_NOT_EXIST);
         }
 
-        if (!PriorityIdentifier.BACKUP.name().equals(maybeMethodToDelete.get().getPriority())) {
+        if (!BACKUP.name().equals(maybeMethodToDelete.get().getPriority())) {
             return Either.left(MfaDeleteFailureReason.CANNOT_DELETE_DEFAULT_METHOD);
         }
 
@@ -135,7 +137,7 @@ private List<MfaMethodData> getMfaMethodsForNonMigratedUser(
     }
 
     public Either<MfaCreateFailureReason, MfaMethodData> addBackupMfa(
-            String email, MfaMethodCreateRequest.MfaMethod mfaMethod) {
+            String email, MfaMethodCreateOrUpdateRequest.MfaMethod mfaMethod) {
         if (mfaMethod.priorityIdentifier() == PriorityIdentifier.DEFAULT) {
             return Either.left(MfaCreateFailureReason.INVALID_PRIORITY_IDENTIFIER);
         }
@@ -203,4 +205,98 @@ public Either<MfaCreateFailureReason, MfaMethodData> addBackupMfa(
                             ((AuthAppMfaDetail) mfaMethod.method()).credential()));
         }
     }
+
+    public Either<MfaUpdateFailureReason, MfaMethodData> updateMfaMethod(
+            String email, String mfaIdentifier, MfaMethodCreateOrUpdateRequest request) {
+        var mfaMethods = persistentService.getUserCredentialsFromEmail(email).getMfaMethods();
+
+        var maybeMethodToUpdate =
+                mfaMethods.stream()
+                        .filter(mfaMethod -> mfaIdentifier.equals(mfaMethod.getMfaIdentifier()))
+                        .findFirst();
+
+        return maybeMethodToUpdate
+                .map(
+                        method ->
+                                switch (PriorityIdentifier.valueOf(method.getPriority())) {
+                                    case DEFAULT -> handleDefaultMethodUpdate(
+                                            method,
+                                            request.mfaMethod(),
+                                            email,
+                                            mfaIdentifier,
+                                            mfaMethods);
+                                    case BACKUP -> null; // will be implemented in subsequent PR
+                                })
+                .orElse(Either.left(MfaUpdateFailureReason.UNKOWN_MFA_IDENTIFIER));
+    }
+
+    private Either<MfaUpdateFailureReason, MfaMethodData> handleDefaultMethodUpdate(
+            MFAMethod defaultMethod,
+            MfaMethodCreateOrUpdateRequest.MfaMethod updatedMethod,
+            String email,
+            String mfaIdentifier,
+            List<MFAMethod> allMethodsForUser) {
+        var requestedPriority = updatedMethod.priorityIdentifier();
+        if (requestedPriority == BACKUP) {
+            return Either.left(MfaUpdateFailureReason.CANNOT_CHANGE_PRIORITY_OF_DEFAULT_METHOD);
+        }
+
+        if (updateRequestChangesMethodType(
+                defaultMethod.getMfaMethodType(), updatedMethod.method())) {
+            return Either.left(MfaUpdateFailureReason.CANNOT_CHANGE_TYPE_OF_MFA_METHOD);
+        }
+
+        Either<String, MFAMethod> databaseUpdateResult;
+
+        if (updatedMethod.method() instanceof SmsMfaDetail updatedSmsDetail) {
+            var isExistingDefaultPhoneNumber =
+                    updatedSmsDetail.phoneNumber().equals(defaultMethod.getDestination());
+            var isExistingBackupPhoneNumber =
+                    allMethodsForUser.stream()
+                            .filter(
+                                    mfaMethod ->
+                                            !mfaMethod.getMfaIdentifier().equals(mfaIdentifier))
+                            .anyMatch(
+                                    mfaMethod ->
+                                            mfaMethod
+                                                    .getDestination()
+                                                    .equals(updatedSmsDetail.phoneNumber()));
+            if (isExistingDefaultPhoneNumber) {
+                return Either.left(
+                        MfaUpdateFailureReason.REQUEST_TO_UPDATE_MFA_METHOD_WITH_NO_CHANGE);
+            } else if (isExistingBackupPhoneNumber) {
+                return Either.left(
+                        MfaUpdateFailureReason.ATTEMPT_TO_UPDATE_PHONE_NUMBER_WITH_BACKUP_NUMBER);
+            } else {
+                databaseUpdateResult =
+                        persistentService.updateMigratedMethodPhoneNumber(
+                                email, updatedSmsDetail.phoneNumber(), mfaIdentifier);
+            }
+        } else {
+            var authAppDetail = (AuthAppMfaDetail) updatedMethod.method();
+            if (authAppDetail.credential().equals(defaultMethod.getCredentialValue())) {
+                return Either.left(
+                        MfaUpdateFailureReason.REQUEST_TO_UPDATE_MFA_METHOD_WITH_NO_CHANGE);
+            } else {
+                databaseUpdateResult =
+                        persistentService.updateMigratedAuthAppCredential(
+                                email, authAppDetail.credential(), mfaIdentifier);
+            }
+        }
+
+        return databaseUpdateResult
+                .flatMap(MfaMethodData::from)
+                .mapLeft(
+                        errorString -> {
+                            LOG.error(errorString);
+                            return MfaUpdateFailureReason.UNEXPECTED_ERROR;
+                        });
+    }
+
+    private boolean updateRequestChangesMethodType(
+            String methodTypeFromExisting, MfaDetail updatedMethodDetail) {
+        return updatedMethodDetail instanceof AuthAppMfaDetail
+                ? !MFAMethodType.AUTH_APP.name().equals(methodTypeFromExisting)
+                : !MFAMethodType.SMS.name().equals(methodTypeFromExisting);
+    }
 }
diff --git a/shared/src/main/java/uk/gov/di/authentication/shared/services/mfa/MfaUpdateFailureReason.java b/shared/src/main/java/uk/gov/di/authentication/shared/services/mfa/MfaUpdateFailureReason.java
new file mode 100644
index 00000000000..b9e107ca872
--- /dev/null
+++ b/shared/src/main/java/uk/gov/di/authentication/shared/services/mfa/MfaUpdateFailureReason.java
@@ -0,0 +1,10 @@
+package uk.gov.di.authentication.shared.services.mfa;
+
+public enum MfaUpdateFailureReason {
+    CANNOT_CHANGE_PRIORITY_OF_DEFAULT_METHOD,
+    CANNOT_CHANGE_TYPE_OF_MFA_METHOD,
+    REQUEST_TO_UPDATE_MFA_METHOD_WITH_NO_CHANGE,
+    UNKOWN_MFA_IDENTIFIER,
+    ATTEMPT_TO_UPDATE_PHONE_NUMBER_WITH_BACKUP_NUMBER,
+    UNEXPECTED_ERROR
+}


Reviews:
user: login: andrew-moores
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T12:18:50Z
user: login: BeckaL
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T13:19:26Z
user: login: BeckaL
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T13:20:09Z
user: login: BeckaL
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T13:20:26Z

Review comments:
path: integration-tests/src/test/java/uk/gov/di/authentication/services/DynamoServiceIntegrationTest.java
user: login: andrew-moores
body: 🤔 I wonder if this name change makes more sense in reverse, i.e. the database item called MFAMethodData and the domain model MFAMethod?  Or MFAMethodItem and MFAMethod?
created_at: 2025-03-27T12:07:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -53,8 +76,56 @@ public APIGatewayProxyResponseEvent updateMFAMethodsHandler(
                     configurationService.getEnvironment());
             return generateApiGatewayProxyErrorResponse(400, ErrorResponse.ERROR_1063);
         }
+        var publicSubjectId =
+                input.getPathParameters()
+                        .get("publicSubjectId"); // Error handling for when this empty to come in
+        // subsequent commit
+        var mfaIdentifier =
+                input.getPathParameters()
+                        .get("mfaIdentifier"); // Error handling for when this empty to come in
+        // subsequent commit
+
+        var userProfile =
+                authenticationService
+                        .getOptionalUserProfileFromPublicSubject(publicSubjectId)
+                        .get(); // Error handling for this to come in subsequent commit
+
+        try {
+            var mfaMethodUpdateRequest =
+                    objectMapper.readValue(
+                            input.getBody(), MfaMethodCreateOrUpdateRequest.class, true);
+            var result =
+                    mfaMethodsService.updateMfaMethod(
+                            userProfile.getEmail(), mfaIdentifier, mfaMethodUpdateRequest);
+
+            if (result.isLeft()) {
+                var failureReason = result.getLeft();
+                return handleUpdateMfaFailureReason(failureReason);
+            }
+
+            var successfulUpdate = result.get();
+
+            return generateApiGatewayProxyResponse(200, successfulUpdate, true);
+        } catch (Json.JsonException e) {
+            return generateApiGatewayProxyErrorResponse(400, ErrorResponse.ERROR_1001);
+        }
+    }
 
-        return generateApiGatewayProxyResponse(200, "{}");
+    private static APIGatewayProxyResponseEvent handleUpdateMfaFailureReason(
path: account-management-api/src/main/java/uk/gov/di/accountmanagement/lambda/MFAMethodsPutHandler.java
user: login: andrew-moores
body: Is it worth updating the OpenAPI spec with these errors?
created_at: 2025-03-27T12:14:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2,43 +2,188 @@
 
 import com.amazonaws.services.lambda.runtime.Context;
 import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
+import com.google.gson.JsonParser;
+import io.vavr.control.Either;
 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.MethodSource;
+import uk.gov.di.authentication.shared.entity.ErrorResponse;
+import uk.gov.di.authentication.shared.entity.PriorityIdentifier;
+import uk.gov.di.authentication.shared.entity.UserProfile;
+import uk.gov.di.authentication.shared.entity.mfa.MFAMethodType;
+import uk.gov.di.authentication.shared.entity.mfa.MfaMethodCreateOrUpdateRequest;
+import uk.gov.di.authentication.shared.entity.mfa.MfaMethodData;
+import uk.gov.di.authentication.shared.entity.mfa.SmsMfaDetail;
+import uk.gov.di.authentication.shared.services.AuthenticationService;
 import uk.gov.di.authentication.shared.services.ConfigurationService;
+import uk.gov.di.authentication.shared.services.mfa.MfaMethodsService;
+import uk.gov.di.authentication.shared.services.mfa.MfaUpdateFailureReason;
 
 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.junit.jupiter.api.Assertions.assertEquals;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 import static uk.gov.di.accountmanagement.helpers.CommonTestVariables.VALID_HEADERS;
+import static uk.gov.di.authentication.sharedtest.matchers.APIGatewayProxyResponseEventMatcher.hasJsonBody;
+import static uk.gov.di.authentication.sharedtest.matchers.APIGatewayProxyResponseEventMatcher.hasStatus;
 
 class MFAMethodsPutHandlerTest {
 
     private static final ConfigurationService configurationService =
             mock(ConfigurationService.class);
+    private static final MfaMethodsService mfaMethodsService = mock(MfaMethodsService.class);
+    private static final AuthenticationService authenticationService =
+            mock(AuthenticationService.class);
+    private static final UserProfile userProfile = mock(UserProfile.class);
     private static final Context context = mock(Context.class);
+    private static final String MFA_IDENTIFIER = "some-mfa-identifier";
+    private static final String PUBLIC_SUBJECT_ID = "some-subject-id";
+    private static final String EMAIL = "joe.bloggs@digital.cabinet-office.gov.uk";
 
     private MFAMethodsPutHandler handler;
 
     private final APIGatewayProxyRequestEvent event =
             new APIGatewayProxyRequestEvent()
                     .withPathParameters(
                             Map.ofEntries(
-                                    Map.entry("publicSubjectId", "some-subject-id"),
-                                    Map.entry("mfaIdentifier", "some-mfa-identifier")))
+                                    Map.entry("publicSubjectId", PUBLIC_SUBJECT_ID),
+                                    Map.entry("mfaIdentifier", MFA_IDENTIFIER)))
                     .withHeaders(VALID_HEADERS);
 
     @BeforeEach
     void setUp() {
         when(configurationService.isMfaMethodManagementApiEnabled()).thenReturn(true);
-        handler = new MFAMethodsPutHandler(configurationService);
+        handler =
+                new MFAMethodsPutHandler(
+                        configurationService, mfaMethodsService, authenticationService);
     }
 
     @Test
-    void shouldReturn204WhenFeatureFlagEnabled() {
-        var result = handler.handleRequest(event, context);
+    void shouldReturn200WithUpdatedMethodWhenFeatureFlagEnabled() {
+        var phoneNumber = "123456789";
+        var updateRequest =
+                MfaMethodCreateOrUpdateRequest.from(
+                        PriorityIdentifier.DEFAULT,
+                        new SmsMfaDetail(MFAMethodType.SMS, phoneNumber));
+        var eventWithUpdateRequest =
+                event.withBody(
+                        format(
+                                """
+                        {
+                          "mfaMethod": {
+                            "priorityIdentifier": "DEFAULT",
+                            "method": {
+                                "mfaMethodType": "SMS",
+                                "phoneNumber": "%s"
+                            }
+                          }
+                        }
+                        """,
+                                phoneNumber));
+
+        when(userProfile.getEmail()).thenReturn(EMAIL);
+        when(authenticationService.getOptionalUserProfileFromPublicSubject(PUBLIC_SUBJECT_ID))
+                .thenReturn(Optional.of(userProfile));
+
+        var updatedMfaMethod =
+                MfaMethodData.smsMethodData(
+                        MFA_IDENTIFIER, PriorityIdentifier.DEFAULT, true, phoneNumber);
+        when(mfaMethodsService.updateMfaMethod(EMAIL, MFA_IDENTIFIER, updateRequest))
+                .thenReturn(Either.right(updatedMfaMethod));
+
+        var result = handler.handleRequest(eventWithUpdateRequest, context);
+
         assertEquals(200, result.getStatusCode());
+        var expectedResponse =
+                format(
+                        """
+                {
+                  "mfaIdentifier": "%s",
+                  "priorityIdentifier": "DEFAULT",
+                  "methodVerified": true,
+                  "method": {
+                    "mfaMethodType": "SMS",
+                    "phoneNumber": "%s"
+                  }
+                }
+                """,
+                        MFA_IDENTIFIER, phoneNumber);
+        var expectedResponseParsedToString =
+                JsonParser.parseString(expectedResponse).getAsJsonObject().toString();
+        assertEquals(expectedResponseParsedToString, result.getBody());
+    }
+
+    private static Stream<Arguments> updateFailureReasonsToExpectedResponses() {
+        return Stream.of(
+                Arguments.of(
path: account-management-api/src/test/java/uk/gov/di/accountmanagement/lambda/MFAMethodsPutHandlerTest.java
user: login: andrew-moores
body: Would static imports make this a bit easier to read?
created_at: 2025-03-27T12:15:43Z
author_association: CONTRIBUTOR
path: integration-tests/src/test/java/uk/gov/di/authentication/services/DynamoServiceIntegrationTest.java
user: login: BeckaL
body: Yeah definitely think we should do a naming clarification, will do in a refactor PR. Liked your idea of calling the database object "Item", not sure what that means for the domain object
created_at: 2025-03-27T13:19:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -53,8 +76,56 @@ public APIGatewayProxyResponseEvent updateMFAMethodsHandler(
                     configurationService.getEnvironment());
             return generateApiGatewayProxyErrorResponse(400, ErrorResponse.ERROR_1063);
         }
+        var publicSubjectId =
+                input.getPathParameters()
+                        .get("publicSubjectId"); // Error handling for when this empty to come in
+        // subsequent commit
+        var mfaIdentifier =
+                input.getPathParameters()
+                        .get("mfaIdentifier"); // Error handling for when this empty to come in
+        // subsequent commit
+
+        var userProfile =
+                authenticationService
+                        .getOptionalUserProfileFromPublicSubject(publicSubjectId)
+                        .get(); // Error handling for this to come in subsequent commit
+
+        try {
+            var mfaMethodUpdateRequest =
+                    objectMapper.readValue(
+                            input.getBody(), MfaMethodCreateOrUpdateRequest.class, true);
+            var result =
+                    mfaMethodsService.updateMfaMethod(
+                            userProfile.getEmail(), mfaIdentifier, mfaMethodUpdateRequest);
+
+            if (result.isLeft()) {
+                var failureReason = result.getLeft();
+                return handleUpdateMfaFailureReason(failureReason);
+            }
+
+            var successfulUpdate = result.get();
+
+            return generateApiGatewayProxyResponse(200, successfulUpdate, true);
+        } catch (Json.JsonException e) {
+            return generateApiGatewayProxyErrorResponse(400, ErrorResponse.ERROR_1001);
+        }
+    }
 
-        return generateApiGatewayProxyResponse(200, "{}");
+    private static APIGatewayProxyResponseEvent handleUpdateMfaFailureReason(
path: account-management-api/src/main/java/uk/gov/di/accountmanagement/lambda/MFAMethodsPutHandler.java
user: login: BeckaL
body: Not sure here - the API spec reflects all these statuses, think it's not worth putting the individual cases in but can discuss and do follow up
created_at: 2025-03-27T13:20:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2,43 +2,188 @@
 
 import com.amazonaws.services.lambda.runtime.Context;
 import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
+import com.google.gson.JsonParser;
+import io.vavr.control.Either;
 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.MethodSource;
+import uk.gov.di.authentication.shared.entity.ErrorResponse;
+import uk.gov.di.authentication.shared.entity.PriorityIdentifier;
+import uk.gov.di.authentication.shared.entity.UserProfile;
+import uk.gov.di.authentication.shared.entity.mfa.MFAMethodType;
+import uk.gov.di.authentication.shared.entity.mfa.MfaMethodCreateOrUpdateRequest;
+import uk.gov.di.authentication.shared.entity.mfa.MfaMethodData;
+import uk.gov.di.authentication.shared.entity.mfa.SmsMfaDetail;
+import uk.gov.di.authentication.shared.services.AuthenticationService;
 import uk.gov.di.authentication.shared.services.ConfigurationService;
+import uk.gov.di.authentication.shared.services.mfa.MfaMethodsService;
+import uk.gov.di.authentication.shared.services.mfa.MfaUpdateFailureReason;
 
 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.junit.jupiter.api.Assertions.assertEquals;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 import static uk.gov.di.accountmanagement.helpers.CommonTestVariables.VALID_HEADERS;
+import static uk.gov.di.authentication.sharedtest.matchers.APIGatewayProxyResponseEventMatcher.hasJsonBody;
+import static uk.gov.di.authentication.sharedtest.matchers.APIGatewayProxyResponseEventMatcher.hasStatus;
 
 class MFAMethodsPutHandlerTest {
 
     private static final ConfigurationService configurationService =
             mock(ConfigurationService.class);
+    private static final MfaMethodsService mfaMethodsService = mock(MfaMethodsService.class);
+    private static final AuthenticationService authenticationService =
+            mock(AuthenticationService.class);
+    private static final UserProfile userProfile = mock(UserProfile.class);
     private static final Context context = mock(Context.class);
+    private static final String MFA_IDENTIFIER = "some-mfa-identifier";
+    private static final String PUBLIC_SUBJECT_ID = "some-subject-id";
+    private static final String EMAIL = "joe.bloggs@digital.cabinet-office.gov.uk";
 
     private MFAMethodsPutHandler handler;
 
     private final APIGatewayProxyRequestEvent event =
             new APIGatewayProxyRequestEvent()
                     .withPathParameters(
                             Map.ofEntries(
-                                    Map.entry("publicSubjectId", "some-subject-id"),
-                                    Map.entry("mfaIdentifier", "some-mfa-identifier")))
+                                    Map.entry("publicSubjectId", PUBLIC_SUBJECT_ID),
+                                    Map.entry("mfaIdentifier", MFA_IDENTIFIER)))
                     .withHeaders(VALID_HEADERS);
 
     @BeforeEach
     void setUp() {
         when(configurationService.isMfaMethodManagementApiEnabled()).thenReturn(true);
-        handler = new MFAMethodsPutHandler(configurationService);
+        handler =
+                new MFAMethodsPutHandler(
+                        configurationService, mfaMethodsService, authenticationService);
     }
 
     @Test
-    void shouldReturn204WhenFeatureFlagEnabled() {
-        var result = handler.handleRequest(event, context);
+    void shouldReturn200WithUpdatedMethodWhenFeatureFlagEnabled() {
+        var phoneNumber = "123456789";
+        var updateRequest =
+                MfaMethodCreateOrUpdateRequest.from(
+                        PriorityIdentifier.DEFAULT,
+                        new SmsMfaDetail(MFAMethodType.SMS, phoneNumber));
+        var eventWithUpdateRequest =
+                event.withBody(
+                        format(
+                                """
+                        {
+                          "mfaMethod": {
+                            "priorityIdentifier": "DEFAULT",
+                            "method": {
+                                "mfaMethodType": "SMS",
+                                "phoneNumber": "%s"
+                            }
+                          }
+                        }
+                        """,
+                                phoneNumber));
+
+        when(userProfile.getEmail()).thenReturn(EMAIL);
+        when(authenticationService.getOptionalUserProfileFromPublicSubject(PUBLIC_SUBJECT_ID))
+                .thenReturn(Optional.of(userProfile));
+
+        var updatedMfaMethod =
+                MfaMethodData.smsMethodData(
+                        MFA_IDENTIFIER, PriorityIdentifier.DEFAULT, true, phoneNumber);
+        when(mfaMethodsService.updateMfaMethod(EMAIL, MFA_IDENTIFIER, updateRequest))
+                .thenReturn(Either.right(updatedMfaMethod));
+
+        var result = handler.handleRequest(eventWithUpdateRequest, context);
+
         assertEquals(200, result.getStatusCode());
+        var expectedResponse =
+                format(
+                        """
+                {
+                  "mfaIdentifier": "%s",
+                  "priorityIdentifier": "DEFAULT",
+                  "methodVerified": true,
+                  "method": {
+                    "mfaMethodType": "SMS",
+                    "phoneNumber": "%s"
+                  }
+                }
+                """,
+                        MFA_IDENTIFIER, phoneNumber);
+        var expectedResponseParsedToString =
+                JsonParser.parseString(expectedResponse).getAsJsonObject().toString();
+        assertEquals(expectedResponseParsedToString, result.getBody());
+    }
+
+    private static Stream<Arguments> updateFailureReasonsToExpectedResponses() {
+        return Stream.of(
+                Arguments.of(
path: account-management-api/src/test/java/uk/gov/di/accountmanagement/lambda/MFAMethodsPutHandlerTest.java
user: login: BeckaL
body: follow up PR will do 👍 
created_at: 2025-03-27T13:20:26Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-03-27T11:46:56Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=govuk-one-login_authentication-api&pullRequest=6198) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=govuk-one-login_authentication-api&pullRequest=6198&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=govuk-one-login_authentication-api&pullRequest=6198&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=govuk-one-login_authentication-api&pullRequest=6198&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [95.9% Coverage on New Code](https://sonarcloud.io/component_measures?id=govuk-one-login_authentication-api&pullRequest=6198&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [1.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=govuk-one-login_authentication-api&pullRequest=6198&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=govuk-one-login_authentication-api&pullRequest=6198)


author_association: NONE