From 7e6d4050a1962c5e8e00846b0b878bd35463b18f Mon Sep 17 00:00:00 2001 From: demolaf Date: Fri, 5 Jun 2026 13:27:50 +0100 Subject: [PATCH 1/4] feat(auth): handle GIdP password policy violations with specific error messages --- .../com/firebase/ui/auth/AuthException.kt | 84 +++++++++++++--- .../auth/ui/screens/email/EmailAuthScreen.kt | 4 +- .../com/firebase/ui/auth/AuthExceptionTest.kt | 98 +++++++++++++++++++ 3 files changed, 173 insertions(+), 13 deletions(-) diff --git a/auth/src/main/java/com/firebase/ui/auth/AuthException.kt b/auth/src/main/java/com/firebase/ui/auth/AuthException.kt index ae9d96e53..4165fafa4 100644 --- a/auth/src/main/java/com/firebase/ui/auth/AuthException.kt +++ b/auth/src/main/java/com/firebase/ui/auth/AuthException.kt @@ -123,6 +123,27 @@ abstract class AuthException( val reason: String? = null ) : AuthException(message, cause) + /** + * The password violates one or more Google Identity Platform password policy requirements. + * + * This exception is thrown when GIdP password policy enforcement is enabled and the supplied + * password fails one or more configured constraints (e.g. missing uppercase, missing digit). + * [failingRequirements] contains the raw server-side constraint identifiers such as + * `MISSING_UPPERCASE_CHARACTER` or `MISSING_NON_ALPHANUMERIC_CHARACTER`. + * + * [message] is a newline-separated, human-readable description of each failing constraint, + * suitable for direct display in the UI. + * + * @property message Human-readable description of the failing constraints + * @property failingRequirements Raw server-side constraint identifiers + * @property cause The underlying [Throwable] that caused this exception + */ + class PasswordPolicyViolationException( + message: String, + val failingRequirements: List, + cause: Throwable? = null + ) : AuthException(message, cause) + /** * An account with the given email already exists. * @@ -354,7 +375,34 @@ abstract class AuthException( // If already an AuthException, return it directly is AuthException -> firebaseException - // Handle specific Firebase Auth exceptions first (before general FirebaseException) + // Handle specific Firebase Auth exceptions first (before general FirebaseException). + // FirebaseAuthWeakPasswordException extends FirebaseAuthInvalidCredentialsException, + // so it must be checked before the parent type. + is FirebaseAuthWeakPasswordException -> { + val sourceText = firebaseException.reason ?: firebaseException.message ?: "" + if (sourceText.contains("PASSWORD_DOES_NOT_MEET_REQUIREMENTS", ignoreCase = true)) { + val requirements = parsePasswordPolicyRequirements(sourceText) + val humanReadable = requirements + .joinToString("\n") { mapRequirementToString(it, stringProvider) } + PasswordPolicyViolationException( + message = humanReadable.ifEmpty { + stringProvider?.errorWeakPasswordGeneric.nonEmpty() + ?: "Password does not meet policy requirements" + }, + failingRequirements = requirements, + cause = firebaseException + ) + } else { + WeakPasswordException( + message = stringProvider?.errorWeakPasswordGeneric.nonEmpty() + ?: firebaseException.message + ?: "Password is too weak", + cause = firebaseException, + reason = firebaseException.reason + ) + } + } + is FirebaseAuthInvalidCredentialsException -> { InvalidCredentialsException( message = stringProvider?.errorInvalidCredentials.nonEmpty() @@ -389,16 +437,6 @@ abstract class AuthException( } } - is FirebaseAuthWeakPasswordException -> { - WeakPasswordException( - message = stringProvider?.errorWeakPasswordGeneric.nonEmpty() - ?: firebaseException.message - ?: "Password is too weak", - cause = firebaseException, - reason = firebaseException.reason - ) - } - is FirebaseAuthUserCollisionException -> { when (firebaseException.errorCode) { "ERROR_EMAIL_ALREADY_IN_USE" -> EmailAlreadyInUseException( @@ -500,5 +538,29 @@ abstract class AuthException( } private fun String?.nonEmpty(): String? = this?.ifEmpty { null } + + private fun parsePasswordPolicyRequirements(message: String): List { + val start = message.indexOf('[') + val end = message.indexOf(']') + if (start == -1 || end == -1 || end <= start) return emptyList() + return message.substring(start + 1, end) + .split(',') + .map { it.trim() } + .filter { it.isNotEmpty() } + } + + private fun mapRequirementToString(code: String, stringProvider: AuthUIStringProvider?): String { + return when (code.uppercase()) { + "MISSING_UPPERCASE_CHARACTER" -> + stringProvider?.passwordMissingUppercase ?: "Password must contain at least one uppercase letter" + "MISSING_LOWERCASE_CHARACTER" -> + stringProvider?.passwordMissingLowercase ?: "Password must contain at least one lowercase letter" + "MISSING_NUMERIC_CHARACTER" -> + stringProvider?.passwordMissingDigit ?: "Password must contain at least one number" + "MISSING_NON_ALPHANUMERIC_CHARACTER" -> + stringProvider?.passwordMissingSpecialCharacter ?: "Password must contain at least one special character" + else -> code + } + } } } diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/screens/email/EmailAuthScreen.kt b/auth/src/main/java/com/firebase/ui/auth/ui/screens/email/EmailAuthScreen.kt index 62972d18c..1f566f5cf 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/screens/email/EmailAuthScreen.kt +++ b/auth/src/main/java/com/firebase/ui/auth/ui/screens/email/EmailAuthScreen.kt @@ -306,7 +306,7 @@ fun EmailAuthScreen( password = passwordTextValue.value, ) } catch (e: Exception) { - + onError(AuthException.from(e, stringProvider)) } } }, @@ -318,7 +318,7 @@ fun EmailAuthScreen( actionCodeSettings = configuration.passwordResetActionCodeSettings, ) } catch (e: Exception) { - + onError(AuthException.from(e, stringProvider)) } } }, diff --git a/auth/src/test/java/com/firebase/ui/auth/AuthExceptionTest.kt b/auth/src/test/java/com/firebase/ui/auth/AuthExceptionTest.kt index caa382bb1..52174f2f0 100644 --- a/auth/src/test/java/com/firebase/ui/auth/AuthExceptionTest.kt +++ b/auth/src/test/java/com/firebase/ui/auth/AuthExceptionTest.kt @@ -19,6 +19,7 @@ import com.google.common.truth.Truth.assertThat import com.google.firebase.FirebaseException import com.google.firebase.auth.FirebaseAuthException import com.google.firebase.auth.FirebaseAuthInvalidUserException +import com.google.firebase.auth.FirebaseAuthWeakPasswordException import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito.mock @@ -113,6 +114,7 @@ class AuthExceptionTest { assertThat(AuthException.InvalidCredentialsException("Test")).isInstanceOf(AuthException::class.java) assertThat(AuthException.UserNotFoundException("Test")).isInstanceOf(AuthException::class.java) assertThat(AuthException.WeakPasswordException("Test")).isInstanceOf(AuthException::class.java) + assertThat(AuthException.PasswordPolicyViolationException("Test", emptyList())).isInstanceOf(AuthException::class.java) assertThat(AuthException.EmailAlreadyInUseException("Test")).isInstanceOf(AuthException::class.java) assertThat(AuthException.TooManyRequestsException("Test")).isInstanceOf(AuthException::class.java) assertThat(AuthException.MfaRequiredException("Test")).isInstanceOf(AuthException::class.java) @@ -183,4 +185,100 @@ class AuthExceptionTest { assertThat(result.message).isEqualTo("Firebase: user disabled") } + + // ============================================================================================= + // GIdP password policy + // ============================================================================================= + + @Test + fun `from() maps GIdP policy violation in reason to PasswordPolicyViolationException`() { + val firebaseException = FirebaseAuthWeakPasswordException( + "ERROR_WEAK_PASSWORD", + "weak", + "PASSWORD_DOES_NOT_MEET_REQUIREMENTS : [MISSING_UPPERCASE_CHARACTER, MISSING_NUMERIC_CHARACTER]" + ) + + val result = AuthException.from(firebaseException) + + assertThat(result).isInstanceOf(AuthException.PasswordPolicyViolationException::class.java) + val policyEx = result as AuthException.PasswordPolicyViolationException + assertThat(policyEx.failingRequirements).containsExactly( + "MISSING_UPPERCASE_CHARACTER", + "MISSING_NUMERIC_CHARACTER" + ).inOrder() + assertThat(policyEx.cause).isEqualTo(firebaseException) + } + + @Test + fun `from() maps GIdP policy violation in message when reason is null to PasswordPolicyViolationException`() { + val firebaseException = FirebaseAuthWeakPasswordException( + "ERROR_WEAK_PASSWORD", + "PASSWORD_DOES_NOT_MEET_REQUIREMENTS : [MISSING_LOWERCASE_CHARACTER, MISSING_NON_ALPHANUMERIC_CHARACTER]", + null + ) + + val result = AuthException.from(firebaseException) + + assertThat(result).isInstanceOf(AuthException.PasswordPolicyViolationException::class.java) + val policyEx = result as AuthException.PasswordPolicyViolationException + assertThat(policyEx.failingRequirements).containsExactly( + "MISSING_LOWERCASE_CHARACTER", + "MISSING_NON_ALPHANUMERIC_CHARACTER" + ).inOrder() + } + + @Test + fun `from() maps policy violation with string provider to human-readable message`() { + val firebaseException = FirebaseAuthWeakPasswordException( + "ERROR_WEAK_PASSWORD", + "weak", + "PASSWORD_DOES_NOT_MEET_REQUIREMENTS : [MISSING_UPPERCASE_CHARACTER, MISSING_NUMERIC_CHARACTER]" + ) + + val stringProvider = mock(AuthUIStringProvider::class.java) + whenever(stringProvider.passwordMissingUppercase).thenReturn("Needs uppercase") + whenever(stringProvider.passwordMissingDigit).thenReturn("Needs a number") + + val result = AuthException.from(firebaseException, stringProvider) + + assertThat(result).isInstanceOf(AuthException.PasswordPolicyViolationException::class.java) + assertThat(result.message).isEqualTo("Needs uppercase\nNeeds a number") + } + + @Test + fun `from() maps unknown requirement code to raw code string`() { + val firebaseException = FirebaseAuthWeakPasswordException( + "ERROR_WEAK_PASSWORD", + "weak", + "PASSWORD_DOES_NOT_MEET_REQUIREMENTS : [SOME_FUTURE_REQUIREMENT]" + ) + + val result = AuthException.from(firebaseException) + + assertThat(result).isInstanceOf(AuthException.PasswordPolicyViolationException::class.java) + val policyEx = result as AuthException.PasswordPolicyViolationException + assertThat(policyEx.failingRequirements).containsExactly("SOME_FUTURE_REQUIREMENT") + assertThat(policyEx.message).isEqualTo("SOME_FUTURE_REQUIREMENT") + } + + @Test + fun `from() maps plain weak password (no policy) to WeakPasswordException`() { + val firebaseException = FirebaseAuthWeakPasswordException( + "ERROR_WEAK_PASSWORD", + "The given password is invalid.", + "Password should be at least 6 characters" + ) + + val result = AuthException.from(firebaseException) + + assertThat(result).isInstanceOf(AuthException.WeakPasswordException::class.java) + } + + @Test + fun `PasswordPolicyViolationException stores failingRequirements correctly`() { + val requirements = listOf("MISSING_UPPERCASE_CHARACTER", "MISSING_NUMERIC_CHARACTER") + val exception = AuthException.PasswordPolicyViolationException("msg", requirements) + + assertThat(exception.failingRequirements).isEqualTo(requirements) + } } \ No newline at end of file From cfbcc8482310e15c14f6da76ffba784aaae29b60 Mon Sep 17 00:00:00 2001 From: demolaf Date: Mon, 8 Jun 2026 09:53:42 +0100 Subject: [PATCH 2/4] updates --- .../com/firebase/ui/auth/AuthException.kt | 41 ++++++++--- .../auth/ui/components/ErrorRecoveryDialog.kt | 7 ++ .../com/firebase/ui/auth/AuthExceptionTest.kt | 70 +++++++++++++------ 3 files changed, 86 insertions(+), 32 deletions(-) diff --git a/auth/src/main/java/com/firebase/ui/auth/AuthException.kt b/auth/src/main/java/com/firebase/ui/auth/AuthException.kt index 4165fafa4..fa45746ee 100644 --- a/auth/src/main/java/com/firebase/ui/auth/AuthException.kt +++ b/auth/src/main/java/com/firebase/ui/auth/AuthException.kt @@ -507,12 +507,26 @@ abstract class AuthException( } is FirebaseException -> { - NetworkException( - message = stringProvider?.errorNetworkGeneric.nonEmpty() - ?: firebaseException.message - ?: "Network error occurred", - cause = firebaseException - ) + val msg = firebaseException.message ?: "" + if (msg.contains("PASSWORD_DOES_NOT_MEET_REQUIREMENTS", ignoreCase = true)) { + val requirements = parsePasswordPolicyRequirements(msg) + val humanReadable = requirements + .joinToString("\n") { mapRequirementToString(it, stringProvider) } + PasswordPolicyViolationException( + message = humanReadable.ifEmpty { + stringProvider?.errorWeakPasswordGeneric.nonEmpty() + ?: "Password does not meet policy requirements" + }, + failingRequirements = requirements, + cause = firebaseException + ) + } else { + NetworkException( + message = stringProvider?.errorNetworkGeneric.nonEmpty() + ?: msg.ifEmpty { "Network error occurred" }, + cause = firebaseException + ) + } } else -> { @@ -539,16 +553,25 @@ abstract class AuthException( private fun String?.nonEmpty(): String? = this?.ifEmpty { null } + // Finds the [...] content that immediately follows PASSWORD_DOES_NOT_MEET_REQUIREMENTS + // in both FirebaseException and FirebaseAuthWeakPasswordException messages. + // GIdP returns human-readable requirement strings inside those brackets, e.g. + // "...PASSWORD_DOES_NOT_MEET_REQUIREMENTS:Missing password requirements: [Password must contain at least 10 characters]" private fun parsePasswordPolicyRequirements(message: String): List { - val start = message.indexOf('[') - val end = message.indexOf(']') - if (start == -1 || end == -1 || end <= start) return emptyList() + val policyIndex = message.indexOf("PASSWORD_DOES_NOT_MEET_REQUIREMENTS", ignoreCase = true) + if (policyIndex == -1) return emptyList() + val start = message.indexOf('[', policyIndex) + val end = message.indexOf(']', start) + if (start == -1 || end == -1) return emptyList() return message.substring(start + 1, end) .split(',') .map { it.trim() } .filter { it.isNotEmpty() } } + // GIdP already returns human-readable requirement strings, so this is a pass-through + // for those. For older SDK versions that may surface short error codes instead + // (e.g. MISSING_UPPERCASE_CHARACTER), map those to localised strings. private fun mapRequirementToString(code: String, stringProvider: AuthUIStringProvider?): String { return when (code.uppercase()) { "MISSING_UPPERCASE_CHARACTER" -> diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/components/ErrorRecoveryDialog.kt b/auth/src/main/java/com/firebase/ui/auth/ui/components/ErrorRecoveryDialog.kt index dff4daa60..230bb835a 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/components/ErrorRecoveryDialog.kt +++ b/auth/src/main/java/com/firebase/ui/auth/ui/components/ErrorRecoveryDialog.kt @@ -140,6 +140,11 @@ private fun getRecoveryMessage( } ?: baseMessage } + is AuthException.PasswordPolicyViolationException -> { + error.message?.takeIf { it.isNotBlank() } + ?: stringProvider.weakPasswordRecoveryMessage + } + is AuthException.EmailAlreadyInUseException -> { // Include email if available val baseMessage = stringProvider.emailAlreadyInUseRecoveryMessage @@ -201,6 +206,7 @@ private fun getRecoveryActionText( is AuthException.NetworkException, is AuthException.InvalidCredentialsException, is AuthException.WeakPasswordException, + is AuthException.PasswordPolicyViolationException, is AuthException.TooManyRequestsException, is AuthException.PhoneVerificationCooldownException -> stringProvider.retryAction is AuthException.UnknownException -> stringProvider.retryAction @@ -221,6 +227,7 @@ private fun isRecoverable(error: AuthException): Boolean { is AuthException.InvalidCredentialsException -> true is AuthException.UserNotFoundException -> true is AuthException.WeakPasswordException -> true + is AuthException.PasswordPolicyViolationException -> true is AuthException.EmailAlreadyInUseException -> true is AuthException.TooManyRequestsException -> false // User must wait is AuthException.PhoneVerificationCooldownException -> false // User must wait for cooldown diff --git a/auth/src/test/java/com/firebase/ui/auth/AuthExceptionTest.kt b/auth/src/test/java/com/firebase/ui/auth/AuthExceptionTest.kt index 52174f2f0..7ab4b4e86 100644 --- a/auth/src/test/java/com/firebase/ui/auth/AuthExceptionTest.kt +++ b/auth/src/test/java/com/firebase/ui/auth/AuthExceptionTest.kt @@ -191,30 +191,48 @@ class AuthExceptionTest { // ============================================================================================= @Test - fun `from() maps GIdP policy violation in reason to PasswordPolicyViolationException`() { - val firebaseException = FirebaseAuthWeakPasswordException( - "ERROR_WEAK_PASSWORD", - "weak", - "PASSWORD_DOES_NOT_MEET_REQUIREMENTS : [MISSING_UPPERCASE_CHARACTER, MISSING_NUMERIC_CHARACTER]" + fun `from() maps GIdP policy violation FirebaseException to PasswordPolicyViolationException`() { + val msg = "An internal error has occurred. [ PASSWORD_DOES_NOT_MEET_REQUIREMENTS:" + + "Missing password requirements: [Password must contain at least 10 characters] ]" + val firebaseException = object : com.google.firebase.FirebaseException(msg) {} + + val result = AuthException.from(firebaseException) + + assertThat(result).isInstanceOf(AuthException.PasswordPolicyViolationException::class.java) + val policyEx = result as AuthException.PasswordPolicyViolationException + assertThat(policyEx.failingRequirements).containsExactly( + "Password must contain at least 10 characters" ) + assertThat(policyEx.message).isEqualTo("Password must contain at least 10 characters") + assertThat(policyEx.cause).isEqualTo(firebaseException) + } + + @Test + fun `from() maps GIdP policy violation with multiple requirements`() { + val msg = "An internal error has occurred. [ PASSWORD_DOES_NOT_MEET_REQUIREMENTS:" + + "Missing password requirements: [Password must contain at least 10 characters, " + + "Password must contain at least one uppercase letter] ]" + val firebaseException = object : com.google.firebase.FirebaseException(msg) {} val result = AuthException.from(firebaseException) assertThat(result).isInstanceOf(AuthException.PasswordPolicyViolationException::class.java) val policyEx = result as AuthException.PasswordPolicyViolationException assertThat(policyEx.failingRequirements).containsExactly( - "MISSING_UPPERCASE_CHARACTER", - "MISSING_NUMERIC_CHARACTER" + "Password must contain at least 10 characters", + "Password must contain at least one uppercase letter" ).inOrder() - assertThat(policyEx.cause).isEqualTo(firebaseException) + assertThat(policyEx.message).isEqualTo( + "Password must contain at least 10 characters\nPassword must contain at least one uppercase letter" + ) } @Test - fun `from() maps GIdP policy violation in message when reason is null to PasswordPolicyViolationException`() { + fun `from() maps GIdP policy violation in FirebaseAuthWeakPasswordException reason`() { val firebaseException = FirebaseAuthWeakPasswordException( "ERROR_WEAK_PASSWORD", - "PASSWORD_DOES_NOT_MEET_REQUIREMENTS : [MISSING_LOWERCASE_CHARACTER, MISSING_NON_ALPHANUMERIC_CHARACTER]", - null + "weak", + "PASSWORD_DOES_NOT_MEET_REQUIREMENTS : [MISSING_UPPERCASE_CHARACTER, MISSING_NUMERIC_CHARACTER]" ) val result = AuthException.from(firebaseException) @@ -222,19 +240,18 @@ class AuthExceptionTest { assertThat(result).isInstanceOf(AuthException.PasswordPolicyViolationException::class.java) val policyEx = result as AuthException.PasswordPolicyViolationException assertThat(policyEx.failingRequirements).containsExactly( - "MISSING_LOWERCASE_CHARACTER", - "MISSING_NON_ALPHANUMERIC_CHARACTER" + "MISSING_UPPERCASE_CHARACTER", + "MISSING_NUMERIC_CHARACTER" ).inOrder() } @Test - fun `from() maps policy violation with string provider to human-readable message`() { + fun `from() maps policy violation short codes via string provider`() { val firebaseException = FirebaseAuthWeakPasswordException( "ERROR_WEAK_PASSWORD", "weak", "PASSWORD_DOES_NOT_MEET_REQUIREMENTS : [MISSING_UPPERCASE_CHARACTER, MISSING_NUMERIC_CHARACTER]" ) - val stringProvider = mock(AuthUIStringProvider::class.java) whenever(stringProvider.passwordMissingUppercase).thenReturn("Needs uppercase") whenever(stringProvider.passwordMissingDigit).thenReturn("Needs a number") @@ -246,19 +263,17 @@ class AuthExceptionTest { } @Test - fun `from() maps unknown requirement code to raw code string`() { - val firebaseException = FirebaseAuthWeakPasswordException( - "ERROR_WEAK_PASSWORD", - "weak", - "PASSWORD_DOES_NOT_MEET_REQUIREMENTS : [SOME_FUTURE_REQUIREMENT]" - ) + fun `from() passes through unknown requirement strings as-is`() { + val msg = "An internal error has occurred. [ PASSWORD_DOES_NOT_MEET_REQUIREMENTS:" + + "Missing password requirements: [Some future requirement] ]" + val firebaseException = object : com.google.firebase.FirebaseException(msg) {} val result = AuthException.from(firebaseException) assertThat(result).isInstanceOf(AuthException.PasswordPolicyViolationException::class.java) val policyEx = result as AuthException.PasswordPolicyViolationException - assertThat(policyEx.failingRequirements).containsExactly("SOME_FUTURE_REQUIREMENT") - assertThat(policyEx.message).isEqualTo("SOME_FUTURE_REQUIREMENT") + assertThat(policyEx.failingRequirements).containsExactly("Some future requirement") + assertThat(policyEx.message).isEqualTo("Some future requirement") } @Test @@ -274,6 +289,15 @@ class AuthExceptionTest { assertThat(result).isInstanceOf(AuthException.WeakPasswordException::class.java) } + @Test + fun `from() maps plain FirebaseException without policy to NetworkException`() { + val firebaseException = object : com.google.firebase.FirebaseException("Network timeout") {} + + val result = AuthException.from(firebaseException) + + assertThat(result).isInstanceOf(AuthException.NetworkException::class.java) + } + @Test fun `PasswordPolicyViolationException stores failingRequirements correctly`() { val requirements = listOf("MISSING_UPPERCASE_CHARACTER", "MISSING_NUMERIC_CHARACTER") From bf18e23d52494c814e16128ce40aa729526f7b7a Mon Sep 17 00:00:00 2001 From: demolaf Date: Mon, 8 Jun 2026 10:04:54 +0100 Subject: [PATCH 3/4] updates --- auth/src/main/java/com/firebase/ui/auth/AuthException.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/auth/src/main/java/com/firebase/ui/auth/AuthException.kt b/auth/src/main/java/com/firebase/ui/auth/AuthException.kt index fa45746ee..7f9dbd84e 100644 --- a/auth/src/main/java/com/firebase/ui/auth/AuthException.kt +++ b/auth/src/main/java/com/firebase/ui/auth/AuthException.kt @@ -561,8 +561,8 @@ abstract class AuthException( val policyIndex = message.indexOf("PASSWORD_DOES_NOT_MEET_REQUIREMENTS", ignoreCase = true) if (policyIndex == -1) return emptyList() val start = message.indexOf('[', policyIndex) - val end = message.indexOf(']', start) - if (start == -1 || end == -1) return emptyList() + val end = message.indexOf(']', policyIndex) + if (start == -1 || end == -1 || end <= start) return emptyList() return message.substring(start + 1, end) .split(',') .map { it.trim() } @@ -573,7 +573,7 @@ abstract class AuthException( // for those. For older SDK versions that may surface short error codes instead // (e.g. MISSING_UPPERCASE_CHARACTER), map those to localised strings. private fun mapRequirementToString(code: String, stringProvider: AuthUIStringProvider?): String { - return when (code.uppercase()) { + return when (code.uppercase(java.util.Locale.US)) { "MISSING_UPPERCASE_CHARACTER" -> stringProvider?.passwordMissingUppercase ?: "Password must contain at least one uppercase letter" "MISSING_LOWERCASE_CHARACTER" -> From 114dee8a91c4fbbb731dd23a3e835f2fef83a41b Mon Sep 17 00:00:00 2001 From: demolaf Date: Mon, 8 Jun 2026 10:22:26 +0100 Subject: [PATCH 4/4] cleanup --- .../com/firebase/ui/auth/AuthException.kt | 35 ++++--------------- .../com/firebase/ui/auth/AuthExceptionTest.kt | 24 +++---------- 2 files changed, 10 insertions(+), 49 deletions(-) diff --git a/auth/src/main/java/com/firebase/ui/auth/AuthException.kt b/auth/src/main/java/com/firebase/ui/auth/AuthException.kt index 7f9dbd84e..779b93a8d 100644 --- a/auth/src/main/java/com/firebase/ui/auth/AuthException.kt +++ b/auth/src/main/java/com/firebase/ui/auth/AuthException.kt @@ -127,15 +127,13 @@ abstract class AuthException( * The password violates one or more Google Identity Platform password policy requirements. * * This exception is thrown when GIdP password policy enforcement is enabled and the supplied - * password fails one or more configured constraints (e.g. missing uppercase, missing digit). - * [failingRequirements] contains the raw server-side constraint identifiers such as - * `MISSING_UPPERCASE_CHARACTER` or `MISSING_NON_ALPHANUMERIC_CHARACTER`. + * password fails one or more configured constraints (e.g. minimum length, missing uppercase). * - * [message] is a newline-separated, human-readable description of each failing constraint, - * suitable for direct display in the UI. + * [message] is a newline-separated, human-readable description of each failing constraint + * as returned by the server, suitable for direct display in the UI. * * @property message Human-readable description of the failing constraints - * @property failingRequirements Raw server-side constraint identifiers + * @property failingRequirements The individual constraint strings from the server * @property cause The underlying [Throwable] that caused this exception */ class PasswordPolicyViolationException( @@ -382,10 +380,8 @@ abstract class AuthException( val sourceText = firebaseException.reason ?: firebaseException.message ?: "" if (sourceText.contains("PASSWORD_DOES_NOT_MEET_REQUIREMENTS", ignoreCase = true)) { val requirements = parsePasswordPolicyRequirements(sourceText) - val humanReadable = requirements - .joinToString("\n") { mapRequirementToString(it, stringProvider) } PasswordPolicyViolationException( - message = humanReadable.ifEmpty { + message = requirements.joinToString("\n").ifEmpty { stringProvider?.errorWeakPasswordGeneric.nonEmpty() ?: "Password does not meet policy requirements" }, @@ -510,10 +506,8 @@ abstract class AuthException( val msg = firebaseException.message ?: "" if (msg.contains("PASSWORD_DOES_NOT_MEET_REQUIREMENTS", ignoreCase = true)) { val requirements = parsePasswordPolicyRequirements(msg) - val humanReadable = requirements - .joinToString("\n") { mapRequirementToString(it, stringProvider) } PasswordPolicyViolationException( - message = humanReadable.ifEmpty { + message = requirements.joinToString("\n").ifEmpty { stringProvider?.errorWeakPasswordGeneric.nonEmpty() ?: "Password does not meet policy requirements" }, @@ -568,22 +562,5 @@ abstract class AuthException( .map { it.trim() } .filter { it.isNotEmpty() } } - - // GIdP already returns human-readable requirement strings, so this is a pass-through - // for those. For older SDK versions that may surface short error codes instead - // (e.g. MISSING_UPPERCASE_CHARACTER), map those to localised strings. - private fun mapRequirementToString(code: String, stringProvider: AuthUIStringProvider?): String { - return when (code.uppercase(java.util.Locale.US)) { - "MISSING_UPPERCASE_CHARACTER" -> - stringProvider?.passwordMissingUppercase ?: "Password must contain at least one uppercase letter" - "MISSING_LOWERCASE_CHARACTER" -> - stringProvider?.passwordMissingLowercase ?: "Password must contain at least one lowercase letter" - "MISSING_NUMERIC_CHARACTER" -> - stringProvider?.passwordMissingDigit ?: "Password must contain at least one number" - "MISSING_NON_ALPHANUMERIC_CHARACTER" -> - stringProvider?.passwordMissingSpecialCharacter ?: "Password must contain at least one special character" - else -> code - } - } } } diff --git a/auth/src/test/java/com/firebase/ui/auth/AuthExceptionTest.kt b/auth/src/test/java/com/firebase/ui/auth/AuthExceptionTest.kt index 7ab4b4e86..ea8ec7ecd 100644 --- a/auth/src/test/java/com/firebase/ui/auth/AuthExceptionTest.kt +++ b/auth/src/test/java/com/firebase/ui/auth/AuthExceptionTest.kt @@ -232,7 +232,7 @@ class AuthExceptionTest { val firebaseException = FirebaseAuthWeakPasswordException( "ERROR_WEAK_PASSWORD", "weak", - "PASSWORD_DOES_NOT_MEET_REQUIREMENTS : [MISSING_UPPERCASE_CHARACTER, MISSING_NUMERIC_CHARACTER]" + "PASSWORD_DOES_NOT_MEET_REQUIREMENTS : [Password must contain uppercase, Password must contain a number]" ) val result = AuthException.from(firebaseException) @@ -240,26 +240,10 @@ class AuthExceptionTest { assertThat(result).isInstanceOf(AuthException.PasswordPolicyViolationException::class.java) val policyEx = result as AuthException.PasswordPolicyViolationException assertThat(policyEx.failingRequirements).containsExactly( - "MISSING_UPPERCASE_CHARACTER", - "MISSING_NUMERIC_CHARACTER" + "Password must contain uppercase", + "Password must contain a number" ).inOrder() - } - - @Test - fun `from() maps policy violation short codes via string provider`() { - val firebaseException = FirebaseAuthWeakPasswordException( - "ERROR_WEAK_PASSWORD", - "weak", - "PASSWORD_DOES_NOT_MEET_REQUIREMENTS : [MISSING_UPPERCASE_CHARACTER, MISSING_NUMERIC_CHARACTER]" - ) - val stringProvider = mock(AuthUIStringProvider::class.java) - whenever(stringProvider.passwordMissingUppercase).thenReturn("Needs uppercase") - whenever(stringProvider.passwordMissingDigit).thenReturn("Needs a number") - - val result = AuthException.from(firebaseException, stringProvider) - - assertThat(result).isInstanceOf(AuthException.PasswordPolicyViolationException::class.java) - assertThat(result.message).isEqualTo("Needs uppercase\nNeeds a number") + assertThat(policyEx.message).isEqualTo("Password must contain uppercase\nPassword must contain a number") } @Test