From 79254360b33d84ee9bbe5b72bd08fdedca9cfb39 Mon Sep 17 00:00:00 2001 From: Guillaume Bernos Date: Tue, 7 Oct 2025 10:56:42 +0200 Subject: [PATCH] feat: MFA Config Model (MfaConfiguration, MfaFactor) --- .../compose/configuration/MfaConfiguration.kt | 42 +++++ .../auth/compose/configuration/MfaFactor.kt | 33 ++++ .../configuration/MfaConfigurationTest.kt | 162 ++++++++++++++++++ 3 files changed, 237 insertions(+) create mode 100644 auth/src/main/java/com/firebase/ui/auth/compose/configuration/MfaConfiguration.kt create mode 100644 auth/src/main/java/com/firebase/ui/auth/compose/configuration/MfaFactor.kt create mode 100644 auth/src/test/java/com/firebase/ui/auth/compose/configuration/MfaConfigurationTest.kt diff --git a/auth/src/main/java/com/firebase/ui/auth/compose/configuration/MfaConfiguration.kt b/auth/src/main/java/com/firebase/ui/auth/compose/configuration/MfaConfiguration.kt new file mode 100644 index 000000000..393ef0f26 --- /dev/null +++ b/auth/src/main/java/com/firebase/ui/auth/compose/configuration/MfaConfiguration.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2025 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.firebase.ui.auth.compose.configuration + +/** + * Configuration class for Multi-Factor Authentication (MFA) enrollment and verification behavior. + * + * This class controls which MFA factors are available to users, whether enrollment is mandatory, + * and whether recovery codes are generated. + * + * @property allowedFactors List of MFA factors that users are permitted to enroll in. + * Defaults to [MfaFactor.Sms, MfaFactor.Totp]. + * @property requireEnrollment Whether MFA enrollment is mandatory for all users. + * When true, users must enroll in at least one MFA factor. + * Defaults to false. + * @property enableRecoveryCodes Whether to generate and provide recovery codes to users + * after successful MFA enrollment. These codes can be used + * as a backup authentication method. Defaults to true. + */ +class MfaConfiguration( + val allowedFactors: List = listOf(MfaFactor.Sms, MfaFactor.Totp), + val requireEnrollment: Boolean = false, + val enableRecoveryCodes: Boolean = true +) { + init { + require(allowedFactors.isNotEmpty()) { + "At least one MFA factor must be allowed" + } + } +} diff --git a/auth/src/main/java/com/firebase/ui/auth/compose/configuration/MfaFactor.kt b/auth/src/main/java/com/firebase/ui/auth/compose/configuration/MfaFactor.kt new file mode 100644 index 000000000..78926ace4 --- /dev/null +++ b/auth/src/main/java/com/firebase/ui/auth/compose/configuration/MfaFactor.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2025 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.firebase.ui.auth.compose.configuration + +/** + * Represents the different Multi-Factor Authentication (MFA) factors that can be used + * for enrollment and verification. + */ +enum class MfaFactor { + /** + * SMS-based authentication factor. + * Users receive a verification code via text message to their registered phone number. + */ + Sms, + + /** + * Time-based One-Time Password (TOTP) authentication factor. + * Users generate verification codes using an authenticator app. + */ + Totp +} diff --git a/auth/src/test/java/com/firebase/ui/auth/compose/configuration/MfaConfigurationTest.kt b/auth/src/test/java/com/firebase/ui/auth/compose/configuration/MfaConfigurationTest.kt new file mode 100644 index 000000000..aa80d4533 --- /dev/null +++ b/auth/src/test/java/com/firebase/ui/auth/compose/configuration/MfaConfigurationTest.kt @@ -0,0 +1,162 @@ +/* + * Copyright 2025 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.firebase.ui.auth.compose.configuration + +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +/** + * Unit tests for [MfaConfiguration] covering default values, custom configurations, + * and validation rules. + * + * @suppress Internal test class + */ +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE) +class MfaConfigurationTest { + + // ============================================================================================= + // Default Configuration Tests + // ============================================================================================= + + @Test + fun `MfaConfiguration with defaults uses correct values`() { + val config = MfaConfiguration() + + assertThat(config.allowedFactors).containsExactly(MfaFactor.Sms, MfaFactor.Totp) + assertThat(config.requireEnrollment).isFalse() + assertThat(config.enableRecoveryCodes).isTrue() + } + + @Test + fun `MfaConfiguration default allowedFactors includes both Sms and Totp`() { + val config = MfaConfiguration() + + assertThat(config.allowedFactors).hasSize(2) + assertThat(config.allowedFactors).contains(MfaFactor.Sms) + assertThat(config.allowedFactors).contains(MfaFactor.Totp) + } + + // ============================================================================================= + // Custom Configuration Tests + // ============================================================================================= + + @Test + fun `MfaConfiguration with custom allowedFactors only Sms`() { + val config = MfaConfiguration( + allowedFactors = listOf(MfaFactor.Sms) + ) + + assertThat(config.allowedFactors).containsExactly(MfaFactor.Sms) + assertThat(config.allowedFactors).hasSize(1) + } + + @Test + fun `MfaConfiguration with custom allowedFactors only Totp`() { + val config = MfaConfiguration( + allowedFactors = listOf(MfaFactor.Totp) + ) + + assertThat(config.allowedFactors).containsExactly(MfaFactor.Totp) + assertThat(config.allowedFactors).hasSize(1) + } + + @Test + fun `MfaConfiguration with requireEnrollment enabled`() { + val config = MfaConfiguration( + requireEnrollment = true + ) + + assertThat(config.requireEnrollment).isTrue() + } + + @Test + fun `MfaConfiguration with enableRecoveryCodes disabled`() { + val config = MfaConfiguration( + enableRecoveryCodes = false + ) + + assertThat(config.enableRecoveryCodes).isFalse() + } + + @Test + fun `MfaConfiguration with all custom values`() { + val config = MfaConfiguration( + allowedFactors = listOf(MfaFactor.Sms), + requireEnrollment = true, + enableRecoveryCodes = false + ) + + assertThat(config.allowedFactors).containsExactly(MfaFactor.Sms) + assertThat(config.requireEnrollment).isTrue() + assertThat(config.enableRecoveryCodes).isFalse() + } + + // ============================================================================================= + // Validation Tests + // ============================================================================================= + + @Test + fun `MfaConfiguration throws when allowedFactors is empty`() { + try { + MfaConfiguration( + allowedFactors = emptyList() + ) + } catch (e: Exception) { + assertThat(e).isInstanceOf(IllegalArgumentException::class.java) + assertThat(e.message).isEqualTo("At least one MFA factor must be allowed") + } + } + + @Test + fun `MfaConfiguration allows both factors in any order`() { + val config1 = MfaConfiguration( + allowedFactors = listOf(MfaFactor.Sms, MfaFactor.Totp) + ) + val config2 = MfaConfiguration( + allowedFactors = listOf(MfaFactor.Totp, MfaFactor.Sms) + ) + + assertThat(config1.allowedFactors).hasSize(2) + assertThat(config2.allowedFactors).hasSize(2) + assertThat(config1.allowedFactors).containsExactly(MfaFactor.Sms, MfaFactor.Totp) + assertThat(config2.allowedFactors).containsExactly(MfaFactor.Totp, MfaFactor.Sms) + } + + // ============================================================================================= + // MfaFactor Enum Tests + // ============================================================================================= + + @Test + fun `MfaFactor enum has exactly two values`() { + val factors = MfaFactor.entries + + assertThat(factors).hasSize(2) + assertThat(factors).containsExactly(MfaFactor.Sms, MfaFactor.Totp) + } + + @Test + fun `MfaFactor Sms has correct name`() { + assertThat(MfaFactor.Sms.name).isEqualTo("Sms") + } + + @Test + fun `MfaFactor Totp has correct name`() { + assertThat(MfaFactor.Totp.name).isEqualTo("Totp") + } +}