Skip to content

feat(auth): reshape reauthContent into a ReauthContentState content slot - #2452

Open
demolaf wants to merge 5 commits into
version-10.0.0-beta05from
feat/reauth-content-state-slot
Open

feat(auth): reshape reauthContent into a ReauthContentState content slot#2452
demolaf wants to merge 5 commits into
version-10.0.0-beta05from
feat/reauth-content-state-slot

Conversation

@demolaf

@demolaf demolaf commented Aug 24, 2026

Copy link
Copy Markdown
Member

reauthContent was documented and shaped as a content slot alongside emailContent, phoneContent and the MFA slots, but it received only (AuthState.ReauthenticationRequired, onDismiss) and every API needed to build a reauthentication UI — filterToLinkedProviders, isReauthenticationMode, the federated provider driver — was internal or private. A custom slot could therefore only perform email/password reauthentication and had to dead-end for a Google- or OAuth-only account. The success handoff was also easy to get wrong: onDismiss reset auth state to Idle while retryOperation emitted AuthState.Success, so both orderings a caller would naturally reach either clobbered the success or cancelled the scope the retry ran in, silently dropping the sensitive operation.

reauthContent now receives a single ReauthContentState carrying the user, the reason, the providers already filtered to those linked to that user, and callbacks to select a provider or dismiss. The caller renders a provider chooser; the library owns credential exchange and dismiss/retry sequencing. Selecting AuthProvider.Email or AuthProvider.Phone hands off to the library's own sub-flow, honouring the caller's emailContent / phoneContent, and an MFA-enrolled user now completes the second factor inside the reauth surface rather than having the challenge render beneath it.

Reauthentication is a request-scoped state machine rather than seven independently mutable Compose holders. AuthState.Reauthentication carries a requestId and the pending operation; proof of reauthentication is a reauthenticatedUid stamped on AuthState.Success at the three credential-exchange sites, and the pending operation is consumed only for a library-published success on that same uid, exactly once. Activity recreation resumes the same request; process death reports an interruption rather than dropping it.

⚠️ Breaking changes

  • reauthContent takes a single ReauthContentState instead of (state, onDismiss).
  • AuthState.Success can no longer be constructed outside the library. It records which uid a reauthentication re-proved, and that proof must not be forgeable by app code.
  • AuthState.ReauthenticationRequired is now AuthState.Reauthentication.Required, nested with the other reauthentication phases under a new public AuthState.Reauthentication sealed class.
  • ReauthContentState moves to com.firebase.ui.auth.ui.screens.reauth.
  • MfaChallengeScreen and MfaEnrollmentScreen move to com.firebase.ui.auth.ui.screens.mfa.

While a reauthentication is in progress, authStateFlow() and AuthFlowController.state() emit AuthState.Reauthentication phases, so is AuthState.Error, is AuthState.Loading and is AuthState.Cancelled do not match for that window. The outcome is published as an ordinary state once the request completes. This is documented in auth/README.md.

  • ReauthContentState.kt: new public state holder, following the MfaEnrollmentContentState conventions.
  • AuthState.kt: Success gains reauthenticatedUid and an internal constructor; the reauthentication phases become a nested sealed hierarchy keyed by requestId.
  • FirebaseAuthUI.kt: one guarded transition entry point plus session start/finish; ordinary states are folded into reauthentication phases only while a screen is registered to drain them, so an arming created by public API with no screen composed stays inert.
  • FirebaseAuthScreen.kt: the linked-provider list reaches the slot instead of being discarded; provider selection, error-dialog recovery, deep links and the non-terminal navigation branches are inert while a reauthentication is armed.
  • EmailAuthProvider+FirebaseAuthUI.kt, OAuthProvider+FirebaseAuthUI.kt: stamp reauthenticatedUid where the reauthenticated identity is known; account creation and credential linking are rejected in reauthentication mode.
  • SignInUI.kt: sign-up, password recovery and email-link sign-in are not offered while reauthenticating, and Credential Manager autofill is skipped so a saved password for another account cannot be auto-submitted.
  • ui/screens/reauth/, ui/screens/mfa/: reauthentication and MFA UI extracted into their own packages, mirroring the existing ui/screens/email/ and ui/screens/phone/.

Added FirebaseAuthScreenReauthContentStateTest, EmailAuthScreenReauthEmailLockTest and coverage across FirebaseAuthUIAuthStateTest, plus e2e coverage of reauthentication through the slot — every new test verified to be load-bearing by temporarily reverting the fix and confirming it fails.

Usage

FirebaseAuthScreen(
    configuration = configuration,
    onSignInSuccess = { },
    onSignInFailure = { },
    onSignInCancelled = { },
    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") } },
        )
    },
)

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a robust reauthentication flow in FirebaseUI Auth for Android, separating operation-level cancellations (AuthState.Cancelled) from flow-level aborts (AuthState.Aborted). It adds support for a custom, stateless reauthContent slot in FirebaseAuthScreen while keeping credential exchanges owned by the library, locks the email field to read-only during reauthentication, and resolves several state-resetting edge cases. The review feedback suggests making the OAuth reauthentication path more robust and fail-fast by explicitly throwing an exception if auth.currentUser is unexpectedly null, rather than silently failing with a safe call.

@demolaf
demolaf force-pushed the feat/reauth-content-state-slot branch 2 times, most recently from 5dfbe74 to 81b3b32 Compare August 25, 2026 00:38
@demolaf
demolaf changed the base branch from version-10.0.0-beta04-old to version-10.0.0-beta04 August 25, 2026 00:42
@demolaf
demolaf marked this pull request as ready for review August 25, 2026 09:22
@demolaf
demolaf force-pushed the feat/reauth-content-state-slot branch 2 times, most recently from 1e1858f to 82c68c0 Compare August 25, 2026 09:36
@demolaf
demolaf marked this pull request as draft August 25, 2026 14:10
@demolaf
demolaf changed the base branch from version-10.0.0-beta04 to version-10.0.0-beta05 August 26, 2026 14:29
@demolaf
demolaf marked this pull request as ready for review August 26, 2026 14:30
@demolaf
demolaf force-pushed the feat/reauth-content-state-slot branch 3 times, most recently from 692a7a5 to f05a83c Compare August 28, 2026 09:45

@russellwheatley russellwheatley left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two suggestions on the reauth config copy and state survival across rotation, nothing blocking otherwise. The uid-matching consumption logic, the internal AuthState.Success constructor, and the inert-while-armed gating all check out.

reauthSubRoute.value = null
reauthError.value = null
pendingReauthState.value = state
pendingReauthConfig.value = configuration.copy(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This copy() only overrides providers/isNewEmailAccountsAllowed/isReauthenticationMode, so isCredentialLinkingEnabled stays whatever the app set at the top level. If it's true, signInAndLinkWithCredential takes the linkWithCredential branch instead of reauthenticate for a credential that's already linked (filterToLinkedProviders only offers linked ones), which either gets rejected by Firebase or succeeds without actually satisfying the recent-login guarantee this flow exists for. Not covered by a test that combines isCredentialLinkingEnabled with isReauthenticationMode. Might be worth forcing isCredentialLinkingEnabled = false (and isAnonymousUpgradeEnabled = false for symmetry) here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed, canLinkCredential now requires !config.isReauthenticationMode, so a reauthentication can never use linkWithCredential. Left isAnonymousUpgradeEnabled alone — anonymous users have no linked providers, so it's unreachable.

Comment thread auth/src/main/java/com/firebase/ui/auth/ui/screens/FirebaseAuthScreen.kt Outdated
@demolaf
demolaf force-pushed the feat/reauth-content-state-slot branch from 4ac3329 to 0469224 Compare August 31, 2026 11:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants