From d5c16d0443bdf814372124c7e97a759696d5ebdd Mon Sep 17 00:00:00 2001 From: bytesized Date: Wed, 26 Aug 2026 15:41:13 -0700 Subject: [PATCH 1/2] Bug 2060541 - Recheck auth status on startup, weekly --- components/fxa-client/src/internal/mod.rs | 22 +++++++++ .../fxa-client/src/internal/state_manager.rs | 22 ++++++++- .../src/internal/state_persistence.rs | 2 + .../fxa-client/src/state_machine/helpers.rs | 21 +++++++++ .../fxa-client/src/state_machine/mod.rs | 6 ++- .../src/state_machine/transitions.rs | 27 ++++++++++- .../rc_crypto/nss/fixtures/profile/logins.db | Bin 40960 -> 45056 bytes examples/fxa-client/src/main.rs | 44 ++++++++++++++---- 8 files changed, 133 insertions(+), 11 deletions(-) diff --git a/components/fxa-client/src/internal/mod.rs b/components/fxa-client/src/internal/mod.rs index 2c24db44f5d..7dd49b55aa2 100644 --- a/components/fxa-client/src/internal/mod.rs +++ b/components/fxa-client/src/internal/mod.rs @@ -92,6 +92,8 @@ impl FirefoxAccount { last_seen_profile: None, access_token_cache: HashMap::new(), logged_out_from_auth_issues: false, + last_auth_recheck_time: None, + recovery_refresh_token: None, }) } @@ -272,6 +274,26 @@ impl FirefoxAccount { pub fn simulate_permanent_auth_token_issue(&mut self) { self.state.simulate_permanent_auth_token_issue() } + + /// Get the last time we re-checked our authentication after a failure. + pub fn last_auth_recheck_time(&self) -> Option { + self.state.last_auth_recheck_time() + } + + /// Set the last time we re-checked our authentication after a failure. + /// + /// **💾 This method alters the persisted account state.** + pub fn set_last_auth_recheck_time(&mut self, time: Option) { + self.state.set_last_auth_recheck_time(time); + } + + /// Restores the account's refresh token from `recovery_refresh_token` so that we can retry a + /// previously-failed authorization. + /// + /// **💾 This method alters the persisted account state.** + pub fn restore_refresh_token(&mut self) { + self.state.restore_refresh_token(); + } } #[derive(Debug, Clone, Deserialize, Serialize)] diff --git a/components/fxa-client/src/internal/state_manager.rs b/components/fxa-client/src/internal/state_manager.rs index 04c1776d266..2e65b713c63 100644 --- a/components/fxa-client/src/internal/state_manager.rs +++ b/components/fxa-client/src/internal/state_manager.rs @@ -74,6 +74,18 @@ impl StateManager { self.persisted_state.server_local_device_info = Some(local_device) } + pub fn last_auth_recheck_time(&self) -> Option { + self.persisted_state.last_auth_recheck_time.clone() + } + + pub fn set_last_auth_recheck_time(&mut self, time: Option) { + self.persisted_state.last_auth_recheck_time = time; + } + + pub fn restore_refresh_token(&mut self) { + self.persisted_state.refresh_token = self.persisted_state.recovery_refresh_token.clone(); + } + /// Clear out the last known LocalDevice info. This means that the next call to /// `ensure_capabilities()` will re-send our capabilities to the server /// @@ -209,6 +221,8 @@ impl StateManager { self.persisted_state.session_token = None; self.persisted_state.logged_out_from_auth_issues = false; self.persisted_state.last_seen_profile = None; + self.persisted_state.last_auth_recheck_time = None; + self.persisted_state.recovery_refresh_token = None; self.flow_store.clear(); } @@ -222,7 +236,13 @@ impl StateManager { /// * `device_capabilities` /// * `last_handled_command` pub fn on_auth_issues(&mut self) { - self.persisted_state.refresh_token = None; + // Attempt to reset the timer that indicates how long until we recheck the auth issues + use std::time::SystemTime; + if let Ok(now) = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) { + self.persisted_state.last_auth_recheck_time = Some(now.as_secs()); + } + + self.persisted_state.recovery_refresh_token = self.persisted_state.refresh_token.take(); self.persisted_state.scoped_keys = HashMap::new(); self.persisted_state.commands_data = HashMap::new(); self.persisted_state.access_token_cache = HashMap::new(); diff --git a/components/fxa-client/src/internal/state_persistence.rs b/components/fxa-client/src/internal/state_persistence.rs index 8f37087764d..4fbaf598614 100644 --- a/components/fxa-client/src/internal/state_persistence.rs +++ b/components/fxa-client/src/internal/state_persistence.rs @@ -110,6 +110,8 @@ pub(crate) struct StateV2 { pub(crate) server_local_device_info: Option, #[serde(default)] pub(crate) logged_out_from_auth_issues: bool, + pub(crate) last_auth_recheck_time: Option, + pub(crate) recovery_refresh_token: Option, } #[cfg(test)] diff --git a/components/fxa-client/src/state_machine/helpers.rs b/components/fxa-client/src/state_machine/helpers.rs index 5e9670426e1..53b7461e01d 100644 --- a/components/fxa-client/src/state_machine/helpers.rs +++ b/components/fxa-client/src/state_machine/helpers.rs @@ -249,6 +249,27 @@ impl<'a> RetryingAccount<'a> { } } } + + /// Get the time of the last authorization attempt, or `None` if authorization has not been + /// attempted. + pub fn last_auth_recheck_time(&self) -> Option { + self.inner.last_auth_recheck_time() + } + + /// Set the time of the last authorization attempt. + /// + /// **💾 This method alters the persisted account state.** + pub fn set_last_auth_recheck_time(&mut self, time: Option) { + self.inner.set_last_auth_recheck_time(time); + } + + /// Restores the account's refresh token from `recovery_refresh_token` so that we can retry a + /// previously-failed authorization. + /// + /// **💾 This method alters the persisted account state.** + pub fn restore_refresh_token(&mut self) { + self.inner.restore_refresh_token(); + } } #[cfg(test)] diff --git a/components/fxa-client/src/state_machine/mod.rs b/components/fxa-client/src/state_machine/mod.rs index aea7c7d269f..8b406814b3e 100644 --- a/components/fxa-client/src/state_machine/mod.rs +++ b/components/fxa-client/src/state_machine/mod.rs @@ -30,7 +30,11 @@ impl FirefoxAccount { // Must run before transition() — side effects read `device_config`. self.handle_state_machine_initialization(&event)?; - let was_in_auth_issues = matches!(self.auth_state, FxaState::AuthIssues); + // We want to keep track of transitions to the `AuthIssues` state that aren't from the + // `AuthIssues` state (that's not a state transition) or the `Uninitialized` state + // (we don't want to count initialization as a state transition either). + let was_in_auth_issues = matches!(self.auth_state, FxaState::AuthIssues) + || matches!(self.auth_state, FxaState::Uninitialized); let from_state = self.auth_state.clone(); breadcrumb!("FxaStateMachine.process_event starting: {event}"); diff --git a/components/fxa-client/src/state_machine/transitions.rs b/components/fxa-client/src/state_machine/transitions.rs index d2e3f69f1a5..8181961a9f9 100644 --- a/components/fxa-client/src/state_machine/transitions.rs +++ b/components/fxa-client/src/state_machine/transitions.rs @@ -24,7 +24,32 @@ pub fn transition( // ── From Uninitialized ────────────────────────────────────────── (S::Uninitialized, E::Initialize { device_config }) => match account.get_auth_state() { FxaRustAuthState::Disconnected => Ok(S::Disconnected), - FxaRustAuthState::AuthIssues => Ok(S::AuthIssues), + FxaRustAuthState::AuthIssues => { + // This probably indicates that the user is not authorized but there are various + // corner cases where we might have gotten something wrong. For example, a bug in an + // older browser version that we've since fixed or an FxA server bug. + // Because of this, we will recheck the authorization status from time to time. + use std::time::SystemTime; + let last_auth_time: u64 = account.last_auth_recheck_time().unwrap_or(0); + let next_auth_time = last_auth_time + (7 * 24 * 60 * 60); // One week + if let Ok(now) = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) { + let now: u64 = now.as_secs(); + if next_auth_time <= now { + account.set_last_auth_recheck_time(Some(now)); + // We clear our our refresh token when we have auth issues. To re-check our + // auth status, we need to restore it. + account.restore_refresh_token(); + match account.check_authorization_status() { + Ok(true) => Ok(S::Connected), + _ => Ok(S::AuthIssues), + } + } else { + Ok(S::AuthIssues) + } + } else { + Ok(S::AuthIssues) + } + } FxaRustAuthState::Connected => { match account.finish_initialize(&device_config.capabilities) { Ok(()) => Ok(S::Connected), diff --git a/components/support/rc_crypto/nss/fixtures/profile/logins.db b/components/support/rc_crypto/nss/fixtures/profile/logins.db index ebe1a36f4b2f23b9b7f98e178376fb9ca60a006f..9253f9b3b7d66435ed5e050539c91b1bcaa20ae0 100644 GIT binary patch delta 294 zcmZoTz|`=7X@ayW3j+fKHxR=B50C*8g8@b$gLPt~ydn#OUY8Ir>jwrd)?@}QZT^e= z^=vBq0enaJ9C`QhHn1jdY<$e(+Q`MlE-o+6*qB_Bn3R*6RFs;SoRM1W17~nM2e~?i zxGID=I{CONz=ahwxD=ouGi7oXmxc<6mztMcR9R4xni7y$TwI=Cl%f#g8W93ksR?wK z$>cAr23#F1EbQXa(v1D_n}4z1VV?YfQ;9Q(5iHbGy;+(|3nZktxq-KhiA9WoUu?6W u0uR4vV>+V{gEfP-tSlodD{Et}Dk~!+!(?4~`^nkzQ#SvTXKGmFAOHYIHAWl& delta 213 zcmZp8z|?SnX@ayWGXnzy7ZAe$Cy>Dc;{a(!AZDJ}C@;^RKeqi7d Result<()> { @@ -92,7 +96,12 @@ fn main() -> Result<()> { println!("The account state managed by this utility can be used by many app-services demos and examples."); println!("Run with `help` or `--help` for more"); print_status(&fxa); - return Ok(()); + + // Even though we are ostensibly just printing the status, sometimes the process of just + // initializing can change the state. This happens, for example, if we are in the + // `AuthIssues` state and the timer has expired to re-check the auth, which is + // successful this time. + return fxa.persist(); } Some(Command::Login { scopes }) => { let scope_refs: Vec<&str> = if scopes.is_empty() { @@ -134,6 +143,9 @@ fn main() -> Result<()> { account.disconnect(); } Command::Login { .. } => unreachable!(), + Command::ForceAuthIssues => { + account.on_auth_issues(); + } } } } @@ -164,13 +176,29 @@ impl Cli { fn print_status(fxa: &CliFxa) { match fxa.account() { None => println!("Not logged in"), - Some(account) => match account.check_authorization_status() { - Ok(status) if status.active => { - println!("Account is logged in and authorized by the server") + Some(account) => { + let mut state: FxaState = account.get_state(); + if state == FxaState::Uninitialized { + state = account + .process_event(FxaEvent::Initialize { + device_config: DeviceConfig { + name: "test-device".to_owned(), + device_type: DeviceType::Mobile, + capabilities: vec![DeviceCapability::SendTab], + }, + }) + .unwrap(); + } + println!("Account currently in state: {state}"); + + match account.check_authorization_status() { + Ok(status) if status.active => { + println!("Account is logged in and authorized by the server") + } + Ok(_) => println!("Account is logged in but not authorized by the server"), + Err(e) => println!("Account logged in but account status failed: {e}"), } - Ok(_) => println!("Account is logged in but not authorized by the server"), - Err(e) => println!("Account logged in but account status failed: {e}"), - }, + } } } From 5d7a378715f7f26913e1225aa39617c33cd8be29 Mon Sep 17 00:00:00 2001 From: bytesized Date: Wed, 26 Aug 2026 15:41:13 -0700 Subject: [PATCH 2/2] Bug 2060541 - Recheck auth status on startup, weekly --- components/fxa-client/src/internal/state_manager.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/fxa-client/src/internal/state_manager.rs b/components/fxa-client/src/internal/state_manager.rs index 2e65b713c63..6b8b47f42b2 100644 --- a/components/fxa-client/src/internal/state_manager.rs +++ b/components/fxa-client/src/internal/state_manager.rs @@ -75,7 +75,7 @@ impl StateManager { } pub fn last_auth_recheck_time(&self) -> Option { - self.persisted_state.last_auth_recheck_time.clone() + self.persisted_state.last_auth_recheck_time } pub fn set_last_auth_recheck_time(&mut self, time: Option) {