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 c5838da354..e30e9a3282 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 @@ -9,6 +9,8 @@ import uk.gov.di.authentication.shared.entity.ClientConsent; import uk.gov.di.authentication.shared.entity.MFAMethod; import uk.gov.di.authentication.shared.entity.MFAMethodType; +import uk.gov.di.authentication.shared.entity.MFAMethodV2; +import uk.gov.di.authentication.shared.entity.PriorityIdentifier; import uk.gov.di.authentication.shared.entity.UserCredentials; import uk.gov.di.authentication.shared.entity.UserProfile; import uk.gov.di.authentication.shared.entity.ValidScopes; @@ -171,6 +173,110 @@ void shouldAddAuthAppMFAMethod() { assertThat(mfaMethod.getCredentialValue(), equalTo(TEST_MFA_APP_CREDENTIAL)); } + @Test + void shouldAddAuthAppMFAMethodV2() { + setUpDynamo(); + dynamoService.addMFAMethodV2( + TEST_EMAIL, + MFAMethodType.AUTH_APP, + true, + true, + PriorityIdentifier.PRIMARY, + 1, + "test"); + UserCredentials updatedUserCredentials = + dynamoService.getUserCredentialsFromEmail(TEST_EMAIL); + + assertThat(updatedUserCredentials.getMfaMethodsV2().size(), equalTo(1)); + MFAMethodV2 mfaMethod = updatedUserCredentials.getMfaMethodsV2().get(0); + assertThat(mfaMethod.getMfaMethodType(), equalTo(MFAMethodType.AUTH_APP.getValue())); + assertThat(mfaMethod.isMethodVerified(), equalTo(true)); + assertThat(mfaMethod.isEnabled(), equalTo(true)); + assertThat(mfaMethod.getPriorityIdentifier(), equalTo(PriorityIdentifier.PRIMARY)); + assertThat(mfaMethod.getMfaIdentifier(), equalTo(1)); + assertThat(mfaMethod.getEndPoint(), equalTo("test")); + } + + @Test + void shouldUpdateAuthAppMFAMethodV2() { + setUpDynamo(); + dynamoService.addMFAMethodV2( + TEST_EMAIL, + MFAMethodType.AUTH_APP, + true, + true, + PriorityIdentifier.PRIMARY, + 1, + "test"); + + dynamoService.updateMFAmethodV2( + TEST_EMAIL, + MFAMethodType.AUTH_APP, + true, + false, + PriorityIdentifier.SECONDARY, + 1, + "test"); + + UserCredentials updatedUserCredentials = + dynamoService.getUserCredentialsFromEmail(TEST_EMAIL); + + assertThat(updatedUserCredentials.getMfaMethodsV2().size(), equalTo(1)); + MFAMethodV2 mfaMethod = updatedUserCredentials.getMfaMethodsV2().get(0); + assertThat(mfaMethod.getMfaMethodType(), equalTo(MFAMethodType.AUTH_APP.getValue())); + assertThat(mfaMethod.isMethodVerified(), equalTo(true)); + assertThat(mfaMethod.isEnabled(), equalTo(false)); + assertThat(mfaMethod.getPriorityIdentifier(), equalTo(PriorityIdentifier.SECONDARY)); + assertThat(mfaMethod.getMfaIdentifier(), equalTo(1)); + assertThat(mfaMethod.getEndPoint(), equalTo("test")); + } + + @Test + void shouldDeleteAuthAppMFAMethodV2() { + + setUpDynamo(); + dynamoService.addMFAMethodV2( + TEST_EMAIL, + MFAMethodType.AUTH_APP, + true, + true, + PriorityIdentifier.PRIMARY, + 1, + "test"); + + List users = dynamoService.readMFAMethodsV2(TEST_EMAIL); + + assertThat(users.size(), equalTo(1)); + + dynamoService.deleteMFAmethodV2(TEST_EMAIL, 1); + + users = dynamoService.readMFAMethodsV2(TEST_EMAIL); + assertThat(users.size(), equalTo(0)); + } + + @Test + void shouldReadAuthAppMFAMethodv2() { + setUpDynamo(); + dynamoService.addMFAMethodV2( + TEST_EMAIL, + MFAMethodType.AUTH_APP, + true, + true, + PriorityIdentifier.PRIMARY, + 1, + "test"); + dynamoService.addMFAMethodV2( + TEST_EMAIL, + MFAMethodType.AUTH_APP, + true, + false, + PriorityIdentifier.SECONDARY, + 3, + "google"); + List users = dynamoService.readMFAMethodsV2(TEST_EMAIL); + assertThat(users.size(), equalTo(2)); + } + @Test void shouldSetAuthAppMFAMethodNotEnabledAndSetPhoneNumberAndAccountVerifiedWhenMfaMethodExists() { diff --git a/shared/src/main/java/uk/gov/di/authentication/shared/entity/MFAMethodV2.java b/shared/src/main/java/uk/gov/di/authentication/shared/entity/MFAMethodV2.java new file mode 100644 index 0000000000..3e6c99620e --- /dev/null +++ b/shared/src/main/java/uk/gov/di/authentication/shared/entity/MFAMethodV2.java @@ -0,0 +1,162 @@ +package uk.gov.di.authentication.shared.entity; + +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbAttribute; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbConvertedBy; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import uk.gov.di.authentication.shared.dynamodb.BooleanToIntAttributeConverter; + +import java.util.Map; +import java.util.Objects; + +@DynamoDbBean +public class MFAMethodV2 { + + public static final String ATTRIBUTE_MFA_IDENTIFIER = "MfaIdentifier"; + public static final String ATTRIBUTE_PRIORITY_IDENTIFIER = "PriorityIdentifier"; + public static final String ATTRIBUTE_MFA_METHOD_TYPE = "MfaMethodType"; + public static final String ATTRIBUTE_END_POINT = "EndPoint"; + public static final String ATTRIBUTE_METHOD_VERIFIED = "MethodVerified"; + public static final String ATTRIBUTE_ENABLED = "Enabled"; + public static final String ATTRIBUTE_UPDATED = "Updated"; + + private String mfaMethodType; + private boolean methodVerified; + private boolean enabled; + private String updated; + private int mfaIdentifier; + private PriorityIdentifier priorityIdentifier; + private String endPoint; + + public MFAMethodV2() {} + + public MFAMethodV2( + int mfaIdentifier, + PriorityIdentifier priorityIdentifier, + String mfaMethodType, + String endPoint, + boolean methodVerified, + boolean enabled, + String updated) { + this.mfaMethodType = mfaMethodType; + this.methodVerified = methodVerified; + this.enabled = enabled; + this.updated = updated; + this.mfaIdentifier = mfaIdentifier; + this.priorityIdentifier = priorityIdentifier; + this.endPoint = endPoint; + } + + @DynamoDbAttribute(ATTRIBUTE_MFA_METHOD_TYPE) + public String getMfaMethodType() { + return mfaMethodType; + } + + public void setMfaMethodType(String mfaMethodType) { + this.mfaMethodType = mfaMethodType; + } + + @DynamoDbConvertedBy(BooleanToIntAttributeConverter.class) + @DynamoDbAttribute(ATTRIBUTE_METHOD_VERIFIED) + public boolean isMethodVerified() { + return methodVerified; + } + + public void setMethodVerified(boolean methodVerified) { + this.methodVerified = methodVerified; + } + + @DynamoDbConvertedBy(BooleanToIntAttributeConverter.class) + @DynamoDbAttribute(ATTRIBUTE_ENABLED) + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + @DynamoDbAttribute(ATTRIBUTE_UPDATED) + public String getUpdated() { + return updated; + } + + public void setUpdated(String updated) { + this.updated = updated; + } + + @DynamoDbAttribute(ATTRIBUTE_MFA_IDENTIFIER) + public int getMfaIdentifier() { + return mfaIdentifier; + } + + public void setMfaIdentifier(int mfaIdentifier) { + this.mfaIdentifier = mfaIdentifier; + } + + @DynamoDbAttribute(ATTRIBUTE_PRIORITY_IDENTIFIER) + public PriorityIdentifier getPriorityIdentifier() { + return priorityIdentifier; + } + + public void setPriorityIdentifier(PriorityIdentifier priorityIdentifier) { + this.priorityIdentifier = priorityIdentifier; + } + + @DynamoDbAttribute(ATTRIBUTE_END_POINT) + public String getEndPoint() { + return endPoint; + } + + public void setEndPoint(String endPoint) { + this.endPoint = endPoint; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MFAMethodV2 that = (MFAMethodV2) o; + return Objects.equals(mfaMethodType, that.mfaMethodType) + && Objects.equals(methodVerified, that.methodVerified) + && Objects.equals(enabled, that.enabled) + && Objects.equals(updated, that.updated) + && Objects.equals(mfaIdentifier, that.mfaIdentifier) + && Objects.equals(priorityIdentifier, that.priorityIdentifier) + && Objects.equals(endPoint, that.endPoint); + } + + @Override + public int hashCode() { + return Objects.hash( + mfaIdentifier, + priorityIdentifier, + mfaMethodType, + endPoint, + methodVerified, + enabled, + updated); + } + + AttributeValue toAttributeValue() { + return AttributeValue.fromM( + Map.ofEntries( + Map.entry( + ATTRIBUTE_MFA_METHOD_TYPE, + AttributeValue.fromS(getMfaMethodType())), + Map.entry( + ATTRIBUTE_METHOD_VERIFIED, + AttributeValue.fromN(isMethodVerified() ? "1" : "0")), + Map.entry(ATTRIBUTE_ENABLED, AttributeValue.fromN(isEnabled() ? "1" : "0")), + Map.entry(ATTRIBUTE_UPDATED, AttributeValue.fromS(getUpdated())), + Map.entry( + ATTRIBUTE_MFA_IDENTIFIER, + AttributeValue.builder() + .n(String.valueOf(getMfaIdentifier())) + .build()), + Map.entry( + ATTRIBUTE_PRIORITY_IDENTIFIER, + AttributeValue.fromS(getPriorityIdentifier().getValue())), + Map.entry(ATTRIBUTE_END_POINT, AttributeValue.fromS(getEndPoint())))); + } +} diff --git a/shared/src/main/java/uk/gov/di/authentication/shared/entity/PriorityIdentifier.java b/shared/src/main/java/uk/gov/di/authentication/shared/entity/PriorityIdentifier.java new file mode 100644 index 0000000000..8e0785007b --- /dev/null +++ b/shared/src/main/java/uk/gov/di/authentication/shared/entity/PriorityIdentifier.java @@ -0,0 +1,16 @@ +package uk.gov.di.authentication.shared.entity; + +public enum PriorityIdentifier { + PRIMARY("PRIMARY"), + SECONDARY("SECONDARY"); + + private String value; + + PriorityIdentifier(String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} 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 afb1420359..6f37d117d0 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 @@ -17,6 +17,7 @@ public class UserCredentials { public static final String ATTRIBUTE_UPDATED = "Updated"; public static final String ATTRIBUTE_MIGRATED_PASSWORD = "MigratedPassword"; public static final String ATTRIBUTE_MFA_METHODS = "MfaMethods"; + private static final String ATTRIBUTE_MFA_METHODS_V2 = "MfaMethodsV2"; public static final String ATTRIBUTE_TEST_USER = "testUser"; private String email; @@ -26,6 +27,7 @@ public class UserCredentials { private String updated; private String migratedPassword; private List mfaMethods; + private List mfaMethodsV2; private int testUser; public UserCredentials() {} @@ -141,6 +143,38 @@ public UserCredentials setMfaMethod(MFAMethod mfaMethod) { return this; } + @DynamoDbAttribute(ATTRIBUTE_MFA_METHODS_V2) + public List getMfaMethodsV2() { + return mfaMethodsV2; + } + + public UserCredentials withMfaMethodsV2(List mfaMethodsV2) { + this.mfaMethodsV2 = mfaMethodsV2; + return this; + } + + public void setMfaMethodsV2(List mfaMethodsV2) { + this.mfaMethodsV2 = mfaMethodsV2; + } + + public UserCredentials addMfaMethodV2(MFAMethodV2 mfaMethodV2) { + if (this.mfaMethodsV2 == null) { + this.mfaMethodsV2 = List.of(mfaMethodV2); + } else { + this.mfaMethodsV2.add(mfaMethodV2); + } + return this; + } + + public UserCredentials deleteMfaMethodV2(int mfaIdentifier) { + if (this.mfaMethodsV2 == null) { + return this; + } else { + this.mfaMethodsV2.removeIf(t -> t.getMfaIdentifier() == (mfaIdentifier)); + } + return this; + } + public UserCredentials removeAuthAppByCredentialIfPresent(String authAppCredential) { if (this.mfaMethods == null) { return this; 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 8a63a85f21..c84f270599 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 @@ -19,6 +19,8 @@ import uk.gov.di.authentication.shared.entity.ClientConsent; import uk.gov.di.authentication.shared.entity.MFAMethod; import uk.gov.di.authentication.shared.entity.MFAMethodType; +import uk.gov.di.authentication.shared.entity.MFAMethodV2; +import uk.gov.di.authentication.shared.entity.PriorityIdentifier; import uk.gov.di.authentication.shared.entity.TermsAndConditions; import uk.gov.di.authentication.shared.entity.User; import uk.gov.di.authentication.shared.entity.UserCredentials; @@ -32,6 +34,7 @@ import java.time.LocalDateTime; import java.time.ZoneId; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; @@ -472,6 +475,83 @@ public void updateMFAMethod( .setMfaMethod(mfaMethod)); } + public void addMFAMethodV2( + String email, + MFAMethodType mfaMethodType, + boolean methodVerified, + boolean enabled, + PriorityIdentifier priorityIdentifier, + int mfaIdentifier, + String endpoint) { + String dateTime = NowHelper.toTimestampString(NowHelper.now()); + MFAMethodV2 mfaMethodV2 = + new MFAMethodV2( + mfaIdentifier, + priorityIdentifier, + MFAMethodType.AUTH_APP.getValue(), + endpoint, + methodVerified, + enabled, + dateTime); + dynamoUserCredentialsTable.updateItem( + dynamoUserCredentialsTable + .getItem( + Key.builder() + .partitionValue(email.toLowerCase(Locale.ROOT)) + .build()) + .addMfaMethodV2(mfaMethodV2)); + } + + public void updateMFAmethodV2( + String email, + MFAMethodType mfaMethodType, + boolean methodVerified, + boolean enabled, + PriorityIdentifier priorityIdentifier, + int mfaIdentifier, + String endpoint) { + String dateTime = NowHelper.toTimestampString(NowHelper.now()); + MFAMethodV2 updatedMfaMethodV2 = + new MFAMethodV2( + mfaIdentifier, + priorityIdentifier, + MFAMethodType.AUTH_APP.getValue(), + endpoint, + methodVerified, + enabled, + dateTime); + dynamoUserCredentialsTable.updateItem( + dynamoUserCredentialsTable + .getItem( + Key.builder() + .partitionValue(email.toLowerCase(Locale.ROOT)) + .build()) + .withMfaMethodsV2(List.of(updatedMfaMethodV2))); + } + + public void deleteMFAmethodV2(String email, int mfaIdentifier) { + + dynamoUserCredentialsTable.updateItem( + dynamoUserCredentialsTable + .getItem( + Key.builder() + .partitionValue(email.toLowerCase(Locale.ROOT)) + .build()) + .deleteMfaMethodV2(mfaIdentifier)); + } + + public List readMFAMethodsV2(String email) { + var userItem = + dynamoUserCredentialsTable.getItem( + Key.builder().partitionValue(email.toLowerCase(Locale.ROOT)).build()); + + if (userItem != null && userItem.getMfaMethodsV2() != null) { + return userItem.getMfaMethodsV2(); + } else { + return Collections.emptyList(); + } + } + @Override public void setAuthAppAndAccountVerified(String email, String credentialValue) { var dateTime = NowHelper.toTimestampString(NowHelper.now());