Demo

FirebaseUI Auth is a modern, Compose-based authentication library that provides drop-in UI components for Firebase Authentication. It eliminates boilerplate code and promotes best practices for user authentication on Android.
Built entirely with Jetpack Compose and Material Design 3, FirebaseUI Auth offers:
Equivalent FirebaseUI libraries are available for iOS and Web.

Ensure your application is configured for use with Firebase. See the Firebase documentation for setup instructions.
Minimum Requirements:
Add the FirebaseUI Auth library dependency to your build.gradle.kts (Module):
dependencies {
// FirebaseUI for Auth
implementation("com.firebaseui:firebase-ui-auth:10.0.0-beta05")
// Required: Firebase Auth
implementation(platform("com.google.firebase:firebase-bom:34.17.0"))
implementation("com.google.firebase:firebase-auth")
// Required: Jetpack Compose
implementation(platform("androidx.compose:compose-bom:2026.06.01"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.material3:material3")
// Optional: Facebook Login (if using FacebookAuthProvider)
implementation("com.facebook.android:facebook-login:16.3.0")
}
Localization Support:
To optimize APK size, configure resource filtering for only the languages your app supports:
android {
defaultConfig {
resourceConfigurations += listOf("en", "es", "fr") // Add your supported languages
}
}
Google Sign-In configuration is automatically provided by the google-services Gradle plugin. Ensure you have enabled Google Sign-In in the Firebase Console.
If using Facebook Login, add your Facebook App ID to strings.xml:
<resources>
<string name="facebook_application_id" translatable="false">YOUR_FACEBOOK_APP_ID</string>
<string name="facebook_login_protocol_scheme" translatable="false">fbYOUR_FACEBOOK_APP_ID</string>
<string name="facebook_client_token" translatable="false">CHANGE-ME</string>
</resources>
See the Facebook for Developers documentation for setup instructions.
Twitter, GitHub, Microsoft, Yahoo, and Apple providers require configuration in the Firebase Console but no additional Android-specific setup. See the Firebase Auth documentation for provider-specific instructions.
Here's the simplest way to add authentication to your app with Email and Google Sign-In:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyAppTheme {
val configuration = authUIConfiguration {
context = applicationContext
providers {
provider(AuthProvider.Email())
provider(AuthProvider.Google())
}
}
FirebaseAuthScreen(
configuration = configuration,
onSignInSuccess = { result ->
Toast.makeText(this, "Welcome!", Toast.LENGTH_SHORT).show()
// Navigate to main app screen
},
onSignInFailure = { exception ->
Toast.makeText(this, "Error: ${exception.message}", Toast.LENGTH_SHORT).show()
},
onSignInCancelled = {
// User backed out of a single provider (e.g. dismissed the Google
// Credential Manager sheet); FirebaseAuthScreen already returns to
// the method picker on its own — no action needed here.
}
)
}
}
}
}
That's it! This provides a complete authentication flow with:
Before showing the authentication UI, check if a user is already signed in:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val authUI = FirebaseAuthUI.getInstance()
if (authUI.isSignedIn()) {
// User is already signed in, navigate to main app
startActivity(Intent(this, MainAppActivity::class.java))
finish()
} else {
// Show authentication UI
setContent {
FirebaseAuthScreen(/* ... */)
}
}
}
}
Or observe authentication state changes reactively:
@Composable
fun AuthGate() {
val authUI = remember { FirebaseAuthUI.getInstance() }
val authState by authUI.authStateFlow().collectAsState(initial = AuthState.Idle)
when {
authState is AuthState.Success -> {
// User is signed in
MainAppScreen()
}
else -> {
// Show authentication
FirebaseAuthScreen(/* ... */)
}
}
}
FirebaseAuthUI is the central class that coordinates all authentication operations. It manages UI state and provides methods for signing in, signing up, and managing user accounts.
// Get the default instance
val authUI = FirebaseAuthUI.getInstance()
// Or get an instance for a specific Firebase app
val customApp = Firebase.app("secondary")
val authUI = FirebaseAuthUI.getInstance(customApp)
// Or create with custom auth (for multi-tenancy)
val customAuth = Firebase.auth(customApp)
val authUI = FirebaseAuthUI.create(app = customApp, auth = customAuth)
Key Methods:
| Method | Return Type | Description |
|---|---|---|
isSignedIn() |
Boolean |
Checks if a user is currently signed in |
getCurrentUser() |
FirebaseUser? |
Returns the current user, if signed in |
authStateFlow() |
Flow<AuthState> |
Observes authentication state changes |
createAuthFlow(config) |
AuthFlowController |
Creates a sign-in flow controller |
signOut(context) |
suspend fun |
Signs out the current user |
delete(context) |
suspend fun |
Deletes the current user account |
AuthUIConfiguration defines all settings for your authentication flow. Use the DSL builder function for easy configuration:
val authTheme = AuthUITheme.fromMaterialTheme() // @Composable — resolve it here, not below
val configuration = authUIConfiguration {
// Required: an application Context. Omitting it throws when the block is evaluated.
context = applicationContext
// Required: Authentication providers
providers {
provider(AuthProvider.Email())
provider(AuthProvider.Google())
provider(AuthProvider.Phone())
}
// Optional: Theme. AuthUITheme.fromMaterialTheme() and AuthUITheme.Adaptive are
// @Composable, so resolve them above the builder and assign the result here.
theme = authTheme
// Optional: Terms of Service and Privacy Policy URLs
tosUrl = "https://example.com/terms"
privacyPolicyUrl = "https://example.com/privacy"
// Optional: App logo. Wrap the source in an AuthUIAsset — a bare ImageVector is a type error.
logo = AuthUIAsset.Vector(Icons.Default.AccountCircle)
// Optional: Enable MFA (default: true)
isMfaEnabled = true
// Optional: Enable Credential Manager (default: true)
isCredentialManagerEnabled = true
// Optional: Allow anonymous user upgrade (default: false)
isAnonymousUpgradeEnabled = true
// Optional: Require display name on sign-up (default: true)
isDisplayNameRequired = true
// Optional: Allow new email accounts (default: true)
isNewEmailAccountsAllowed = true
// Optional: Always show provider choice even with one provider (default: false)
isProviderChoiceAlwaysShown = false
// Optional: Custom string provider for localization
stringProvider = MyCustomStringProvider()
// Optional: Locale override
locale = Locale.FRENCH
// Optional: when a non-anonymous user is already signed in, link the new credential
// onto that account instead of switching accounts (default: false)
isCredentialLinkingEnabled = false
// Optional: send password-reset links to your own page rather than the Firebase-hosted
// one (default: null)
passwordResetActionCodeSettings = actionCodeSettings {
url = "https://example.com/reset"
handleCodeInApp = true
setAndroidPackageName(packageName, true, null)
}
// Optional: resolve an email to its providers with the legacy fetchSignInMethodsForEmail
// call. Only useful if email enumeration protection is disabled on your project
// (default: false)
legacyFetchSignInWithEmail = false
}
AuthFlowController manages the lifecycle of an authentication flow programmatically. This is the low-level API for advanced use cases.
val controller = authUI.createAuthFlow(configuration)
// Register before the Activity reaches STARTED — as a property initializer or in onCreate.
// Registering later (in a click listener, say) throws.
val authLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { /* the flow finished; inspect FirebaseAuth.currentUser or the result extras */ }
authLauncher.launch(controller.createIntent(this))
// Follow the flow in detail by collecting its state
lifecycleScope.launch {
controller.authStateFlow.collect { state ->
when (state) {
is AuthState.Success -> {
// Handle success
val user = state.user
}
is AuthState.Error -> {
// Handle error
Log.e(TAG, "Auth failed", state.exception)
}
is AuthState.Cancelled -> {
// User cancelled a single sign-in attempt (e.g. dismissed the
// Credential Manager sheet, backed out of MFA); the flow stays open
}
is AuthState.Aborted -> {
// Flow was ended via controller.cancel()
finish()
}
else -> {
// Handle other states (RequiresMfa, RequiresEmailVerification, etc.)
}
}
}
}
// Cancel the flow if needed
controller.cancel()
// Clean up when done
override fun onDestroy() {
super.onDestroy()
controller.dispose()
}
AuthState represents the current state of authentication:
sealed class AuthState {
object Idle : AuthState()
data class Loading(val message: String?) : AuthState()
data class Success(val result: AuthResult?, val user: FirebaseUser, val isNewUser: Boolean = false) : AuthState()
data class Error(val exception: AuthException, val isRecoverable: Boolean) : AuthState()
data class RequiresMfa(val resolver: MultiFactorResolver, val hint: String? = null) : AuthState()
data class RequiresEmailVerification(val user: FirebaseUser, val email: String) : AuthState()
data class RequiresProfileCompletion(val user: FirebaseUser, val missingFields: List<String> = emptyList()) : AuthState()
object Cancelled : AuthState()
object Aborted : AuthState()
object PasswordResetLinkSent : AuthState()
object EmailSignInLinkSent : AuthState()
data class SMSAutoVerified(val credential: PhoneAuthCredential) : AuthState()
data class PhoneNumberVerificationRequired(
val verificationId: String,
val forceResendingToken: PhoneAuthProvider.ForceResendingToken
) : AuthState()
}
Configure email/password authentication with optional customization:
val emailProvider = AuthProvider.Email(
// Optional: Require display name (default: true)
isDisplayNameRequired = true,
// Optional: Enable email link sign-in (default: false)
isEmailLinkSignInEnabled = true,
// Optional: Force email link on same device (default: true)
isEmailLinkForceSameDeviceEnabled = true,
// Optional: Action code settings for email link
emailLinkActionCodeSettings = actionCodeSettings {
url = "https://example.com/auth"
handleCodeInApp = true
setAndroidPackageName(packageName, true, null)
},
// Optional: Allow new accounts (default: true)
isNewAccountsAllowed = true,
// Optional: Minimum password length (default: 6)
minimumPasswordLength = 8,
// Optional: Custom password validation rules
passwordValidationRules = listOf(
PasswordRule.MinimumLength(8),
PasswordRule.RequireUppercase,
PasswordRule.RequireLowercase,
PasswordRule.RequireDigit,
PasswordRule.RequireSpecialCharacter
)
)
val configuration = authUIConfiguration {
context = applicationContext
providers { provider(emailProvider) }
}
Configure phone number authentication with SMS verification:
val phoneProvider = AuthProvider.Phone(
// Optional: Default phone number in international format
defaultNumber = "+15551234567",
// Optional: Default country code (ISO alpha-2 format)
defaultCountryCode = "US",
// Optional: Allowed countries
allowedCountries = listOf("US", "CA", "GB"),
// Optional: Timeout for SMS delivery in seconds (default: 60)
timeout = 60L,
// Optional: Enable instant verification (default: true)
isInstantVerificationEnabled = true
)
val configuration = authUIConfiguration {
context = applicationContext
providers {
provider(phoneProvider)
}
}
Configure Google Sign-In with optional scopes and server client ID:
val googleProvider = AuthProvider.Google(
// Required: Scopes to request
scopes = listOf("https://www.googleapis.com/auth/drive.file"),
// Optional: Server client ID for backend authentication
serverClientId = "YOUR_SERVER_CLIENT_ID.apps.googleusercontent.com",
// Optional: Custom OAuth parameters
customParameters = mapOf("prompt" to "select_account")
)
val configuration = authUIConfiguration {
context = applicationContext
providers {
provider(googleProvider)
}
}
Configure Facebook Login with optional permissions:
val facebookProvider = AuthProvider.Facebook(
// Optional: Permissions to request (default: ["email", "public_profile"])
scopes = listOf("email", "public_profile", "user_friends"),
// Optional: Custom OAuth parameters
customParameters = mapOf("display" to "popup")
)
val configuration = authUIConfiguration {
context = applicationContext
providers {
provider(facebookProvider)
}
}
FirebaseUI supports Twitter, GitHub, Microsoft, Yahoo, and Apple:
// Twitter
val twitterProvider = AuthProvider.Twitter(
// Required: Custom OAuth parameters
customParameters = mapOf("lang" to "en")
)
// GitHub
val githubProvider = AuthProvider.Github(
// Optional: Scopes to request (default: ["user:email"])
scopes = listOf("user:email", "read:user"),
// Required: Custom OAuth parameters
customParameters = mapOf("allow_signup" to "false")
)
// Microsoft
val microsoftProvider = AuthProvider.Microsoft(
// Optional: Scopes to request (default: ["openid", "profile", "email"])
scopes = listOf("openid", "profile", "email", "User.Read"),
// Optional: Tenant ID for Azure Active Directory
tenant = "YOUR_TENANT_ID",
// Required: Custom OAuth parameters
customParameters = mapOf("prompt" to "consent")
)
// Yahoo
val yahooProvider = AuthProvider.Yahoo(
// Optional: Scopes to request (default: ["openid", "profile", "email"])
scopes = listOf("openid", "profile", "email"),
// Required: Custom OAuth parameters
customParameters = mapOf("language" to "en-us")
)
// Apple
val appleProvider = AuthProvider.Apple(
// Optional: Scopes to request (default: ["name", "email"])
scopes = listOf("name", "email"),
// Optional: Locale for the sign-in page
locale = "en_US",
// Required: Custom OAuth parameters
customParameters = mapOf("ui_locales" to "en-US")
)
val configuration = authUIConfiguration {
context = applicationContext
providers {
provider(twitterProvider)
provider(githubProvider)
provider(microsoftProvider)
provider(yahooProvider)
provider(appleProvider)
}
}
Enable anonymous authentication to let users use your app without signing in:
val configuration = authUIConfiguration {
context = applicationContext
providers {
provider(AuthProvider.Anonymous())
}
// Enable anonymous user upgrade
isAnonymousUpgradeEnabled = true
}
Support any OAuth provider configured in the Firebase Console:
val lineProvider = AuthProvider.GenericOAuth(
// Required: Provider name
providerName = "LINE",
// Required: Provider ID as configured in Firebase Console
providerId = "oidc.line",
// Required: Scopes to request
scopes = listOf("profile", "openid", "email"),
// Required: Custom OAuth parameters
customParameters = mapOf("prompt" to "consent"),
// Required: Button label
buttonLabel = "Sign in with LINE",
// Optional: Custom button icon
buttonIcon = AuthUIAsset.Resource(R.drawable.ic_line),
// Optional: Custom button background color
buttonColor = Color(0xFF06C755),
// Optional: Custom button content color
contentColor = Color.White
)
val configuration = authUIConfiguration {
context = applicationContext
providers {
provider(lineProvider)
}
}
The high-level API provides a complete, opinionated authentication experience with minimal code:
@Composable
fun AuthenticationScreen() {
val localContext = LocalContext.current
val configuration = authUIConfiguration {
context = localContext
providers {
provider(AuthProvider.Email())
provider(AuthProvider.Google())
provider(AuthProvider.Facebook())
provider(AuthProvider.Phone())
}
tosUrl = "https://example.com/terms"
privacyPolicyUrl = "https://example.com/privacy"
logo = AuthUIAsset.Vector(Icons.Default.Lock)
}
FirebaseAuthScreen(
configuration = configuration,
onSignInSuccess = { result ->
val user = result.user
val isNewUser = result.additionalUserInfo?.isNewUser ?: false
if (isNewUser) {
// First-time user
navigateToOnboarding()
} else {
// Returning user
navigateToHome()
}
},
onSignInFailure = { exception ->
when (exception) {
is AuthException.NetworkException -> {
showSnackbar("No internet connection")
}
is AuthException.TooManyRequestsException -> {
showSnackbar("Too many attempts. Please try again later.")
}
else -> {
showSnackbar("Authentication failed: ${exception.message}")
}
}
},
onSignInCancelled = {
// User backed out of a single provider; the screen already returns
// to the method picker on its own.
}
)
}
FirebaseAuthScreen Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
configuration |
AuthUIConfiguration |
Required | Authentication configuration (providers, theme, etc.) |
onSignInSuccess |
(AuthResult) -> Unit |
Required | Callback when sign-in succeeds |
onSignInFailure |
(AuthException) -> Unit |
Required | Callback when sign-in fails |
onSignInCancelled |
() -> Unit |
Required | Callback when the user backs out of a single sign-in attempt (AuthState.Cancelled, e.g. dismissing the Google Credential Manager sheet); FirebaseAuthScreen already returns to the method picker itself, so this is informational only. Not called when the whole flow ends via AuthFlowController.cancel() (AuthState.Aborted) — that state is observable directly on authUI.authStateFlow()/authFlowController.authStateFlow for callers who need it |
modifier |
Modifier |
Modifier |
Modifier for the composable |
authUI |
FirebaseAuthUI |
FirebaseAuthUI.getInstance() |
Custom FirebaseAuthUI instance (for multi-app support) |
emailLink |
String? |
null |
Email link for passwordless sign-in (see Email Link Sign-In) |
mfaConfiguration |
MfaConfiguration |
MfaConfiguration() |
MFA settings (see Multi-Factor Authentication) |
authenticatedContent |
@Composable ((AuthState, AuthSuccessUiContext) -> Unit)? |
null |
Optional content to show after successful authentication |
Using authenticatedContent:
Show custom UI after authentication completes, before navigating away:
FirebaseAuthScreen(
configuration = configuration,
onSignInSuccess = { result ->
// Called after authenticatedContent is dismissed
navigateToHome()
},
onSignInFailure = { exception ->
showError(exception)
},
onSignInCancelled = {
// User backed out of a single provider; the screen already returns
// to the method picker on its own.
},
authenticatedContent = { state, uiContext ->
// Show a welcome screen or profile completion UI
Column(
modifier = Modifier.fillMaxSize().padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text("Welcome, ${(state as? AuthState.Success)?.user?.displayName}!")
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = { uiContext.onContinue() }) {
Text("Continue to App")
}
}
}
)
For maximum control, use the AuthFlowController:
class AuthActivity : ComponentActivity() {
private lateinit var controller: AuthFlowController
private val authLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { /* the flow finished */ }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val authUI = FirebaseAuthUI.getInstance()
val configuration = authUIConfiguration {
context = applicationContext
providers {
provider(AuthProvider.Email())
provider(AuthProvider.Google())
}
}
controller = authUI.createAuthFlow(configuration)
// Only on a fresh start; unguarded, every recreation would launch a second flow.
if (savedInstanceState == null) {
authLauncher.launch(controller.createIntent(this))
}
lifecycleScope.launch {
controller.authStateFlow.collect { handleAuthState(it) }
}
}
private fun handleAuthState(state: AuthState) {
when (state) {
is AuthState.Success -> {
// Successfully signed in
val user = state.user
startActivity(Intent(this, MainActivity::class.java))
finish()
}
is AuthState.Error -> {
// Handle error
AlertDialog.Builder(this)
.setTitle("Authentication Failed")
.setMessage(state.exception.message)
.setPositiveButton("OK", null)
.show()
}
is AuthState.RequiresMfa -> {
// User needs to complete MFA challenge
showMfaChallengeDialog(state.resolver)
}
is AuthState.RequiresEmailVerification -> {
// Email verification needed
showEmailVerificationScreen(state.user)
}
is AuthState.Cancelled -> {
// User cancelled a single sign-in attempt; the flow stays open
// and returns to the method picker
}
is AuthState.Aborted -> {
// Flow was ended via controller.cancel()
finish()
}
else -> {
// Handle other states
}
}
}
override fun onDestroy() {
super.onDestroy()
controller.dispose()
}
}
FirebaseAuthScreen accepts optional slot parameters that let you replace individual screens with your own UI while keeping all authentication logic intact. Each slot receives a state object with the data and callbacks needed to drive your UI.
FirebaseAuthScreen(
configuration = configuration,
onSignInSuccess = { /* ... */ },
onSignInFailure = { /* ... */ },
onSignInCancelled = { /* ... */ },
customMethodPickerLayout = { providers, onProviderSelected -> /* ... */ },
customMethodPickerTermsConfiguration = MethodPickerTermsConfiguration(
content = { Text("By continuing you agree to our Terms") },
accepted = termsAccepted,
disableProvidersUntilAccepted = true,
),
emailContent = { state -> /* ... */ },
phoneContent = { state -> /* ... */ },
mfaEnrollmentContent = { state -> /* ... */ },
mfaChallengeContent = { state -> /* ... */ },
reauthContent = { state -> /* ... */ },
) { authState, uiContext ->
// authenticated content
}
customMethodPickerLayout)Replaces the default provider selection screen. Receives the configured providers and a callback to invoke when the user selects one.
customMethodPickerLayout = { providers, onProviderSelected ->
Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) {
providers.forEach { provider ->
OutlinedButton(
onClick = { onProviderSelected(provider) },
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
) {
Text("Continue with ${provider.providerName}")
}
}
}
}
Use customMethodPickerTermsConfiguration alongside it to add a terms-of-service checkbox that can optionally gate provider selection until accepted.
customMethodPickerTermsConfiguration = MethodPickerTermsConfiguration(
content = { Text("I agree to the Terms of Service") },
accepted = termsAccepted,
disableProvidersUntilAccepted = true,
)
emailContent)Replaces the default email sign-in / sign-up / password reset screens. The EmailAuthContentState carries the current mode (SignIn, SignUp, ResetPassword, EmailLinkSignIn), field values, and callbacks for every action.
emailContent = { state ->
when (state.mode) {
EmailAuthMode.SignIn -> {
Column {
OutlinedTextField(
value = state.email,
onValueChange = state.onEmailChange,
label = { Text("Email") },
)
OutlinedTextField(
value = state.password,
onValueChange = state.onPasswordChange,
label = { Text("Password") },
visualTransformation = PasswordVisualTransformation(),
)
state.error?.let { Text(it, color = MaterialTheme.colorScheme.error) }
Button(onClick = state.onSignInClick, enabled = !state.isLoading) {
Text("Sign in")
}
TextButton(onClick = state.onGoToSignUp) { Text("Create account") }
TextButton(onClick = state.onGoToResetPassword) { Text("Forgot password?") }
}
}
EmailAuthMode.SignUp -> { /* ... */ }
EmailAuthMode.ResetPassword -> { /* ... */ }
EmailAuthMode.EmailLinkSignIn -> { /* ... */ }
}
}
phoneContent)Replaces the default phone number entry and SMS code verification screens. The PhoneAuthContentState carries the current step (EnterPhoneNumber, EnterVerificationCode), field values, and callbacks.
phoneContent = { state ->
when (state.step) {
PhoneAuthStep.EnterPhoneNumber -> {
Column {
OutlinedTextField(
value = state.phoneNumber,
onValueChange = state.onPhoneNumberChange,
label = { Text("Phone number") },
)
state.error?.let { Text(it, color = MaterialTheme.colorScheme.error) }
Button(onClick = state.onSendCodeClick, enabled = !state.isLoading) {
Text("Send code")
}
}
}
PhoneAuthStep.EnterVerificationCode -> {
Column {
OutlinedTextField(
value = state.verificationCode,
onValueChange = state.onVerificationCodeChange,
label = { Text("Verification code") },
)
Button(onClick = state.onVerifyCodeClick, enabled = !state.isLoading) {
Text("Verify")
}
if (state.resendTimer == 0) {
TextButton(onClick = state.onResendCodeClick) { Text("Resend code") }
}
}
}
}
}
mfaEnrollmentContent)Replaces the default MFA enrollment screens. The MfaEnrollmentContentState carries the current step, availableFactors, enrolledFactors, and callbacks for factor selection, unenrollment, and navigation.
mfaEnrollmentContent = { state ->
when (state.step) {
MfaEnrollmentStep.SelectFactor -> {
Column {
state.availableFactors.forEach { factor ->
Button(onClick = { state.onFactorSelected(factor) }) {
Text("Enroll ${factor.name}")
}
}
state.onSkipClick?.let { skip ->
TextButton(onClick = skip) { Text("Skip") }
}
}
}
// Handle other steps...
else -> { /* ... */ }
}
}
mfaChallengeContent)Replaces the default MFA verification screen shown during sign-in. The MfaChallengeContentState carries factorType, verificationCode, resendTimer, and callbacks to verify or resend.
mfaChallengeContent = { state ->
Column {
state.maskedPhoneNumber?.let { Text("Code sent to $it") }
OutlinedTextField(
value = state.verificationCode,
onValueChange = state.onVerificationCodeChange,
label = { Text("Verification code") },
)
state.error?.let { Text(it, color = MaterialTheme.colorScheme.error) }
Button(onClick = state.onVerifyClick, enabled = !state.isLoading) {
Text("Verify")
}
if (state.resendTimer == 0) {
state.onResendCodeClick?.let { resend ->
TextButton(onClick = resend) { Text("Resend code") }
}
} else {
Text("Resend available in ${state.resendTimer}s")
}
TextButton(onClick = state.onCancelClick) { Text("Cancel") }
}
}
reauthContent)Replaces the default reauthentication bottom sheet shown when a sensitive operation requires the user to re-verify their identity. The ReauthContentState carries user, reason, the providers already filtered to those linked to that user, and callbacks to select a provider or dismiss.
The library owns the credential exchange, so the slot only renders a provider chooser. Selecting a federated provider reauthenticates directly; selecting AuthProvider.Email or AuthProvider.Phone hands off to the library's own email/phone sub-flow, which honours your emailContent / phoneContent slots and replaces this slot while it is active. Password and OTP entry therefore never appear here.
If the account has multi-factor authentication enrolled, Firebase needs the second factor to complete the reauthentication too. The library presents the MFA challenge as another sub-flow over this slot, honouring your mfaChallengeContent slot; resolving it completes the reauthentication and the pending operation resumes. Backing out of the challenge returns to this slot with the operation still pending, and a failed challenge latches into state.error like any other failed attempt.
reauthContent = { state ->
AlertDialog(
onDismissRequest = state.onDismiss,
title = { Text(state.reason ?: "Verify your identity") },
text = {
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
state.error?.let { Text(it, color = MaterialTheme.colorScheme.error) }
if (state.isLoading) CircularProgressIndicator()
state.providers.forEach { provider ->
Button(
onClick = { state.onProviderSelected(provider) },
enabled = !state.isLoading,
) { Text("Continue with ${provider.providerName}") }
}
}
},
confirmButton = {},
dismissButton = {
TextButton(onClick = state.onDismiss) { Text("Cancel") }
},
)
}
While this slot is shown the library suppresses its own loading and error dialogs, so render state.isLoading and state.error yourself. state.error is the same message the library's own error dialog would have shown, and state.exception carries the exception behind it when you need to branch on the failure type. On success the library resumes the operation that required reauthentication — there is nothing to retry. state.onDismiss abandons reauthentication and calls onSignInCancelled, so any pending operation will never run; backing out of a single provider attempt returns to the slot with the operation still pending and does not call onSignInCancelled. Render the slot so it blocks interaction with the content behind it — that content stays composed, and the library only makes its own affordances inert.
An armed reauthentication survives Activity recreation: rotating keeps the pending operation, the latched state.error, its state.exception, and any active email/phone sub-flow. The pending operation cannot survive process death, and if it is lost the flow emits an AuthState.Error explaining that identity confirmation was interrupted rather than dropping the operation silently.
For most cases, use withReauth instead — it handles the full reauth cycle automatically and only shows the default bottom sheet. Use reauthContent when you need a custom design for the reauth UI.
Firebase requires the user to have signed in recently before performing sensitive operations like deleting their account or changing their password. If the session is too old, Firebase throws FirebaseAuthRecentLoginRequiredException.
withReauth wraps any sensitive operation. If the exception is thrown, it automatically emits AuthState.Reauthentication.Required and — once the user reauthenticates via the default bottom sheet or your reauthContent slot — retries the original operation.
lifecycleScope.launch {
authUI.withReauth(
context = context,
reason = "Verify your identity to delete your account",
) {
authUI.auth.currentUser?.delete()?.await()
}
}
withReauth handles the full cycle:
FirebaseAuthRecentLoginRequiredException is thrown, emits AuthState.Reauthentication.Required with the retry attached.FirebaseAuthScreen shows the reauth UI scoped to the user's linked providers, including the MFA challenge when the account has a second factor enrolled.AuthState.Success or AuthState.Error.The armed reauthentication lives on the process-cached FirebaseAuthUI, so it survives Activity recreation; it does not survive process death, and a lost operation is reported as an AuthState.Error rather than silently dropped. The operation runs at most once: if a recreation interrupts it mid-flight the flow reports the interruption instead of starting it again, because the first attempt may already have committed.
What authStateFlow() emits while this is running. From the moment FirebaseAuthScreen picks the request up until it ends, every state is published as an AuthState.Reauthentication — the phases of that one request, each carrying its requestId and userUid. The ordinary AuthState.Loading / AuthState.Error / AuthState.Cancelled of the credential exchange are folded into those phases, so is AuthState.Error and is AuthState.Loading do not match for the duration and app-side error dialogs and spinners stay quiet: the library owns the UI for that window. Match is AuthState.Reauthentication if you need to know it is happening. The final outcome — AuthState.Success, AuthState.Error or AuthState.Idle — is published as an ordinary state once the request ends. Arming a request with no FirebaseAuthScreen composed (catching withReauth/delete's exception and showing your own UI) folds nothing: states are published normally, and the next one simply replaces the arming.
Activity-based alternative: use createReauthFlow to start a standalone reauthentication activity scoped to the current user's linked providers, returning an AuthFlowController.
val reauth = authUI.createReauthFlow(
configuration = authUIConfiguration {
context = applicationContext
// Required by the builder; createReauthFlow then filters this list down to the
// providers actually linked to the current user.
providers {
provider(AuthProvider.Email())
provider(AuthProvider.Google())
}
},
)
val intent = reauth.createIntent(context)
launcher.launch(intent)
Enable and configure Multi-Factor Authentication:
val mfaConfig = MfaConfiguration(
// Allowed MFA factors (default: [Sms, Totp])
allowedFactors = listOf(MfaFactor.Sms, MfaFactor.Totp),
// Optional: Require MFA enrollment (default: false)
requireEnrollment = false,
// Optional: restrict the SMS enrollment step's country selector, as ISO 3166-1 alpha-2
// codes (default: null, no restriction). Independent of the phone sign-in provider's own
// allowedCountries — an SMS second factor is configured separately from phone sign-in.
allowedCountries = listOf("US", "CA", "GB")
)
val configuration = authUIConfiguration {
context = applicationContext
providers {
provider(AuthProvider.Email())
}
isMfaEnabled = true
}
Prompt users to enroll in MFA after sign-in:
Every enrollment step is its own navigation destination, so the host owns the step and navigates
between them. Keep the flow state above the NavDisplay — a step switch must not dispose what a
previous step collected.
@Serializable
data class MfaStepKey(val step: MfaEnrollmentStep) : NavKey
@Composable
fun MfaEnrollmentFlow() {
val auth = FirebaseAuth.getInstance()
val currentUser = auth.currentUser
if (currentUser != null) {
val mfaConfig = MfaConfiguration(
allowedFactors = listOf(MfaFactor.Sms, MfaFactor.Totp),
allowedCountries = listOf("US", "CA", "GB")
)
val backStack = rememberNavBackStack(MfaStepKey(MfaEnrollmentStep.SelectFactor))
// Pass the restriction so the SMS step opens on a country the selector will offer. The
// screen also reconciles this itself, so a host that forgets cannot end up sending to an
// unpermitted dial code.
val flowState = rememberMfaEnrollmentFlowState(mfaConfig.allowedCountries)
// Read here, not inside onComplete: LocalContext.current is a @Composable read.
val context = LocalContext.current
NavDisplay(
backStack = backStack,
// NavDisplay throws on an empty back stack, and throws from recomposition, so the
// first step must not pop.
onBack = { if (backStack.size > 1) backStack.removeLastOrNull() },
entryProvider = entryProvider {
entry<MfaStepKey> { key ->
MfaEnrollmentScreen(
user = currentUser,
auth = auth,
configuration = mfaConfig,
onComplete = {
Toast.makeText(context, "MFA enrolled!", Toast.LENGTH_SHORT).show()
navigateToHome()
},
onSkip = { navigateToHome() },
step = key.step,
// A step already on top must not be pushed twice.
onNavigateToStep = {
val target = MfaStepKey(it)
if (backStack.lastOrNull() != target) backStack.add(target)
},
onNavigateBack = {
if (backStack.size > 1) backStack.removeLastOrNull()
},
flowState = flowState,
)
}
},
)
}
}
A back-stack key must be @Serializable to survive process death, so add the
org.jetbrains.kotlin.plugin.serialization plugin to the module hosting this screen. If you would
rather not own any of the navigation, use FirebaseAuthScreen and its mfaEnrollmentContent slot,
which owns it for you.
Or with custom UI:
MfaEnrollmentScreen(
user = currentUser,
auth = auth,
configuration = mfaConfig,
onComplete = { /* ... */ },
onSkip = { /* ... */ },
// Hosted exactly as above — the step and its two navigation callbacks, plus the flow state
// remembered above the NavDisplay.
step = key.step,
onNavigateToStep = {
val target = MfaStepKey(it)
if (backStack.lastOrNull() != target) backStack.add(target)
},
onNavigateBack = { if (backStack.size > 1) backStack.removeLastOrNull() },
flowState = flowState,
) { state ->
when (state.step) {
MfaEnrollmentStep.SelectFactor -> {
CustomFactorSelectionUI(state)
}
MfaEnrollmentStep.ConfigureSms -> {
CustomSmsConfigurationUI(state)
}
MfaEnrollmentStep.ConfigureTotp -> {
CustomTotpConfigurationUI(state)
}
MfaEnrollmentStep.VerifyFactor -> {
CustomVerificationUI(state)
}
}
}
Handle MFA challenges during sign-in. The challenge is automatically detected:
FirebaseAuthScreen(
configuration = configuration,
onSignInSuccess = { result ->
navigateToHome()
},
onSignInFailure = { exception ->
// MFA challenges are handled automatically by FirebaseAuthScreen
// But you can also handle them manually:
if (exception is AuthException.MfaRequiredException) {
// The resolver arrives on AuthState.RequiresMfa, not on the exception.
showMfaChallengePrompt()
}
}
)
Or handle manually:
@Composable
fun ManualMfaChallenge(resolver: MultiFactorResolver) {
MfaChallengeScreen(
resolver = resolver,
auth = FirebaseAuth.getInstance(),
onSuccess = { result ->
// The library resolved the challenge; the user is signed in
navigateToHome()
},
onCancel = {
navigateBack()
},
onError = { showError(it) }
)
}
FirebaseUI Auth provides flexible theming options to match your app's design:
AuthUITheme.Default / AuthUITheme.DefaultDark / AuthUITheme.Adaptive - Pre-configured Material Design 3 themes.copy() - Customize specific properties of the default themes (data class)fromMaterialTheme() - Inherit from your app's existing Material ThemeFirebaseUI provides pre-configured themes for light and dark modes:
val configuration = authUIConfiguration {
context = applicationContext
providers {
provider(AuthProvider.Email())
provider(AuthProvider.Google())
}
theme = AuthUITheme.Default // Light theme
// or
theme = AuthUITheme.DefaultDark // Dark theme
}
AuthUITheme.Adaptive automatically switches between light and dark themes based on the system setting:
val adaptiveTheme = AuthUITheme.Adaptive // @Composable getter — read it outside the builder
val configuration = authUIConfiguration {
context = applicationContext
providers {
provider(AuthProvider.Email())
provider(AuthProvider.Google())
}
theme = adaptiveTheme
}
This is the recommended approach for most apps as it provides a seamless experience that respects the user's system preferences.
Note: Adaptive is a @Composable property that evaluates to Default (light) or DefaultDark (dark) based on isSystemInDarkTheme() at composition time.
Use .copy() to customize specific properties of the default theme:
@Composable
fun AuthScreen() {
val localContext = LocalContext.current
val customTheme = AuthUITheme.Adaptive.copy(
providerButtonShape = MaterialTheme.shapes.extraLarge // Pill-shaped buttons
)
val configuration = authUIConfiguration {
context = localContext
providers {
provider(AuthProvider.Google())
provider(AuthProvider.Email())
}
theme = customTheme
}
FirebaseAuthScreen(
configuration = configuration,
onSignInSuccess = { /* ... */ },
onSignInFailure = { /* ... */ },
onSignInCancelled = { /* ... */ }
)
}
FirebaseUI Auth supports two theming patterns with clear precedence rules:
The simplest approach is to set the theme only in authUIConfiguration:
val adaptiveTheme = AuthUITheme.Adaptive
val configuration = authUIConfiguration {
context = applicationContext
providers {
provider(AuthProvider.Email())
}
theme = adaptiveTheme // Set theme here
}
FirebaseAuthScreen(
configuration = configuration,
onSignInSuccess = { /* ... */ }
)
When to use: This is the recommended pattern for most use cases. It's simple and explicit.
You can also wrap FirebaseAuthScreen with AuthUITheme:
val adaptiveTheme = AuthUITheme.Adaptive
val configuration = authUIConfiguration {
context = applicationContext
providers {
provider(AuthProvider.Email())
}
theme = adaptiveTheme // Theme in configuration
}
AuthUITheme(theme = adaptiveTheme) { // Optional wrapper
Surface(color = MaterialTheme.colorScheme.background) {
FirebaseAuthScreen(
configuration = configuration,
onSignInSuccess = { /* ... */ }
)
}
}
When to use: Use this pattern when you have UI elements outside of FirebaseAuthScreen that need to share the same theme.
Understanding which theme applies is important:
Configuration theme takes precedence:
val configuration = authUIConfiguration {
context = applicationContext
providers { provider(AuthProvider.Email()) }
theme = AuthUITheme.Default // LIGHT theme
}
AuthUITheme(theme = AuthUITheme.DefaultDark) { // DARK wrapper
FirebaseAuthScreen(configuration, ...)
}
// Result: FirebaseAuthScreen uses LIGHT theme (from configuration)
Wrapper as fallback:
val configuration = authUIConfiguration {
context = applicationContext
providers { provider(AuthProvider.Email()) }
// theme not specified (null)
}
AuthUITheme(theme = AuthUITheme.DefaultDark) { // DARK wrapper
FirebaseAuthScreen(configuration, ...)
}
// Result: FirebaseAuthScreen inherits DARK theme from wrapper
Ultimate fallback:
val configuration = authUIConfiguration {
context = applicationContext
providers { provider(AuthProvider.Email()) }
// theme not specified (null)
}
FirebaseAuthScreen(configuration, ...) // No wrapper
// Result: Uses AuthUITheme.Default (light theme)
Best Practice: For clarity and consistency, always set theme in authUIConfiguration. Use the wrapper only if you have additional UI outside FirebaseAuthScreen.
Use fromMaterialTheme() to automatically inherit your app's Material Design theme:
@Composable
fun App() {
MyAppTheme { // Your existing Material3 theme
val localContext = LocalContext.current
val authTheme = AuthUITheme.fromMaterialTheme() // Inherits colors, typography, shapes
val configuration = remember(localContext, authTheme) {
authUIConfiguration {
context = localContext
providers {
provider(AuthProvider.Email())
}
theme = authTheme
}
}
FirebaseAuthScreen(
configuration = configuration,
onSignInSuccess = { /* ... */ }
)
}
}
You can also customize while inheriting:
val authTheme = AuthUITheme.fromMaterialTheme(
providerButtonShape = RoundedCornerShape(16.dp) // Override button shape
)
val configuration = authUIConfiguration {
context = applicationContext
providers {
provider(AuthProvider.Google())
provider(AuthProvider.Facebook())
}
theme = authTheme
}
Build a theme from scratch with full control:
val customTheme = AuthUITheme(
colorScheme = darkColorScheme(
primary = Color(0xFF6200EE),
onPrimary = Color.White,
primaryContainer = Color(0xFF3700B3),
secondary = Color(0xFF03DAC6)
),
typography = Typography(
displayLarge = TextStyle(fontSize = 57.sp, fontWeight = FontWeight.Bold),
bodyLarge = TextStyle(fontSize = 16.sp)
),
shapes = Shapes(
small = RoundedCornerShape(4.dp),
medium = RoundedCornerShape(8.dp),
large = RoundedCornerShape(16.dp)
),
providerButtonShape = RoundedCornerShape(12.dp)
)
val configuration = authUIConfiguration {
context = applicationContext
providers {
provider(AuthProvider.Email())
}
theme = customTheme
}
Option 1: Using .copy() on default theme:
val customTheme = AuthUITheme.Default.copy(
providerButtonShape = RoundedCornerShape(12.dp) // Applies to all provider buttons
)
val configuration = authUIConfiguration {
context = applicationContext
providers {
provider(AuthProvider.Google())
provider(AuthProvider.Facebook())
provider(AuthProvider.Email())
}
theme = customTheme
}
Option 2: Using fromMaterialTheme():
val authTheme = AuthUITheme.fromMaterialTheme(
providerButtonShape = RoundedCornerShape(16.dp)
)
val configuration = authUIConfiguration {
context = applicationContext
providers {
provider(AuthProvider.Google())
provider(AuthProvider.Facebook())
}
theme = authTheme
}
Option 3: Creating custom theme:
val customTheme = AuthUITheme(
colorScheme = MaterialTheme.colorScheme,
typography = MaterialTheme.typography,
shapes = MaterialTheme.shapes,
providerButtonShape = RoundedCornerShape(12.dp)
)
val configuration = authUIConfiguration {
context = applicationContext
providers {
provider(AuthProvider.Google())
provider(AuthProvider.Facebook())
provider(AuthProvider.Email())
}
theme = customTheme
}
Customize specific provider buttons using the pre-defined ProviderStyleDefaults constants:
Using .copy() with default theme:
val customProviderStyles = mapOf(
"google.com" to ProviderStyleDefaults.Google.copy(
shape = RoundedCornerShape(8.dp),
elevation = 4.dp
),
"facebook.com" to ProviderStyleDefaults.Facebook.copy(
shape = RoundedCornerShape(24.dp),
elevation = 0.dp
)
)
val customTheme = AuthUITheme.Default.copy(
providerButtonShape = RoundedCornerShape(12.dp), // Default for all
providerStyles = customProviderStyles // Specific overrides
)
val configuration = authUIConfiguration {
context = applicationContext
providers {
provider(AuthProvider.Google())
provider(AuthProvider.Facebook())
}
theme = customTheme
}
Using fromMaterialTheme():
val customProviderStyles = mapOf(
"google.com" to ProviderStyleDefaults.Google.copy(
shape = RoundedCornerShape(8.dp),
elevation = 4.dp
)
)
val authTheme = AuthUITheme.fromMaterialTheme(
providerButtonShape = RoundedCornerShape(12.dp),
providerStyles = customProviderStyles
)
val configuration = authUIConfiguration {
context = applicationContext
providers {
provider(AuthProvider.Google())
provider(AuthProvider.Facebook())
}
theme = authTheme
}
Real-world example combining global and per-provider customizations:
// Define custom styles for specific providers
val customProviderStyles = mapOf(
"google.com" to ProviderStyleDefaults.Google.copy(
shape = RoundedCornerShape(24.dp), // Pill-shaped Google button
elevation = 6.dp
),
"facebook.com" to ProviderStyleDefaults.Facebook.copy(
shape = RoundedCornerShape(8.dp), // Medium rounded Facebook button
elevation = 0.dp // Flat design
)
// Email provider will use the global providerButtonShape
)
// Customize default theme with global button shape and per-provider styles
val customTheme = AuthUITheme.Default.copy(
providerButtonShape = RoundedCornerShape(12.dp), // Global default for all buttons
providerStyles = customProviderStyles // Specific overrides
)
val configuration = authUIConfiguration {
context = applicationContext
providers {
provider(AuthProvider.Google()) // Uses custom shape (24.dp)
provider(AuthProvider.Facebook()) // Uses custom shape (8.dp)
provider(AuthProvider.Email()) // Uses global shape (12.dp)
}
theme = customTheme
}
Override the colors used by the top app bar shown on auth screens:
val customTheme = AuthUITheme.Default.copy(
topAppBarColors = TopAppBarDefaults.topAppBarColors(
containerColor = Color(0xFF2E7D32),
scrolledContainerColor = Color(0xFF2E7D32),
)
)
val configuration = authUIConfiguration {
context = applicationContext
providers { provider(AuthProvider.Email()) }
theme = customTheme
}
If left unset (null), the top app bar falls back to colors derived from colorScheme's primary/onPrimary.
Customize the animations when navigating between screens using the AuthUITransitions object.
Each spec is an AnimatedContentTransitionScope<Scene<NavKey>> receiver returning one
ContentTransform, so the enter and exit halves are paired with togetherWith:
Slide animations:
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.togetherWith
import com.firebase.ui.auth.configuration.AuthUITransitions
val configuration = authUIConfiguration {
context = applicationContext
providers {
provider(AuthProvider.Email())
provider(AuthProvider.Google())
}
transitions = AuthUITransitions(
// Slide in from right, slide out to left
transitionSpec = { slideInHorizontally { it } togetherWith slideOutHorizontally { -it } },
// Slide in from left, slide out to right
popTransitionSpec = { slideInHorizontally { -it } togetherWith slideOutHorizontally { it } },
// Predictive back falls back to the default cross-fade if left unset, so mirror the pop
predictivePopTransitionSpec = {
slideInHorizontally { -it } togetherWith slideOutHorizontally { it }
}
)
}
Fade animations (default):
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
import com.firebase.ui.auth.configuration.AuthUITransitions
val configuration = authUIConfiguration {
context = applicationContext
providers {
provider(AuthProvider.Phone())
}
transitions = AuthUITransitions(
transitionSpec = { fadeIn() togetherWith fadeOut() },
popTransitionSpec = { fadeIn() togetherWith fadeOut() },
predictivePopTransitionSpec = { fadeIn() togetherWith fadeOut() }
)
}
Scale animations:
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.animation.togetherWith
import com.firebase.ui.auth.configuration.AuthUITransitions
val configuration = authUIConfiguration {
context = applicationContext
providers {
provider(AuthProvider.Facebook())
}
transitions = AuthUITransitions(
transitionSpec = {
fadeIn() + scaleIn(initialScale = 0.9f) togetherWith
fadeOut() + scaleOut(targetScale = 0.9f)
},
popTransitionSpec = {
fadeIn() + scaleIn(initialScale = 0.9f) togetherWith
fadeOut() + scaleOut(targetScale = 0.9f)
}
)
}
Vertical slide:
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith
import com.firebase.ui.auth.configuration.AuthUITransitions
val configuration = authUIConfiguration {
context = applicationContext
providers {
provider(AuthProvider.Email())
}
transitions = AuthUITransitions(
// A vertical push: the new step rises from the bottom as the old one leaves via the top
transitionSpec = { slideInVertically { it } togetherWith slideOutVertically { -it } }
)
}
Per-destination animations:
Read authRoute() off initialState / targetState to vary the animation by the screen being
navigated to or from:
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.togetherWith
import com.firebase.ui.auth.configuration.AuthUITransitions
import com.firebase.ui.auth.ui.screens.AuthRoute
import com.firebase.ui.auth.ui.screens.authRoute
val configuration = authUIConfiguration {
context = applicationContext
providers {
provider(AuthProvider.Email())
}
transitions = AuthUITransitions(
transitionSpec = {
if (targetState.authRoute() is AuthRoute.Success) {
fadeIn() togetherWith fadeOut()
} else {
slideInHorizontally { it } togetherWith slideOutHorizontally { -it }
}
}
)
}
Note: Each spec is independent. Any one left unset falls back to the library's default 700ms cross-fade —
predictivePopTransitionSpecincluded, which does not fall back topopTransitionSpec.predictivePopTransitionSpecalso receives the swipe edge (NavigationEvent.EDGE_LEFT,EDGE_RIGHTorEDGE_NONE) and runs when the gesture starts, so a side effect placed in it fires even for gestures the user goes on to cancel.
Seamlessly upgrade anonymous users to permanent accounts:
// 1. Configure anonymous authentication with upgrade enabled
val configuration = authUIConfiguration {
context = applicationContext
providers {
provider(AuthProvider.Anonymous())
provider(AuthProvider.Email())
provider(AuthProvider.Google())
}
isAnonymousUpgradeEnabled = true
}
// 2. When user wants to create a permanent account, show auth UI
// The library automatically upgrades the anonymous account if one exists
FirebaseAuthScreen(
configuration = configuration,
onSignInSuccess = { result ->
// Anonymous account has been upgraded (if user was anonymous)!
Toast.makeText(this, "Account created!", Toast.LENGTH_SHORT).show()
}
)
Enable passwordless email link authentication:
val emailProvider = AuthProvider.Email(
isEmailLinkSignInEnabled = true,
emailLinkActionCodeSettings = actionCodeSettings {
url = "https://example.com/auth"
handleCodeInApp = true
setAndroidPackageName(packageName, true, "12")
},
passwordValidationRules = emptyList()
)
val configuration = authUIConfiguration {
context = applicationContext
providers {
provider(emailProvider)
}
}
High-Level API - Direct FirebaseAuthScreen usage:
// In your Activity that handles the deep link:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val authUI = FirebaseAuthUI.getInstance()
val emailLink = if (authUI.canHandleIntent(intent)) {
intent.data?.toString()
} else {
null
}
if (emailLink != null) {
setContent {
FirebaseAuthScreen(
configuration = configuration,
emailLink = emailLink,
onSignInSuccess = { result ->
// Email link sign-in successful
},
onSignInFailure = { exception ->
// Handle error
},
onSignInCancelled = {
// User backed out of a single provider; the screen already
// returns to the method picker on its own.
}
)
}
}
}
Low-Level API - Using AuthFlowController:
import com.firebase.ui.auth.util.EmailLinkConstants
// In your Activity that handles the deep link:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val authUI = FirebaseAuthUI.getInstance()
val emailLink = if (authUI.canHandleIntent(intent)) {
intent.data?.toString()
} else {
null
}
if (emailLink != null) {
val controller = authUI.createAuthFlow(configuration)
val intent = controller.createIntent(this).apply {
putExtra(EmailLinkConstants.EXTRA_EMAIL_LINK, emailLink)
}
authLauncher.launch(intent)
}
}
// Handle result
private val authLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
when (result.resultCode) {
Activity.RESULT_OK -> {
// Email link sign-in successful
}
Activity.RESULT_CANCELED -> {
// Handle error or cancellation
}
}
}
Add the intent filter to your AndroidManifest.xml:
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="https"
android:host="example.com"
android:pathPrefix="/auth" />
</intent-filter>
Enforce custom password requirements:
val emailProvider = AuthProvider.Email(
emailLinkActionCodeSettings = null,
minimumPasswordLength = 10,
passwordValidationRules = listOf(
PasswordRule.MinimumLength(10),
PasswordRule.RequireUppercase,
PasswordRule.RequireLowercase,
PasswordRule.RequireDigit,
PasswordRule.RequireSpecialCharacter,
PasswordRule.Custom(
regex = Regex("^(?!.*password).*$"),
errorMessage = "Password cannot contain the word 'password'"
)
)
)
FirebaseUI automatically integrates with Android's Credential Manager API to save and retrieve credentials. This enables:
Credential Manager is enabled by default. To disable:
val configuration = authUIConfiguration {
context = applicationContext
providers {
provider(AuthProvider.Email())
}
isCredentialManagerEnabled = false
}
Every input and button on the auth screens carries a stable, public test tag, and FirebaseUI exposes those tags as Android resource ids automatically — no setup required in your app. This is what lets Firebase Test Lab's Robo test and the Google Play Console's pre-launch report drive a real sign-in during automated testing, instead of typing into the wrong field or getting stuck on a screen it can't navigate.
Why this matters: a crawler that can't tell which field is the password will happily type a username into it, then hammer "sign in" and "forgot password" until your test account is buried in reset emails. Every field and button below resolves to one unambiguous resource id, so a crawler — or your own instrumented test — can target it directly.
Tag reference. Tags are grouped by screen; import com.firebase.ui.auth.ui.FirebaseAuthTestTags.
| Screen | Constant | Resource id |
|---|---|---|
| Sign in | SignIn.EMAIL_FIELD |
fui_sign_in_email_field |
SignIn.PASSWORD_FIELD |
fui_sign_in_password_field |
|
SignIn.SIGN_IN_BUTTON |
fui_sign_in_sign_in_button |
|
SignIn.SIGN_UP_BUTTON |
fui_sign_in_sign_up_button |
|
SignIn.FORGOT_PASSWORD_BUTTON |
fui_sign_in_forgot_password_button |
|
SignIn.EMAIL_LINK_BUTTON |
fui_sign_in_email_link_button |
|
| Sign up | SignUp.NAME_FIELD |
fui_sign_up_name_field |
SignUp.EMAIL_FIELD |
fui_sign_up_email_field |
|
SignUp.PASSWORD_FIELD |
fui_sign_up_password_field |
|
SignUp.CONFIRM_PASSWORD_FIELD |
fui_sign_up_confirm_password_field |
|
SignUp.SIGN_UP_BUTTON |
fui_sign_up_sign_up_button |
|
| Password recovery | ResetPassword.EMAIL_FIELD |
fui_reset_password_email_field |
ResetPassword.SEND_BUTTON |
fui_reset_password_send_button |
|
ResetPassword.DISMISS_BUTTON |
fui_reset_password_dismiss_button |
|
| Email link sign-in | EmailLink.EMAIL_FIELD |
fui_email_link_email_field |
EmailLink.SEND_LINK_BUTTON |
fui_email_link_send_link_button |
|
EmailLink.DISMISS_BUTTON |
fui_email_link_dismiss_button |
|
| Phone number entry | PhoneNumber.PHONE_NUMBER_FIELD |
fui_phone_number_phone_number_field |
PhoneNumber.COUNTRY_SELECTOR_BUTTON |
fui_phone_number_country_selector_button |
|
PhoneNumber.SEND_CODE_BUTTON |
fui_phone_number_send_code_button |
|
| SMS verification | VerificationCode.CODE_FIELD |
fui_verification_code_code_field |
VerificationCode.VERIFY_BUTTON |
fui_verification_code_verify_button |
|
VerificationCode.RESEND_CODE_BUTTON |
fui_verification_code_resend_code_button |
|
VerificationCode.CHANGE_PHONE_NUMBER_BUTTON |
fui_verification_code_change_phone_number_button |
|
| MFA sign-in challenge | MfaChallenge.CODE_FIELD |
fui_mfa_challenge_code_field |
MfaChallenge.VERIFY_BUTTON |
fui_mfa_challenge_verify_button |
|
| Re-authentication | Reauth.PASSWORD_FIELD |
fui_reauth_password_field |
Reauth.VERIFY_BUTTON |
fui_reauth_verify_button |
|
Reauth.DISMISS_BUTTON |
fui_reauth_dismiss_button |
|
| Method picker | MethodPicker.PROVIDER_LIST |
fui_method_picker_provider_list |
MethodPicker.CONTINUE_AS_BUTTON |
fui_method_picker_continue_as_button |
|
| Country selector | CountrySelector.COUNTRY_LIST |
fui_country_selector_country_list |
VerificationCode.CODE_FIELD and MfaChallenge.CODE_FIELD each name the whole six-digit input rather than an individual digit box: the field accepts a complete code in a single ACTION_SET_TEXT/performTextInput call and distributes it across the digit boxes, so one Robo directive or one performTextInput("123456") types the entire code.
In your own instrumented tests, target these the same way you'd target any other tag:
composeTestRule
.onNodeWithTag(FirebaseAuthTestTags.SignIn.EMAIL_FIELD)
.performTextInput("test@example.com")
composeTestRule
.onNodeWithTag(FirebaseAuthTestTags.SignIn.PASSWORD_FIELD)
.performTextInput("correcthorsebatterystaple")
composeTestRule
.onNodeWithTag(FirebaseAuthTestTags.SignIn.SIGN_IN_BUTTON)
.performClick()
Or with UiAutomator, by resource name:
device.findObject(By.res("fui_sign_in_email_field")).text = "test@example.com"
With Firebase Test Lab. Pass the resource ids as Robo directives so the crawler fills real values instead of guessing:
gcloud firebase test android run \
--type=robo \
--app=app-debug.apk \
--robo-directives=fui_sign_in_email_field=test@example.com,fui_sign_in_password_field=correcthorsebatterystaple \
--device model=MediumPhone.arm,version=34
This is exactly the mechanism a Play Console pre-launch report uses, under Test and release → Testing → Pre-launch report → Settings → Test account credentials; the resource ids above are what you enter there for the username and password fields.
Verified with a real Firebase Test Lab Robo run against the sign-in screen (August 2026): the crawler resolved fui_sign_in_email_field and fui_sign_in_password_field as android.widget.EditText nodes, typed the directive values into both, and submitted via fui_sign_in_sign_in_button — along the way also navigating by resource id through sign-up, password recovery, and phone entry, confirming the tagging works generally rather than only where a directive points. Robo's crawling behavior is Google's, not ours, and can change independently of this library; treat this as a snapshot of current behavior rather than a permanent guarantee.
Renaming or removing a tag, or changing the resource id it resolves to, is a breaking change to FirebaseUI's public API — not an internal detail — so a value documented here will not change without a major version bump.
Sign Out:
@Composable
fun SettingsScreen() {
val scope = rememberCoroutineScope()
val context = LocalContext.current
val authUI = remember { FirebaseAuthUI.getInstance() }
Button(
onClick = {
scope.launch {
authUI.signOut(context)
// User is signed out, navigate to auth screen
navigateToAuth()
}
}
) {
Text("Sign Out")
}
}
Delete Account:
Button(
onClick = {
lifecycleScope.launch {
try {
authUI.delete(context)
// Account deleted successfully
navigateToAuth()
} catch (e: Exception) {
when (e) {
is FirebaseAuthRecentLoginRequiredException -> {
// User needs to reauthenticate
showReauthenticationDialog()
}
else -> {
showError("Failed to delete account: ${e.message}")
}
}
}
}
}
) {
Text("Delete Account")
}
FirebaseUI includes default English strings. To add custom localization:
// AuthUIStringProvider declares ~170 abstract `val`s, so override properties, not functions,
// and expect to supply every one — DefaultAuthUIStringProvider is final and cannot be subclassed.
// For most apps, translating the library's own string resources is the lighter option.
class SpanishStringProvider : AuthUIStringProvider {
override val signInWithEmail = "Iniciar sesión con correo"
override val signInWithGoogle = "Iniciar sesión con Google"
override val invalidEmailAddress = "Correo inválido"
// ... every other member of AuthUIStringProvider
}
val configuration = authUIConfiguration {
context = applicationContext
providers {
provider(AuthProvider.Email())
}
stringProvider = SpanishStringProvider(context)
locale = Locale("es", "ES")
}
Or override individual strings in your strings.xml:
<resources>
<!-- Override FirebaseUI strings -->
<string name="fui_sign_in_with_google">Sign in with Google</string>
<string name="fui_sign_in_with_email">Sign in with Email</string>
<string name="fui_invalid_email_address">Invalid email address</string>
<!-- See auth/src/main/res/values/strings.xml for all available strings -->
</resources>
FirebaseUI provides a comprehensive exception hierarchy:
FirebaseAuthScreen(
configuration = configuration,
onSignInFailure = { exception ->
when (exception) {
is AuthException.NetworkException -> {
showSnackbar("No internet connection. Please check your network.")
}
is AuthException.InvalidCredentialsException -> {
showSnackbar("Invalid email or password.")
}
is AuthException.UserNotFoundException -> {
showSnackbar("No account found with this email.")
}
is AuthException.WeakPasswordException -> {
showSnackbar("Password is too weak. Please use a stronger password.")
}
is AuthException.EmailAlreadyInUseException -> {
showSnackbar("An account already exists with this email.")
}
is AuthException.TooManyRequestsException -> {
showSnackbar("Too many attempts. Please try again later.")
}
is AuthException.MfaRequiredException -> {
// Handled automatically by FirebaseAuthScreen
// or show custom MFA challenge
}
is AuthException.AccountLinkingRequiredException -> {
// Account needs to be linked
showAccountLinkingDialog(exception)
}
is AuthException.AuthCancelledException -> {
// User cancelled the flow
navigateBack()
}
is AuthException.UnknownException -> {
showSnackbar("An unexpected error occurred: ${exception.message}")
Log.e(TAG, "Auth error", exception)
}
}
}
)
Use the ErrorRecoveryDialog for automatic error handling:
var errorState by remember { mutableStateOf<AuthException?>(null) }
errorState?.let { error ->
ErrorRecoveryDialog(
error = error,
stringProvider = DefaultAuthUIStringProvider(LocalContext.current),
onRetry = {
// Retry the authentication
errorState = null
retryAuthentication()
},
onDismiss = {
errorState = null
},
onRecover = { exception ->
// Custom recovery logic for specific errors
when (exception) {
is AuthException.AccountLinkingRequiredException -> {
linkAccounts(exception)
}
}
}
)
}
Migrating from 9.x? docs/upgrade-to-10.0.md is the full guide — dependencies, provider configuration, theming, sign-out and deletion, auth-state observation, and the Activity-based route for apps that can't use Compose everywhere.
Contributions are welcome! Please read our contribution guidelines before submitting PRs.
FirebaseUI Auth is available under the Apache 2.0 license.