fxa_client/internal/
state_manager.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5use std::collections::{HashMap, HashSet};
6
7use crate::{
8    internal::{
9        oauth::{AccessTokenInfo, RefreshToken},
10        profile::Profile,
11        state_persistence::state_to_json,
12        CachedResponse, Config, OAuthFlow, PersistedState,
13    },
14    DeviceCapability, FxaRustAuthState, LocalDevice, Result, ScopedKey,
15};
16
17/// Stores and manages the current state of the FxA client
18///
19/// All fields are private, which means that all state mutations must go through this module.  This
20/// makes it easier to reason about state changes.
21pub struct StateManager {
22    /// State that's persisted to disk
23    persisted_state: PersistedState,
24    /// In-progress OAuth flows
25    flow_store: HashMap<String, OAuthFlow>,
26}
27
28impl StateManager {
29    pub(crate) fn new(persisted_state: PersistedState) -> Self {
30        Self {
31            persisted_state,
32            flow_store: HashMap::new(),
33        }
34    }
35
36    pub fn serialize_persisted_state(&self) -> Result<String> {
37        state_to_json(&self.persisted_state)
38    }
39
40    pub fn config(&self) -> &Config {
41        &self.persisted_state.config
42    }
43
44    pub fn refresh_token(&self) -> Option<&RefreshToken> {
45        self.persisted_state.refresh_token.as_ref()
46    }
47
48    pub fn session_token(&self) -> Option<&str> {
49        self.persisted_state.session_token.as_deref()
50    }
51
52    /// Get our device capabilities
53    ///
54    /// This is the last set of capabilities passed to `initialize_device` or `ensure_capabilities`
55    pub fn device_capabilities(&self) -> &HashSet<DeviceCapability> {
56        &self.persisted_state.device_capabilities
57    }
58
59    /// Set our device capabilities
60    pub fn set_device_capabilities(
61        &mut self,
62        capabilities_set: impl IntoIterator<Item = DeviceCapability>,
63    ) {
64        self.persisted_state.device_capabilities = HashSet::from_iter(capabilities_set);
65    }
66
67    /// Get the last known LocalDevice info sent back from the server
68    pub fn server_local_device_info(&self) -> Option<&LocalDevice> {
69        self.persisted_state.server_local_device_info.as_ref()
70    }
71
72    /// Update the last known LocalDevice info when getting one back from the server
73    pub fn update_server_local_device_info(&mut self, local_device: LocalDevice) {
74        self.persisted_state.server_local_device_info = Some(local_device)
75    }
76
77    /// Clear out the last known LocalDevice info. This means that the next call to
78    /// `ensure_capabilities()` will re-send our capabilities to the server
79    ///
80    /// This is typically called when something may invalidate the server's knowledge of our
81    /// local device capabilities, for example replacing our device info.
82    pub fn clear_server_local_device_info(&mut self) {
83        self.persisted_state.server_local_device_info = None
84    }
85
86    pub fn get_commands_data(&self, key: &str) -> Option<&str> {
87        self.persisted_state
88            .commands_data
89            .get(key)
90            .map(String::as_str)
91    }
92
93    pub fn set_commands_data(&mut self, key: &str, data: String) {
94        self.persisted_state
95            .commands_data
96            .insert(key.to_string(), data);
97    }
98
99    pub fn clear_commands_data(&mut self, key: &str) {
100        self.persisted_state.commands_data.remove(key);
101    }
102
103    pub fn last_handled_command_index(&self) -> Option<u64> {
104        self.persisted_state.last_handled_command
105    }
106
107    pub fn set_last_handled_command_index(&mut self, idx: u64) {
108        self.persisted_state.last_handled_command = Some(idx)
109    }
110
111    pub fn current_device_id(&self) -> Option<&str> {
112        self.persisted_state.current_device_id.as_deref()
113    }
114
115    pub fn set_current_device_id(&mut self, device_id: String) {
116        self.persisted_state.current_device_id = Some(device_id);
117    }
118
119    pub fn get_scoped_key(&self, scope: &str) -> Option<&ScopedKey> {
120        self.persisted_state.scoped_keys.get(scope)
121    }
122
123    pub(crate) fn last_seen_profile(&self) -> Option<&CachedResponse<Profile>> {
124        self.persisted_state.last_seen_profile.as_ref()
125    }
126
127    pub(crate) fn set_last_seen_profile(&mut self, profile: CachedResponse<Profile>) {
128        self.persisted_state.last_seen_profile = Some(profile)
129    }
130
131    pub fn clear_last_seen_profile(&mut self) {
132        self.persisted_state.last_seen_profile = None
133    }
134
135    pub fn get_cached_access_token(&mut self, scope: &str) -> Option<&AccessTokenInfo> {
136        self.persisted_state.access_token_cache.get(scope)
137    }
138
139    pub fn add_cached_access_token(&mut self, scope: impl Into<String>, token: AccessTokenInfo) {
140        self.persisted_state
141            .access_token_cache
142            .insert(scope.into(), token);
143    }
144
145    pub fn clear_access_token_cache(&mut self) {
146        self.persisted_state.access_token_cache.clear()
147    }
148
149    /// Begin an OAuth flow.  This saves the OAuthFlow for later.  `state` must be unique to this
150    /// oauth flow process.
151    pub fn begin_oauth_flow(&mut self, state: impl Into<String>, flow: OAuthFlow) {
152        self.flow_store.insert(state.into(), flow);
153    }
154
155    /// Get an OAuthFlow from a previous `begin_oauth_flow()` call
156    ///
157    /// This operation removes the OAuthFlow from the our internal map.  It can only be called once
158    /// per `state` value.
159    pub fn pop_oauth_flow(&mut self, state: &str) -> Option<OAuthFlow> {
160        self.flow_store.remove(state)
161    }
162
163    /// Complete an OAuth flow.
164    pub fn complete_oauth_flow(
165        &mut self,
166        scoped_keys: Vec<(String, ScopedKey)>,
167        refresh_token: RefreshToken,
168        new_session_token: Option<String>,
169    ) {
170        // When our keys change, we might need to re-register device capabilities with the server.
171        // Ensure that this happens on the next call to ensure_capabilities.
172        self.clear_server_local_device_info();
173
174        for (scope, key) in scoped_keys {
175            self.persisted_state.scoped_keys.insert(scope, key);
176        }
177        self.persisted_state.refresh_token = Some(refresh_token);
178        // We prioritize the existing session token if we already have one, because we might have
179        // acquired a session token before the oauth flow
180        if let (None, Some(new_session_token)) = (self.session_token(), new_session_token) {
181            self.set_session_token(new_session_token)
182        }
183        self.persisted_state.logged_out_from_auth_issues = false;
184        self.flow_store.clear();
185    }
186
187    /// Clear any in-progress oauth flows
188    pub fn clear_oauth_flows(&mut self) {
189        self.flow_store.clear();
190    }
191
192    /// Called when the account is disconnected.  This clears most of the auth state, but keeps
193    /// some information in order to eventually reconnect to the same user account later.
194    pub fn disconnect(&mut self) {
195        self.persisted_state.current_device_id = None;
196        self.persisted_state.refresh_token = None;
197        self.persisted_state.scoped_keys = HashMap::new();
198        self.persisted_state.last_handled_command = None;
199        self.persisted_state.commands_data = HashMap::new();
200        self.persisted_state.access_token_cache = HashMap::new();
201        self.persisted_state.device_capabilities = HashSet::new();
202        self.persisted_state.server_local_device_info = None;
203        self.persisted_state.session_token = None;
204        self.persisted_state.logged_out_from_auth_issues = false;
205        self.flow_store.clear();
206    }
207
208    /// Called when we notice authentication issues with the account state.
209    ///
210    /// This clears the auth state, but leaves some fields untouched. That way, if the user
211    /// re-authenticates they can continue using the account without unexpected behavior.  The
212    /// fields that don't change compared to `disconnect()` are:
213    ///
214    ///   * `current_device_id`
215    ///   * `device_capabilities`
216    ///   * `last_handled_command`
217    pub fn on_auth_issues(&mut self) {
218        self.persisted_state.refresh_token = None;
219        self.persisted_state.scoped_keys = HashMap::new();
220        self.persisted_state.commands_data = HashMap::new();
221        self.persisted_state.access_token_cache = HashMap::new();
222        self.persisted_state.server_local_device_info = None;
223        self.persisted_state.session_token = None;
224        self.persisted_state.logged_out_from_auth_issues = true;
225        self.flow_store.clear();
226    }
227
228    /// Called when we begin an OAuth flow.
229    ///
230    /// This clears out tokens/keys set from the previous time we completed an oauth flow.  In
231    /// particular, it clears the session token to avoid
232    /// https://bugzilla.mozilla.org/show_bug.cgi?id=1887071.
233    pub fn on_begin_oauth(&mut self) {
234        self.persisted_state.refresh_token = None;
235        self.persisted_state.scoped_keys = HashMap::new();
236        self.persisted_state.commands_data = HashMap::new();
237        self.persisted_state.access_token_cache = HashMap::new();
238        self.persisted_state.session_token = None;
239    }
240
241    pub fn get_auth_state(&self) -> FxaRustAuthState {
242        if self.persisted_state.refresh_token.is_some() {
243            FxaRustAuthState::Connected
244        } else if self.persisted_state.logged_out_from_auth_issues {
245            FxaRustAuthState::AuthIssues
246        } else {
247            FxaRustAuthState::Disconnected
248        }
249    }
250
251    /// Handle the auth tokens changing
252    ///
253    /// This method updates the token data and clears out data that may be invalidated with the
254    /// token changes.
255    pub fn update_tokens(&mut self, session_token: String, refresh_token: RefreshToken) {
256        self.persisted_state.session_token = Some(session_token);
257        self.persisted_state.refresh_token = Some(refresh_token);
258        self.persisted_state.access_token_cache.clear();
259        self.persisted_state.server_local_device_info = None;
260    }
261
262    /// Used by the application to test auth token issues
263    pub fn simulate_temporary_auth_token_issue(&mut self) {
264        for (_, access_token) in self.persisted_state.access_token_cache.iter_mut() {
265            "invalid-data".clone_into(&mut access_token.token)
266        }
267    }
268
269    /// Used by the application to test auth token issues
270    pub fn simulate_permanent_auth_token_issue(&mut self) {
271        self.persisted_state.session_token = None;
272        self.persisted_state.refresh_token = None;
273        self.persisted_state.access_token_cache.clear();
274    }
275    pub fn set_session_token(&mut self, token: String) {
276        self.persisted_state.session_token = Some(token)
277    }
278}
279
280#[cfg(test)]
281impl StateManager {
282    pub fn is_access_token_cache_empty(&self) -> bool {
283        self.persisted_state.access_token_cache.is_empty()
284    }
285
286    pub fn force_refresh_token(&mut self, token: RefreshToken) {
287        self.persisted_state.refresh_token = Some(token)
288    }
289
290    pub fn force_current_device_id(&mut self, device_id: impl Into<String>) {
291        self.persisted_state.current_device_id = Some(device_id.into())
292    }
293
294    pub fn insert_scoped_key(&mut self, scope: impl Into<String>, key: ScopedKey) {
295        self.persisted_state.scoped_keys.insert(scope.into(), key);
296    }
297}