Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions components/fxa-client/src/internal/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
}

Expand Down Expand Up @@ -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<u64> {
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<u64>) {
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)]
Expand Down
22 changes: 21 additions & 1 deletion components/fxa-client/src/internal/state_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,18 @@ impl StateManager {
self.persisted_state.server_local_device_info = Some(local_device)
}

pub fn last_auth_recheck_time(&self) -> Option<u64> {
self.persisted_state.last_auth_recheck_time
}

pub fn set_last_auth_recheck_time(&mut self, time: Option<u64>) {
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
///
Expand Down Expand Up @@ -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();
}

Expand All @@ -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();
Expand Down
2 changes: 2 additions & 0 deletions components/fxa-client/src/internal/state_persistence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ pub(crate) struct StateV2 {
pub(crate) server_local_device_info: Option<LocalDevice>,
#[serde(default)]
pub(crate) logged_out_from_auth_issues: bool,
pub(crate) last_auth_recheck_time: Option<u64>,
pub(crate) recovery_refresh_token: Option<RefreshToken>,
}

#[cfg(test)]
Expand Down
21 changes: 21 additions & 0 deletions components/fxa-client/src/state_machine/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u64> {
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<u64>) {
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)]
Expand Down
6 changes: 5 additions & 1 deletion components/fxa-client/src/state_machine/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}");
Expand Down
27 changes: 26 additions & 1 deletion components/fxa-client/src/state_machine/transitions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
Binary file modified components/support/rc_crypto/nss/fixtures/profile/logins.db
Binary file not shown.
44 changes: 36 additions & 8 deletions examples/fxa-client/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ mod send_tab;

use clap::{Parser, Subcommand, ValueEnum};
use cli_support::fxa_creds::{self, CliFxa, WELL_KNOWN_SCOPES};
use fxa_client::{FxaConfig, FxaServer};
use fxa_client::{
DeviceCapability, DeviceConfig, DeviceType, FxaConfig, FxaEvent, FxaServer, FxaState,
};

static CLIENT_ID: &str = "a2270f727f45f648";

Expand Down Expand Up @@ -75,6 +77,8 @@ enum Command {
/// List the clients attached to the account (uses session-token auth).
AttachedClients,
Disconnect,
/// Force the user into the FxaState::AuthIssues state
ForceAuthIssues,
}

fn main() -> Result<()> {
Expand All @@ -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() {
Expand Down Expand Up @@ -134,6 +143,9 @@ fn main() -> Result<()> {
account.disconnect();
}
Command::Login { .. } => unreachable!(),
Command::ForceAuthIssues => {
account.on_auth_issues();
}
}
}
}
Expand Down Expand Up @@ -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}"),
},
}
}
}

Expand Down