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

Merged
demolaf merged 8 commits into
version-10.0.0-beta05from
feat/reauth-content-state-slot
Sep 1, 2026
Merged

feat(auth): reshape reauthContent into a ReauthContentState content slot#2452
demolaf merged 8 commits into
version-10.0.0-beta05from
feat/reauth-content-state-slot

Conversation

@demolaf

@demolafdemolaf 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-assistgemini-code-assistBot 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
demolafforce-pushed the feat/reauth-content-state-slot branch 2 times, most recently from 5dfbe74 to 81b3b32CompareAugust 25, 2026 00:38
@demolaf
demolaf changed the base branch from version-10.0.0-beta04-old to version-10.0.0-beta04August 25, 2026 00:42
@demolaf
demolaf marked this pull request as ready for review August 25, 2026 09:22
@demolaf
demolafforce-pushed the feat/reauth-content-state-slot branch 2 times, most recently from 1e1858f to 82c68c0CompareAugust 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-beta05August 26, 2026 14:29
@demolaf
demolaf marked this pull request as ready for review August 26, 2026 14:30
@demolaf
demolafforce-pushed the feat/reauth-content-state-slot branch 3 times, most recently from 692a7a5 to f05a83cCompareAugust 28, 2026 09:45

@russellwheatleyrussellwheatley 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.

Comment threadauth/src/main/java/com/firebase/ui/auth/ui/screens/FirebaseAuthScreen.kt Outdated
Comment threadauth/src/main/java/com/firebase/ui/auth/ui/screens/FirebaseAuthScreen.kt Outdated
@demolaf
demolafforce-pushed the feat/reauth-content-state-slot branch from 4ac3329 to 0469224CompareAugust 31, 2026 11:36

@russellwheatleyrussellwheatley 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.

Re-reviewed after the state-machine refactor and the two follow-up fixes. Both issues from the last round are fixed properly, not just carried over (isCredentialLinkingEnabled/isAnonymousUpgradeEnabled reset now applies at both reauth-config construction sites, and the rememberSaveable gap turned into a proper ReauthPresentationState + Saver that reconciles correctly on both rotation and process death). Two minor items left, neither blocking on their own, flagging so they don't get lost:

  • inline comment below on AuthState.Success's constructor going internal
  • e2eTest/src/test/java/com/firebase/ui/auth/ui/screens/ReauthFlowTest.kt has no dedicated case for sign-out-during-retry or phone-verification-teardown. Both are already covered at the unit/screen level (FirebaseAuthScreenReauthIdleResetTest.kt, PhoneAuthScreenVerificationLifecycleTest.kt), so this is more of a nice-to-have than something I'd hold the PR on.

Comment threadauth/src/main/java/com/firebase/ui/auth/AuthState.kt
…/reauth-content-state-slot
# Conflicts:
#	auth/src/main/java/com/firebase/ui/auth/ui/components/AuthTextField.kt
#	auth/src/main/java/com/firebase/ui/auth/ui/components/ErrorRecoveryDialog.kt
#	auth/src/main/java/com/firebase/ui/auth/ui/screens/FirebaseAuthScreen.kt
#	auth/src/main/java/com/firebase/ui/auth/ui/screens/email/SignInUI.kt
#	auth/src/test/java/com/firebase/ui/auth/ui/screens/email/SignInUITest.kt
@demolaf
demolaf merged commit 444543d into version-10.0.0-beta05Sep 1, 2026
9 checks passed
@demolaf
demolaf deleted the feat/reauth-content-state-slot branch September 1, 2026 14:25
@demolafdemolaf mentioned this pull request Sep 1, 2026
demolaf added a commit that referenced this pull request Sep 1, 2026
…lot (#2452)
* feat(auth): reshape reauthContent into a ReauthContentState content slot
* fix(auth): address reauth review findings and retain state across recreation
* refactor(auth): make reauthentication a request-scoped state machine
* fix(auth): keep a proved reauthentication alive when its operation signs out
* fix(auth): tear down phone verification when a reauthentication attempt fails
* test(auth): cover sign-out-during-retry and phone reauth failure end to end
* test(auth): drop the flaky phone reauth e2e case
Sign up for freeto 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

@demolaf@russellwheatley
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
Skip to content

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

Merged
demolaf merged 8 commits into
version-10.0.0-beta05from
feat/reauth-content-state-slot
Sep 1, 2026
Merged

feat(auth): reshape reauthContent into a ReauthContentState content slot#2452
demolaf merged 8 commits into
version-10.0.0-beta05from
feat/reauth-content-state-slot

Conversation

@demolaf

@demolafdemolaf 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-assistgemini-code-assistBot 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
demolafforce-pushed the feat/reauth-content-state-slot branch 2 times, most recently from 5dfbe74 to 81b3b32CompareAugust 25, 2026 00:38
@demolaf
demolaf changed the base branch from version-10.0.0-beta04-old to version-10.0.0-beta04August 25, 2026 00:42
@demolaf
demolaf marked this pull request as ready for review August 25, 2026 09:22
@demolaf
demolafforce-pushed the feat/reauth-content-state-slot branch 2 times, most recently from 1e1858f to 82c68c0CompareAugust 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-beta05August 26, 2026 14:29
@demolaf
demolaf marked this pull request as ready for review August 26, 2026 14:30
@demolaf
demolafforce-pushed the feat/reauth-content-state-slot branch 3 times, most recently from 692a7a5 to f05a83cCompareAugust 28, 2026 09:45

@russellwheatleyrussellwheatley 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.

Comment threadauth/src/main/java/com/firebase/ui/auth/ui/screens/FirebaseAuthScreen.kt Outdated
Comment threadauth/src/main/java/com/firebase/ui/auth/ui/screens/FirebaseAuthScreen.kt Outdated
@demolaf
demolafforce-pushed the feat/reauth-content-state-slot branch from 4ac3329 to 0469224CompareAugust 31, 2026 11:36

@russellwheatleyrussellwheatley 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.

Re-reviewed after the state-machine refactor and the two follow-up fixes. Both issues from the last round are fixed properly, not just carried over (isCredentialLinkingEnabled/isAnonymousUpgradeEnabled reset now applies at both reauth-config construction sites, and the rememberSaveable gap turned into a proper ReauthPresentationState + Saver that reconciles correctly on both rotation and process death). Two minor items left, neither blocking on their own, flagging so they don't get lost:

  • inline comment below on AuthState.Success's constructor going internal
  • e2eTest/src/test/java/com/firebase/ui/auth/ui/screens/ReauthFlowTest.kt has no dedicated case for sign-out-during-retry or phone-verification-teardown. Both are already covered at the unit/screen level (FirebaseAuthScreenReauthIdleResetTest.kt, PhoneAuthScreenVerificationLifecycleTest.kt), so this is more of a nice-to-have than something I'd hold the PR on.

Comment threadauth/src/main/java/com/firebase/ui/auth/AuthState.kt
…/reauth-content-state-slot
# Conflicts:
#	auth/src/main/java/com/firebase/ui/auth/ui/components/AuthTextField.kt
#	auth/src/main/java/com/firebase/ui/auth/ui/components/ErrorRecoveryDialog.kt
#	auth/src/main/java/com/firebase/ui/auth/ui/screens/FirebaseAuthScreen.kt
#	auth/src/main/java/com/firebase/ui/auth/ui/screens/email/SignInUI.kt
#	auth/src/test/java/com/firebase/ui/auth/ui/screens/email/SignInUITest.kt
@demolaf
demolaf merged commit 444543d into version-10.0.0-beta05Sep 1, 2026
9 checks passed
@demolaf
demolaf deleted the feat/reauth-content-state-slot branch September 1, 2026 14:25
@demolafdemolaf mentioned this pull request Sep 1, 2026
demolaf added a commit that referenced this pull request Sep 1, 2026
…lot (#2452)
* feat(auth): reshape reauthContent into a ReauthContentState content slot
* fix(auth): address reauth review findings and retain state across recreation
* refactor(auth): make reauthentication a request-scoped state machine
* fix(auth): keep a proved reauthentication alive when its operation signs out
* fix(auth): tear down phone verification when a reauthentication attempt fails
* test(auth): cover sign-out-during-retry and phone reauth failure end to end
* test(auth): drop the flaky phone reauth e2e case
Sign up for freeto 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

@demolaf@russellwheatley
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Merged
demolaf merged 8 commits into
version-10.0.0-beta05from
feat/reauth-content-state-slot
Sep 1, 2026
Merged

feat(auth): reshape reauthContent into a ReauthContentState content slot#2452
demolaf merged 8 commits into
version-10.0.0-beta05from
feat/reauth-content-state-slot

Conversation

@demolaf

@demolafdemolaf 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-assistgemini-code-assistBot 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
demolafforce-pushed the feat/reauth-content-state-slot branch 2 times, most recently from 5dfbe74 to 81b3b32CompareAugust 25, 2026 00:38
@demolaf
demolaf changed the base branch from version-10.0.0-beta04-old to version-10.0.0-beta04August 25, 2026 00:42
@demolaf
demolaf marked this pull request as ready for review August 25, 2026 09:22
@demolaf
demolafforce-pushed the feat/reauth-content-state-slot branch 2 times, most recently from 1e1858f to 82c68c0CompareAugust 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-beta05August 26, 2026 14:29
@demolaf
demolaf marked this pull request as ready for review August 26, 2026 14:30
@demolaf
demolafforce-pushed the feat/reauth-content-state-slot branch 3 times, most recently from 692a7a5 to f05a83cCompareAugust 28, 2026 09:45

@russellwheatleyrussellwheatley 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.

Comment threadauth/src/main/java/com/firebase/ui/auth/ui/screens/FirebaseAuthScreen.kt Outdated
Comment threadauth/src/main/java/com/firebase/ui/auth/ui/screens/FirebaseAuthScreen.kt Outdated
@demolaf
demolafforce-pushed the feat/reauth-content-state-slot branch from 4ac3329 to 0469224CompareAugust 31, 2026 11:36

@russellwheatleyrussellwheatley 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.

Re-reviewed after the state-machine refactor and the two follow-up fixes. Both issues from the last round are fixed properly, not just carried over (isCredentialLinkingEnabled/isAnonymousUpgradeEnabled reset now applies at both reauth-config construction sites, and the rememberSaveable gap turned into a proper ReauthPresentationState + Saver that reconciles correctly on both rotation and process death). Two minor items left, neither blocking on their own, flagging so they don't get lost:

  • inline comment below on AuthState.Success's constructor going internal
  • e2eTest/src/test/java/com/firebase/ui/auth/ui/screens/ReauthFlowTest.kt has no dedicated case for sign-out-during-retry or phone-verification-teardown. Both are already covered at the unit/screen level (FirebaseAuthScreenReauthIdleResetTest.kt, PhoneAuthScreenVerificationLifecycleTest.kt), so this is more of a nice-to-have than something I'd hold the PR on.

Comment threadauth/src/main/java/com/firebase/ui/auth/AuthState.kt
…/reauth-content-state-slot
# Conflicts:
#	auth/src/main/java/com/firebase/ui/auth/ui/components/AuthTextField.kt
#	auth/src/main/java/com/firebase/ui/auth/ui/components/ErrorRecoveryDialog.kt
#	auth/src/main/java/com/firebase/ui/auth/ui/screens/FirebaseAuthScreen.kt
#	auth/src/main/java/com/firebase/ui/auth/ui/screens/email/SignInUI.kt
#	auth/src/test/java/com/firebase/ui/auth/ui/screens/email/SignInUITest.kt
@demolaf
demolaf merged commit 444543d into version-10.0.0-beta05Sep 1, 2026
9 checks passed
@demolaf
demolaf deleted the feat/reauth-content-state-slot branch September 1, 2026 14:25
@demolafdemolaf mentioned this pull request Sep 1, 2026
demolaf added a commit that referenced this pull request Sep 1, 2026
…lot (#2452)
* feat(auth): reshape reauthContent into a ReauthContentState content slot
* fix(auth): address reauth review findings and retain state across recreation
* refactor(auth): make reauthentication a request-scoped state machine
* fix(auth): keep a proved reauthentication alive when its operation signs out
* fix(auth): tear down phone verification when a reauthentication attempt fails
* test(auth): cover sign-out-during-retry and phone reauth failure end to end
* test(auth): drop the flaky phone reauth e2e case
Sign up for freeto 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

@demolaf@russellwheatley
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Merged
demolaf merged 8 commits into
version-10.0.0-beta05from
feat/reauth-content-state-slot
Sep 1, 2026
Merged

feat(auth): reshape reauthContent into a ReauthContentState content slot#2452
demolaf merged 8 commits into
version-10.0.0-beta05from
feat/reauth-content-state-slot

Conversation

@demolaf

@demolafdemolaf 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-assistgemini-code-assistBot 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
demolafforce-pushed the feat/reauth-content-state-slot branch 2 times, most recently from 5dfbe74 to 81b3b32CompareAugust 25, 2026 00:38
@demolaf
demolaf changed the base branch from version-10.0.0-beta04-old to version-10.0.0-beta04August 25, 2026 00:42
@demolaf
demolaf marked this pull request as ready for review August 25, 2026 09:22
@demolaf
demolafforce-pushed the feat/reauth-content-state-slot branch 2 times, most recently from 1e1858f to 82c68c0CompareAugust 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-beta05August 26, 2026 14:29
@demolaf
demolaf marked this pull request as ready for review August 26, 2026 14:30
@demolaf
demolafforce-pushed the feat/reauth-content-state-slot branch 3 times, most recently from 692a7a5 to f05a83cCompareAugust 28, 2026 09:45

@russellwheatleyrussellwheatley 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.

Comment threadauth/src/main/java/com/firebase/ui/auth/ui/screens/FirebaseAuthScreen.kt Outdated
Comment threadauth/src/main/java/com/firebase/ui/auth/ui/screens/FirebaseAuthScreen.kt Outdated
@demolaf
demolafforce-pushed the feat/reauth-content-state-slot branch from 4ac3329 to 0469224CompareAugust 31, 2026 11:36

@russellwheatleyrussellwheatley 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.

Re-reviewed after the state-machine refactor and the two follow-up fixes. Both issues from the last round are fixed properly, not just carried over (isCredentialLinkingEnabled/isAnonymousUpgradeEnabled reset now applies at both reauth-config construction sites, and the rememberSaveable gap turned into a proper ReauthPresentationState + Saver that reconciles correctly on both rotation and process death). Two minor items left, neither blocking on their own, flagging so they don't get lost:

  • inline comment below on AuthState.Success's constructor going internal
  • e2eTest/src/test/java/com/firebase/ui/auth/ui/screens/ReauthFlowTest.kt has no dedicated case for sign-out-during-retry or phone-verification-teardown. Both are already covered at the unit/screen level (FirebaseAuthScreenReauthIdleResetTest.kt, PhoneAuthScreenVerificationLifecycleTest.kt), so this is more of a nice-to-have than something I'd hold the PR on.

Comment threadauth/src/main/java/com/firebase/ui/auth/AuthState.kt
…/reauth-content-state-slot
# Conflicts:
#	auth/src/main/java/com/firebase/ui/auth/ui/components/AuthTextField.kt
#	auth/src/main/java/com/firebase/ui/auth/ui/components/ErrorRecoveryDialog.kt
#	auth/src/main/java/com/firebase/ui/auth/ui/screens/FirebaseAuthScreen.kt
#	auth/src/main/java/com/firebase/ui/auth/ui/screens/email/SignInUI.kt
#	auth/src/test/java/com/firebase/ui/auth/ui/screens/email/SignInUITest.kt
@demolaf
demolaf merged commit 444543d into version-10.0.0-beta05Sep 1, 2026
9 checks passed
@demolaf
demolaf deleted the feat/reauth-content-state-slot branch September 1, 2026 14:25
@demolafdemolaf mentioned this pull request Sep 1, 2026
demolaf added a commit that referenced this pull request Sep 1, 2026
…lot (#2452)
* feat(auth): reshape reauthContent into a ReauthContentState content slot
* fix(auth): address reauth review findings and retain state across recreation
* refactor(auth): make reauthentication a request-scoped state machine
* fix(auth): keep a proved reauthentication alive when its operation signs out
* fix(auth): tear down phone verification when a reauthentication attempt fails
* test(auth): cover sign-out-during-retry and phone reauth failure end to end
* test(auth): drop the flaky phone reauth e2e case
Sign up for freeto 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

@demolaf@russellwheatley
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

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

Merged
demolaf merged 8 commits into
version-10.0.0-beta05from
feat/reauth-content-state-slot
Sep 1, 2026
Merged

feat(auth): reshape reauthContent into a ReauthContentState content slot#2452
demolaf merged 8 commits into
version-10.0.0-beta05from
feat/reauth-content-state-slot

Conversation

@demolaf

@demolafdemolaf 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-assistgemini-code-assistBot 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
demolafforce-pushed the feat/reauth-content-state-slot branch 2 times, most recently from 5dfbe74 to 81b3b32CompareAugust 25, 2026 00:38
@demolaf
demolaf changed the base branch from version-10.0.0-beta04-old to version-10.0.0-beta04August 25, 2026 00:42
@demolaf
demolaf marked this pull request as ready for review August 25, 2026 09:22
@demolaf
demolafforce-pushed the feat/reauth-content-state-slot branch 2 times, most recently from 1e1858f to 82c68c0CompareAugust 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-beta05August 26, 2026 14:29
@demolaf
demolaf marked this pull request as ready for review August 26, 2026 14:30
@demolaf
demolafforce-pushed the feat/reauth-content-state-slot branch 3 times, most recently from 692a7a5 to f05a83cCompareAugust 28, 2026 09:45

@russellwheatleyrussellwheatley 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.

Comment threadauth/src/main/java/com/firebase/ui/auth/ui/screens/FirebaseAuthScreen.kt Outdated
Comment threadauth/src/main/java/com/firebase/ui/auth/ui/screens/FirebaseAuthScreen.kt Outdated
@demolaf
demolafforce-pushed the feat/reauth-content-state-slot branch from 4ac3329 to 0469224CompareAugust 31, 2026 11:36

@russellwheatleyrussellwheatley 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.

Re-reviewed after the state-machine refactor and the two follow-up fixes. Both issues from the last round are fixed properly, not just carried over (isCredentialLinkingEnabled/isAnonymousUpgradeEnabled reset now applies at both reauth-config construction sites, and the rememberSaveable gap turned into a proper ReauthPresentationState + Saver that reconciles correctly on both rotation and process death). Two minor items left, neither blocking on their own, flagging so they don't get lost:

  • inline comment below on AuthState.Success's constructor going internal
  • e2eTest/src/test/java/com/firebase/ui/auth/ui/screens/ReauthFlowTest.kt has no dedicated case for sign-out-during-retry or phone-verification-teardown. Both are already covered at the unit/screen level (FirebaseAuthScreenReauthIdleResetTest.kt, PhoneAuthScreenVerificationLifecycleTest.kt), so this is more of a nice-to-have than something I'd hold the PR on.

Comment threadauth/src/main/java/com/firebase/ui/auth/AuthState.kt
…/reauth-content-state-slot
# Conflicts:
#	auth/src/main/java/com/firebase/ui/auth/ui/components/AuthTextField.kt
#	auth/src/main/java/com/firebase/ui/auth/ui/components/ErrorRecoveryDialog.kt
#	auth/src/main/java/com/firebase/ui/auth/ui/screens/FirebaseAuthScreen.kt
#	auth/src/main/java/com/firebase/ui/auth/ui/screens/email/SignInUI.kt
#	auth/src/test/java/com/firebase/ui/auth/ui/screens/email/SignInUITest.kt
@demolaf
demolaf merged commit 444543d into version-10.0.0-beta05Sep 1, 2026
9 checks passed
@demolaf
demolaf deleted the feat/reauth-content-state-slot branch September 1, 2026 14:25
@demolafdemolaf mentioned this pull request Sep 1, 2026
demolaf added a commit that referenced this pull request Sep 1, 2026
…lot (#2452)
* feat(auth): reshape reauthContent into a ReauthContentState content slot
* fix(auth): address reauth review findings and retain state across recreation
* refactor(auth): make reauthentication a request-scoped state machine
* fix(auth): keep a proved reauthentication alive when its operation signs out
* fix(auth): tear down phone verification when a reauthentication attempt fails
* test(auth): cover sign-out-during-retry and phone reauth failure end to end
* test(auth): drop the flaky phone reauth e2e case
Sign up for freeto 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

@demolaf@russellwheatley
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Merged
demolaf merged 8 commits into
version-10.0.0-beta05from
feat/reauth-content-state-slot
Sep 1, 2026
Merged

feat(auth): reshape reauthContent into a ReauthContentState content slot#2452
demolaf merged 8 commits into
version-10.0.0-beta05from
feat/reauth-content-state-slot

Conversation

@demolaf

@demolafdemolaf 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-assistgemini-code-assistBot 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
demolafforce-pushed the feat/reauth-content-state-slot branch 2 times, most recently from 5dfbe74 to 81b3b32CompareAugust 25, 2026 00:38
@demolaf
demolaf changed the base branch from version-10.0.0-beta04-old to version-10.0.0-beta04August 25, 2026 00:42
@demolaf
demolaf marked this pull request as ready for review August 25, 2026 09:22
@demolaf
demolafforce-pushed the feat/reauth-content-state-slot branch 2 times, most recently from 1e1858f to 82c68c0CompareAugust 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-beta05August 26, 2026 14:29
@demolaf
demolaf marked this pull request as ready for review August 26, 2026 14:30
@demolaf
demolafforce-pushed the feat/reauth-content-state-slot branch 3 times, most recently from 692a7a5 to f05a83cCompareAugust 28, 2026 09:45

@russellwheatleyrussellwheatley 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.

Comment threadauth/src/main/java/com/firebase/ui/auth/ui/screens/FirebaseAuthScreen.kt Outdated
Comment threadauth/src/main/java/com/firebase/ui/auth/ui/screens/FirebaseAuthScreen.kt Outdated
@demolaf
demolafforce-pushed the feat/reauth-content-state-slot branch from 4ac3329 to 0469224CompareAugust 31, 2026 11:36

@russellwheatleyrussellwheatley 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.

Re-reviewed after the state-machine refactor and the two follow-up fixes. Both issues from the last round are fixed properly, not just carried over (isCredentialLinkingEnabled/isAnonymousUpgradeEnabled reset now applies at both reauth-config construction sites, and the rememberSaveable gap turned into a proper ReauthPresentationState + Saver that reconciles correctly on both rotation and process death). Two minor items left, neither blocking on their own, flagging so they don't get lost:

  • inline comment below on AuthState.Success's constructor going internal
  • e2eTest/src/test/java/com/firebase/ui/auth/ui/screens/ReauthFlowTest.kt has no dedicated case for sign-out-during-retry or phone-verification-teardown. Both are already covered at the unit/screen level (FirebaseAuthScreenReauthIdleResetTest.kt, PhoneAuthScreenVerificationLifecycleTest.kt), so this is more of a nice-to-have than something I'd hold the PR on.

Comment threadauth/src/main/java/com/firebase/ui/auth/AuthState.kt
…/reauth-content-state-slot
# Conflicts:
#	auth/src/main/java/com/firebase/ui/auth/ui/components/AuthTextField.kt
#	auth/src/main/java/com/firebase/ui/auth/ui/components/ErrorRecoveryDialog.kt
#	auth/src/main/java/com/firebase/ui/auth/ui/screens/FirebaseAuthScreen.kt
#	auth/src/main/java/com/firebase/ui/auth/ui/screens/email/SignInUI.kt
#	auth/src/test/java/com/firebase/ui/auth/ui/screens/email/SignInUITest.kt
@demolaf
demolaf merged commit 444543d into version-10.0.0-beta05Sep 1, 2026
9 checks passed
@demolaf
demolaf deleted the feat/reauth-content-state-slot branch September 1, 2026 14:25
@demolafdemolaf mentioned this pull request Sep 1, 2026
demolaf added a commit that referenced this pull request Sep 1, 2026
…lot (#2452)
* feat(auth): reshape reauthContent into a ReauthContentState content slot
* fix(auth): address reauth review findings and retain state across recreation
* refactor(auth): make reauthentication a request-scoped state machine
* fix(auth): keep a proved reauthentication alive when its operation signs out
* fix(auth): tear down phone verification when a reauthentication attempt fails
* test(auth): cover sign-out-during-retry and phone reauth failure end to end
* test(auth): drop the flaky phone reauth e2e case
Sign up for freeto 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

@demolaf@russellwheatley
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Merged
demolaf merged 8 commits into
version-10.0.0-beta05from
feat/reauth-content-state-slot
Sep 1, 2026
Merged

feat(auth): reshape reauthContent into a ReauthContentState content slot#2452
demolaf merged 8 commits into
version-10.0.0-beta05from
feat/reauth-content-state-slot

Conversation

@demolaf

@demolafdemolaf 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-assistgemini-code-assistBot 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
demolafforce-pushed the feat/reauth-content-state-slot branch 2 times, most recently from 5dfbe74 to 81b3b32CompareAugust 25, 2026 00:38
@demolaf
demolaf changed the base branch from version-10.0.0-beta04-old to version-10.0.0-beta04August 25, 2026 00:42
@demolaf
demolaf marked this pull request as ready for review August 25, 2026 09:22
@demolaf
demolafforce-pushed the feat/reauth-content-state-slot branch 2 times, most recently from 1e1858f to 82c68c0CompareAugust 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-beta05August 26, 2026 14:29
@demolaf
demolaf marked this pull request as ready for review August 26, 2026 14:30
@demolaf
demolafforce-pushed the feat/reauth-content-state-slot branch 3 times, most recently from 692a7a5 to f05a83cCompareAugust 28, 2026 09:45

@russellwheatleyrussellwheatley 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.

Comment threadauth/src/main/java/com/firebase/ui/auth/ui/screens/FirebaseAuthScreen.kt Outdated
Comment threadauth/src/main/java/com/firebase/ui/auth/ui/screens/FirebaseAuthScreen.kt Outdated
@demolaf
demolafforce-pushed the feat/reauth-content-state-slot branch from 4ac3329 to 0469224CompareAugust 31, 2026 11:36

@russellwheatleyrussellwheatley 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.

Re-reviewed after the state-machine refactor and the two follow-up fixes. Both issues from the last round are fixed properly, not just carried over (isCredentialLinkingEnabled/isAnonymousUpgradeEnabled reset now applies at both reauth-config construction sites, and the rememberSaveable gap turned into a proper ReauthPresentationState + Saver that reconciles correctly on both rotation and process death). Two minor items left, neither blocking on their own, flagging so they don't get lost:

  • inline comment below on AuthState.Success's constructor going internal
  • e2eTest/src/test/java/com/firebase/ui/auth/ui/screens/ReauthFlowTest.kt has no dedicated case for sign-out-during-retry or phone-verification-teardown. Both are already covered at the unit/screen level (FirebaseAuthScreenReauthIdleResetTest.kt, PhoneAuthScreenVerificationLifecycleTest.kt), so this is more of a nice-to-have than something I'd hold the PR on.

Comment threadauth/src/main/java/com/firebase/ui/auth/AuthState.kt
…/reauth-content-state-slot
# Conflicts:
#	auth/src/main/java/com/firebase/ui/auth/ui/components/AuthTextField.kt
#	auth/src/main/java/com/firebase/ui/auth/ui/components/ErrorRecoveryDialog.kt
#	auth/src/main/java/com/firebase/ui/auth/ui/screens/FirebaseAuthScreen.kt
#	auth/src/main/java/com/firebase/ui/auth/ui/screens/email/SignInUI.kt
#	auth/src/test/java/com/firebase/ui/auth/ui/screens/email/SignInUITest.kt
@demolaf
demolaf merged commit 444543d into version-10.0.0-beta05Sep 1, 2026
9 checks passed
@demolaf
demolaf deleted the feat/reauth-content-state-slot branch September 1, 2026 14:25
@demolafdemolaf mentioned this pull request Sep 1, 2026
demolaf added a commit that referenced this pull request Sep 1, 2026
…lot (#2452)
* feat(auth): reshape reauthContent into a ReauthContentState content slot
* fix(auth): address reauth review findings and retain state across recreation
* refactor(auth): make reauthentication a request-scoped state machine
* fix(auth): keep a proved reauthentication alive when its operation signs out
* fix(auth): tear down phone verification when a reauthentication attempt fails
* test(auth): cover sign-out-during-retry and phone reauth failure end to end
* test(auth): drop the flaky phone reauth e2e case
Sign up for freeto 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

@demolaf@russellwheatley
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

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

Merged
demolaf merged 8 commits into
version-10.0.0-beta05from
feat/reauth-content-state-slot
Sep 1, 2026
Merged

feat(auth): reshape reauthContent into a ReauthContentState content slot#2452
demolaf merged 8 commits into
version-10.0.0-beta05from
feat/reauth-content-state-slot

Conversation

@demolaf

@demolafdemolaf 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-assistgemini-code-assistBot 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
demolafforce-pushed the feat/reauth-content-state-slot branch 2 times, most recently from 5dfbe74 to 81b3b32CompareAugust 25, 2026 00:38
@demolaf
demolaf changed the base branch from version-10.0.0-beta04-old to version-10.0.0-beta04August 25, 2026 00:42
@demolaf
demolaf marked this pull request as ready for review August 25, 2026 09:22
@demolaf
demolafforce-pushed the feat/reauth-content-state-slot branch 2 times, most recently from 1e1858f to 82c68c0CompareAugust 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-beta05August 26, 2026 14:29
@demolaf
demolaf marked this pull request as ready for review August 26, 2026 14:30
@demolaf
demolafforce-pushed the feat/reauth-content-state-slot branch 3 times, most recently from 692a7a5 to f05a83cCompareAugust 28, 2026 09:45

@russellwheatleyrussellwheatley 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.

Comment threadauth/src/main/java/com/firebase/ui/auth/ui/screens/FirebaseAuthScreen.kt Outdated
Comment threadauth/src/main/java/com/firebase/ui/auth/ui/screens/FirebaseAuthScreen.kt Outdated
@demolaf
demolafforce-pushed the feat/reauth-content-state-slot branch from 4ac3329 to 0469224CompareAugust 31, 2026 11:36

@russellwheatleyrussellwheatley 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.

Re-reviewed after the state-machine refactor and the two follow-up fixes. Both issues from the last round are fixed properly, not just carried over (isCredentialLinkingEnabled/isAnonymousUpgradeEnabled reset now applies at both reauth-config construction sites, and the rememberSaveable gap turned into a proper ReauthPresentationState + Saver that reconciles correctly on both rotation and process death). Two minor items left, neither blocking on their own, flagging so they don't get lost:

  • inline comment below on AuthState.Success's constructor going internal
  • e2eTest/src/test/java/com/firebase/ui/auth/ui/screens/ReauthFlowTest.kt has no dedicated case for sign-out-during-retry or phone-verification-teardown. Both are already covered at the unit/screen level (FirebaseAuthScreenReauthIdleResetTest.kt, PhoneAuthScreenVerificationLifecycleTest.kt), so this is more of a nice-to-have than something I'd hold the PR on.

Comment threadauth/src/main/java/com/firebase/ui/auth/AuthState.kt
…/reauth-content-state-slot
# Conflicts:
#	auth/src/main/java/com/firebase/ui/auth/ui/components/AuthTextField.kt
#	auth/src/main/java/com/firebase/ui/auth/ui/components/ErrorRecoveryDialog.kt
#	auth/src/main/java/com/firebase/ui/auth/ui/screens/FirebaseAuthScreen.kt
#	auth/src/main/java/com/firebase/ui/auth/ui/screens/email/SignInUI.kt
#	auth/src/test/java/com/firebase/ui/auth/ui/screens/email/SignInUITest.kt
@demolaf
demolaf merged commit 444543d into version-10.0.0-beta05Sep 1, 2026
9 checks passed
@demolaf
demolaf deleted the feat/reauth-content-state-slot branch September 1, 2026 14:25
@demolafdemolaf mentioned this pull request Sep 1, 2026
demolaf added a commit that referenced this pull request Sep 1, 2026
…lot (#2452)
* feat(auth): reshape reauthContent into a ReauthContentState content slot
* fix(auth): address reauth review findings and retain state across recreation
* refactor(auth): make reauthentication a request-scoped state machine
* fix(auth): keep a proved reauthentication alive when its operation signs out
* fix(auth): tear down phone verification when a reauthentication attempt fails
* test(auth): cover sign-out-during-retry and phone reauth failure end to end
* test(auth): drop the flaky phone reauth e2e case
Sign up for freeto 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

@demolaf@russellwheatley