FirebaseUI for Auth

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:

  • Simple API - Choose between high-level screens or low-level controllers for maximum flexibility
  • 12+ Authentication Methods - Email/Password, Phone, Google, Facebook, Twitter, GitHub, Microsoft, Yahoo, Apple, Anonymous, and custom OAuth providers
  • Multi-Factor Authentication - SMS and TOTP (Time-based One-Time Password)
  • Android Credential Manager - Automatic credential saving and one-tap sign-in
  • Material Design 3 - Beautiful, themeable UI components that integrate seamlessly with your app
  • Localization Support - Customizable strings for internationalization
  • Security Best Practices - Email verification, reauthentication, account linking, and more

Equivalent FirebaseUI libraries are available for iOS and Web.

Demo

FirebaseUI Compose Demo

Setup

Prerequisites

Ensure your application is configured for use with Firebase. See the Firebase documentation for setup instructions.

Minimum Requirements:

  • Android SDK 23+ (Android 6.0 Marshmallow)
  • Kotlin 2.0+
  • Jetpack Compose
  • Firebase BoM 34.0.0+

Installation

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
    }
}

Provider Configuration

Google Sign-In

Google Sign-In configuration is automatically provided by the google-services Gradle plugin. Ensure you have enabled Google Sign-In in the Firebase Console.

Facebook Login

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.

Other Providers

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.

Quick Start

Minimal Example

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:

  • ✅ Email/password sign-in and sign-up
  • ✅ Google Sign-In
  • ✅ Password reset
  • ✅ Display name collection
  • ✅ Credential Manager integration
  • ✅ Material Design 3 theming
  • ✅ Error handling

Check Authentication State

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(/* ... */)
        }
    }
}

Core Concepts

FirebaseAuthUI

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

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

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

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()
}

Authentication Methods

Email & Password

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) }
}

Phone Number

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)
    }
}

Google Sign-In

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)
    }
}

Facebook Login

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)
    }
}

Other OAuth Providers

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)
    }
}

Anonymous Authentication

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
}

Custom OAuth Provider

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)
    }
}

Usage Patterns

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")
            }
        }
    }
)

Low-Level API (Advanced)

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()
    }
}

Custom UI with Slots

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
}

Method picker (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,
)

Email (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 -> { /* ... */ }
    }
}

Phone (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") }
                }
            }
        }
    }
}

MFA enrollment (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 -> { /* ... */ }
    }
}

MFA challenge (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") }
    }
}

Reauthentication (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.

Reauthentication

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:

  1. Runs the operation.
  2. If FirebaseAuthRecentLoginRequiredException is thrown, emits AuthState.Reauthentication.Required with the retry attached.
  3. FirebaseAuthScreen shows the reauth UI scoped to the user's linked providers, including the MFA challenge when the account has a second factor enrolled.
  4. On successful reauthentication, retries the operation automatically and emits 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)

Multi-Factor Authentication

MFA Configuration

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
}

MFA Enrollment

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)
        }
    }
}

MFA Challenge

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) }
    )
}

Theming & Customization

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 Theme
  • Custom theme - Full control over colors, typography, shapes, and provider button styles

Using Default Themes

FirebaseUI 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.

Customizing Default Theme

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 = { /* ... */ }
    )
}

Theme Behavior & Patterns

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.

Pattern 2: Theme in Wrapper (Optional)

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.

Theme Precedence Rules

Understanding which theme applies is important:

  1. 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)
    
  2. 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
    
  3. 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.

Inheriting from Material Theme

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
}

Creating a Completely Custom Theme

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
}

Provider Button Styling

Setting shapes for all provider buttons

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
}

Customizing individual provider buttons

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
}

Complete customization example

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
}

Customizing the Top App Bar

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.

Screen Transitions

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 — predictivePopTransitionSpec included, which does not fall back to popTransitionSpec. predictivePopTransitionSpec also receives the swipe edge (NavigationEvent.EDGE_LEFT, EDGE_RIGHT or EDGE_NONE) and runs when the gesture starts, so a side effect placed in it fires even for gestures the user goes on to cancel.

Advanced Features

Anonymous User Upgrade

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>

Password Validation Rules

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'"
        )
    )
)

Credential Manager Integration

FirebaseUI automatically integrates with Android's Credential Manager API to save and retrieve credentials. This enables:

  • Automatic sign-in for returning users
  • One-tap sign-in across apps
  • Secure credential storage

Credential Manager is enabled by default. To disable:

val configuration = authUIConfiguration {
    context = applicationContext
    providers {
        provider(AuthProvider.Email())
    }
    isCredentialManagerEnabled = false
}

Automated Testing (Firebase Test Lab & Robo)

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 & Account Deletion

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")
}

Localization

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>

Error Handling

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)
                }
            }
        }
    )
}

Migration Guide

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.


Contributing

Contributions are welcome! Please read our contribution guidelines before submitting PRs.

License

FirebaseUI Auth is available under the Apache 2.0 license.