1use self::{
8 config::Config,
9 oauth::{AuthCircuitBreaker, OAuthFlow, OAUTH_WEBCHANNEL_REDIRECT},
10 state_manager::StateManager,
11 state_persistence::PersistedState,
12 telemetry::FxaTelemetry,
13};
14use crate::{DeviceConfig, Error, FxaConfig, FxaRustAuthState, FxaState, Result};
15use serde_derive::*;
16use std::{
17 collections::{HashMap, HashSet},
18 sync::Arc,
19};
20use url::Url;
21
22mod close_tabs;
26mod commands;
27pub mod config;
28pub mod device;
29mod http_client;
30mod oauth;
31mod profile;
32mod push;
33mod scoped_keys;
34mod scopes;
35mod send_tab;
36mod state_manager;
37mod state_persistence;
38mod telemetry;
39mod util;
40
41type FxAClient = dyn http_client::FxAClient + Sync + Send;
42
43#[cfg(test)]
45unsafe impl Send for http_client::MockFxAClient {}
46#[cfg(test)]
47unsafe impl Sync for http_client::MockFxAClient {}
48
49pub struct FirefoxAccount {
53 client: Arc<FxAClient>,
54 state: StateManager,
55 attached_clients_cache: Option<CachedResponse<Vec<http_client::GetAttachedClientResponse>>>,
56 devices_cache: Option<CachedResponse<Vec<http_client::GetDeviceResponse>>>,
57 auth_circuit_breaker: AuthCircuitBreaker,
58 telemetry: FxaTelemetry,
59 pub(crate) auth_state: FxaState,
62 pub(crate) device_config: Option<DeviceConfig>,
64}
65
66impl FirefoxAccount {
67 fn from_state(state: PersistedState) -> Self {
68 Self {
69 client: Arc::new(http_client::Client::new()),
70 state: StateManager::new(state),
71 attached_clients_cache: None,
72 devices_cache: None,
73 auth_circuit_breaker: Default::default(),
74 telemetry: FxaTelemetry::new(),
75 auth_state: FxaState::Uninitialized,
76 device_config: None,
77 }
78 }
79
80 pub fn with_config(config: Config) -> Self {
82 Self::from_state(PersistedState {
83 config,
84 refresh_token: None,
85 scoped_keys: HashMap::new(),
86 last_handled_command: None,
87 commands_data: HashMap::new(),
88 device_capabilities: HashSet::new(),
89 server_local_device_info: None,
90 session_token: None,
91 current_device_id: None,
92 last_seen_profile: None,
93 access_token_cache: HashMap::new(),
94 logged_out_from_auth_issues: false,
95 })
96 }
97
98 pub fn new(config: FxaConfig) -> Self {
100 Self::with_config(config.into())
101 }
102
103 #[cfg(test)]
104 pub(crate) fn set_client(&mut self, client: Arc<FxAClient>) {
105 self.client = client;
106 }
107
108 pub fn from_json(data: &str) -> Result<Self> {
111 let state = state_persistence::state_from_json(data)?;
112 Ok(Self::from_state(state))
113 }
114
115 pub fn to_json(&self) -> Result<String> {
118 self.state.serialize_persisted_state()
119 }
120
121 pub fn clear_devices_and_attached_clients_cache(&mut self) {
123 self.attached_clients_cache = None;
124 self.devices_cache = None;
125 }
126
127 pub fn get_token_server_endpoint_url(&self) -> Result<String> {
129 Ok(self.state.config().token_server_endpoint_url()?.into())
130 }
131
132 pub fn get_pairing_authority_url(&self) -> Result<String> {
135 if self.state.config().content_url()? == Url::parse(config::CONTENT_URL_RELEASE)? {
137 return Ok("https://firefox.com/pair".to_owned());
138 }
139 if self.state.config().content_url()? == Url::parse(config::CONTENT_URL_CHINA)? {
141 return Ok("https://firefox.com.cn/pair".to_owned());
142 }
143 Ok(self.state.config().pair_url()?.into())
144 }
145
146 pub fn get_connection_success_url(&self) -> Result<String> {
151 let mut url = self.state.config().connect_another_device_url()?;
152 url.query_pairs_mut()
153 .append_pair("showSuccessMessage", "true");
154 Ok(url.into())
155 }
156
157 pub fn get_manage_account_url(&mut self, entrypoint: &str) -> Result<String> {
165 let mut url = self.state.config().settings_url()?;
166 url.query_pairs_mut().append_pair("entrypoint", entrypoint);
167 if self.state.config().redirect_uri == OAUTH_WEBCHANNEL_REDIRECT {
168 url.query_pairs_mut()
169 .append_pair("context", "oauth_webchannel_v1");
170 }
171 self.add_account_identifiers_to_url(url)
172 }
173
174 pub fn get_manage_devices_url(&mut self, entrypoint: &str) -> Result<String> {
182 let mut url = self.state.config().settings_clients_url()?;
183 url.query_pairs_mut().append_pair("entrypoint", entrypoint);
184 self.add_account_identifiers_to_url(url)
185 }
186
187 fn add_account_identifiers_to_url(&mut self, mut url: Url) -> Result<String> {
188 let profile = self.get_profile(false)?;
189 url.query_pairs_mut()
190 .append_pair("uid", &profile.uid)
191 .append_pair("email", &profile.email);
192 Ok(url.into())
193 }
194
195 fn get_refresh_token(&self) -> Result<&str> {
196 match self.state.refresh_token() {
197 Some(token_info) => Ok(&token_info.token),
198 None => Err(Error::NoRefreshToken),
199 }
200 }
201
202 pub fn get_auth_state(&self) -> FxaRustAuthState {
203 self.state.get_auth_state()
204 }
205
206 pub fn disconnect(&mut self) {
213 let current_device_result;
214 {
215 current_device_result = self.get_current_device();
216 }
217
218 if let Some(refresh_token) = self.state.refresh_token() {
219 let destroy_result = match current_device_result {
222 Ok(Some(device)) => self.client.destroy_device_record(
225 self.state.config(),
226 &refresh_token.token,
227 &device.id,
228 ),
229 _ => self
230 .client
231 .destroy_refresh_token(self.state.config(), &refresh_token.token),
232 };
233 if let Err(e) = destroy_result {
234 crate::warn!("Error while destroying the device: {}", e);
235 }
236 }
237 self.state.disconnect();
238 self.clear_devices_and_attached_clients_cache();
239 self.telemetry = FxaTelemetry::new();
240 }
241
242 pub fn on_auth_issues(&mut self) {
249 self.state.on_auth_issues();
250 self.clear_devices_and_attached_clients_cache();
251 self.telemetry = FxaTelemetry::new();
252 }
253
254 pub fn simulate_network_error(&mut self) {
255 self.client.simulate_network_error();
256 }
257
258 pub fn simulate_temporary_auth_token_issue(&mut self) {
259 self.state.simulate_temporary_auth_token_issue()
260 }
261
262 pub fn simulate_permanent_auth_token_issue(&mut self) {
263 self.state.simulate_permanent_auth_token_issue()
264 }
265}
266
267#[derive(Debug, Clone, Deserialize, Serialize)]
268pub(crate) struct CachedResponse<T> {
269 response: T,
270 cached_at: u64,
271 etag: String,
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277 use crate::internal::device::*;
278 use crate::internal::http_client::MockFxAClient;
279 use crate::internal::oauth::*;
280 use mockall::predicate::always;
281 use mockall::predicate::eq;
282
283 #[test]
284 fn test_fxa_is_send() {
285 fn is_send<T: Send>() {}
286 is_send::<FirefoxAccount>();
287 }
288
289 #[test]
290 fn test_serialize_deserialize() {
291 let config = Config::stable_dev("12345678", "https://foo.bar");
292 let fxa1 = FirefoxAccount::with_config(config);
293 let fxa1_json = fxa1.to_json().unwrap();
294 drop(fxa1);
295 let fxa2 = FirefoxAccount::from_json(&fxa1_json).unwrap();
296 let fxa2_json = fxa2.to_json().unwrap();
297 assert_eq!(fxa1_json, fxa2_json);
298 }
299
300 #[test]
301 fn test_get_connection_success_url() {
302 let config = Config::new("https://stable.dev.lcip.org", "12345678", "https://foo.bar");
303 let fxa = FirefoxAccount::with_config(config);
304 let url = fxa.get_connection_success_url().unwrap();
305 assert_eq!(
306 url,
307 "https://stable.dev.lcip.org/connect_another_device?showSuccessMessage=true"
308 .to_string()
309 );
310 }
311
312 #[test]
313 fn test_get_manage_account_url() {
314 let config = Config::new("https://stable.dev.lcip.org", "12345678", "https://foo.bar");
315 let mut fxa = FirefoxAccount::with_config(config);
316 match fxa.get_manage_account_url("test").unwrap_err() {
318 Error::NoCachedToken(_) => {}
319 _ => panic!("error not NoCachedToken"),
320 };
321 fxa.add_cached_profile("123", "test@example.com");
323 let url = fxa.get_manage_account_url("test").unwrap();
324 assert_eq!(
325 url,
326 "https://stable.dev.lcip.org/settings?entrypoint=test&uid=123&email=test%40example.com"
327 .to_string()
328 );
329 }
330
331 #[test]
332 fn test_get_manage_account_url_with_webchannel_redirect() {
333 let config = Config::new(
334 "https://stable.dev.lcip.org",
335 "12345678",
336 OAUTH_WEBCHANNEL_REDIRECT,
337 );
338 let mut fxa = FirefoxAccount::with_config(config);
339 fxa.add_cached_profile("123", "test@example.com");
340 let url = fxa.get_manage_account_url("test").unwrap();
341 assert_eq!(
342 url,
343 "https://stable.dev.lcip.org/settings?entrypoint=test&context=oauth_webchannel_v1&uid=123&email=test%40example.com"
344 .to_string()
345 );
346 }
347
348 #[test]
349 fn test_get_manage_devices_url() {
350 let config = Config::new("https://stable.dev.lcip.org", "12345678", "https://foo.bar");
351 let mut fxa = FirefoxAccount::with_config(config);
352 match fxa.get_manage_devices_url("test").unwrap_err() {
354 Error::NoCachedToken(_) => {}
355 _ => panic!("error not NoCachedToken"),
356 };
357 fxa.add_cached_profile("123", "test@example.com");
359 let url = fxa.get_manage_devices_url("test").unwrap();
360 assert_eq!(
361 url,
362 "https://stable.dev.lcip.org/settings/clients?entrypoint=test&uid=123&email=test%40example.com"
363 .to_string()
364 );
365 }
366
367 #[test]
368 fn test_disconnect_no_refresh_token() {
369 let config = Config::new("https://stable.dev.lcip.org", "12345678", "https://foo.bar");
370 let mut fxa = FirefoxAccount::with_config(config);
371
372 fxa.add_cached_token(
373 "profile",
374 AccessTokenInfo {
375 scope: "profile".to_string(),
376 token: "profiletok".to_string(),
377 key: None,
378 expires_at: u64::MAX,
379 },
380 );
381
382 let client = MockFxAClient::new();
383 fxa.set_client(Arc::new(client));
384
385 assert!(!fxa.state.is_access_token_cache_empty());
386 fxa.disconnect();
387 assert!(fxa.state.is_access_token_cache_empty());
388 }
389
390 #[test]
391 fn test_disconnect_device() {
392 let config = Config::stable_dev("12345678", "https://foo.bar");
393 let mut fxa = FirefoxAccount::with_config(config);
394
395 fxa.state.force_refresh_token(RefreshToken {
396 token: "refreshtok".to_string(),
397 scopes: HashSet::default(),
398 });
399
400 let mut client = MockFxAClient::new();
401 client
402 .expect_get_devices()
403 .with(always(), eq("refreshtok"))
404 .times(1)
405 .returning(|_, _| {
406 Ok(vec![
407 Device {
408 common: http_client::DeviceResponseCommon {
409 id: "1234a".to_owned(),
410 display_name: "My Device".to_owned(),
411 device_type: sync15::DeviceType::Mobile,
412 push_subscription: None,
413 available_commands: HashMap::default(),
414 push_endpoint_expired: false,
415 },
416 is_current_device: true,
417 location: http_client::DeviceLocation {
418 city: None,
419 country: None,
420 state: None,
421 state_code: None,
422 },
423 last_access_time: None,
424 },
425 Device {
426 common: http_client::DeviceResponseCommon {
427 id: "a4321".to_owned(),
428 display_name: "My Other Device".to_owned(),
429 device_type: sync15::DeviceType::Desktop,
430 push_subscription: None,
431 available_commands: HashMap::default(),
432 push_endpoint_expired: false,
433 },
434 is_current_device: false,
435 location: http_client::DeviceLocation {
436 city: None,
437 country: None,
438 state: None,
439 state_code: None,
440 },
441 last_access_time: None,
442 },
443 ])
444 });
445 client
446 .expect_destroy_device_record()
447 .with(always(), eq("refreshtok"), eq("1234a"))
448 .times(1)
449 .returning(|_, _, _| Ok(()));
450 fxa.set_client(Arc::new(client));
451
452 assert!(fxa.state.refresh_token().is_some());
453 fxa.disconnect();
454 assert!(fxa.state.refresh_token().is_none());
455 }
456
457 #[test]
458 fn test_disconnect_no_device() {
459 let config = Config::stable_dev("12345678", "https://foo.bar");
460 let mut fxa = FirefoxAccount::with_config(config);
461
462 fxa.state.force_refresh_token(RefreshToken {
463 token: "refreshtok".to_string(),
464 scopes: HashSet::default(),
465 });
466
467 let mut client = MockFxAClient::new();
468 client
469 .expect_get_devices()
470 .with(always(), eq("refreshtok"))
471 .times(1)
472 .returning(|_, _| {
473 Ok(vec![Device {
474 common: http_client::DeviceResponseCommon {
475 id: "a4321".to_owned(),
476 display_name: "My Other Device".to_owned(),
477 device_type: sync15::DeviceType::Desktop,
478 push_subscription: None,
479 available_commands: HashMap::default(),
480 push_endpoint_expired: false,
481 },
482 is_current_device: false,
483 location: http_client::DeviceLocation {
484 city: None,
485 country: None,
486 state: None,
487 state_code: None,
488 },
489 last_access_time: None,
490 }])
491 });
492 client
493 .expect_destroy_refresh_token()
494 .with(always(), eq("refreshtok"))
495 .times(1)
496 .returning(|_, _| Ok(()));
497 fxa.set_client(Arc::new(client));
498
499 assert!(fxa.state.refresh_token().is_some());
500 fxa.disconnect();
501 assert!(fxa.state.refresh_token().is_none());
502 }
503
504 #[test]
505 fn test_disconnect_network_errors() {
506 let config = Config::stable_dev("12345678", "https://foo.bar");
507 let mut fxa = FirefoxAccount::with_config(config);
508
509 fxa.state.force_refresh_token(RefreshToken {
510 token: "refreshtok".to_string(),
511 scopes: HashSet::default(),
512 });
513
514 let mut client = MockFxAClient::new();
515 client
516 .expect_get_devices()
517 .with(always(), eq("refreshtok"))
518 .times(1)
519 .returning(|_, _| Ok(vec![]));
520 client
521 .expect_destroy_refresh_token()
522 .with(always(), eq("refreshtok"))
523 .times(1)
524 .returning(|_, _| {
525 Err(Error::RemoteError {
526 code: 500,
527 errno: 101,
528 error: "Did not work!".to_owned(),
529 message: "Did not work!".to_owned(),
530 info: "Did not work!".to_owned(),
531 })
532 });
533 fxa.set_client(Arc::new(client));
534
535 assert!(fxa.state.refresh_token().is_some());
536 fxa.disconnect();
537 assert!(fxa.state.refresh_token().is_none());
538 }
539
540 #[test]
541 fn test_on_auth_issues() {
542 let config = Config::new("https://stable.dev.lcip.org", "12345678", "https://foo.bar");
543 let mut fxa = FirefoxAccount::with_config(config);
544
545 fxa.state.force_refresh_token(RefreshToken {
546 token: "refresh_token".to_owned(),
547 scopes: HashSet::new(),
548 });
549 fxa.state.force_current_device_id("original-device-id");
550 assert_eq!(fxa.get_auth_state(), FxaRustAuthState::Connected);
551
552 fxa.on_auth_issues();
553 assert_eq!(fxa.get_auth_state(), FxaRustAuthState::AuthIssues);
554
555 fxa.state.complete_oauth_flow(
556 vec![],
557 RefreshToken {
558 token: "refreshtok".to_owned(),
559 scopes: HashSet::default(),
560 },
561 None,
562 );
563 assert_eq!(fxa.get_auth_state(), FxaRustAuthState::Connected);
564
565 assert_eq!(fxa.state.current_device_id(), Some("original-device-id"));
569 }
570
571 #[test]
572 fn test_get_auth_state() {
573 let config = Config::new("https://stable.dev.lcip.org", "12345678", "https://foo.bar");
574 let mut fxa = FirefoxAccount::with_config(config);
575
576 fn assert_auth_state(fxa: &FirefoxAccount, correct_state: FxaRustAuthState) {
577 assert_eq!(fxa.get_auth_state(), correct_state);
578
579 let persisted = FirefoxAccount::from_json(&fxa.to_json().unwrap()).unwrap();
580 assert_eq!(persisted.get_auth_state(), correct_state);
581 }
582
583 assert_auth_state(&fxa, FxaRustAuthState::Disconnected);
585
586 fxa.state.force_refresh_token(RefreshToken {
588 token: "refresh_token".to_owned(),
589 scopes: HashSet::new(),
590 });
591 assert_auth_state(&fxa, FxaRustAuthState::Connected);
592
593 fxa.disconnect();
594 assert_auth_state(&fxa, FxaRustAuthState::Disconnected);
595
596 fxa.disconnect();
597 assert_auth_state(&fxa, FxaRustAuthState::Disconnected);
598 }
599
600 #[test]
601 fn test_get_pairing_authority_url() {
602 let config = Config::new("https://foo.bar", "12345678", "https://foo.bar");
603 let fxa = FirefoxAccount::with_config(config);
604 assert_eq!(
605 fxa.get_pairing_authority_url().unwrap().as_str(),
606 "https://foo.bar/pair"
607 );
608
609 let config = Config::release("12345678", "https://foo.bar");
610 let fxa = FirefoxAccount::with_config(config);
611 assert_eq!(
612 fxa.get_pairing_authority_url().unwrap().as_str(),
613 "https://firefox.com/pair"
614 );
615
616 let config = Config::china("12345678", "https://foo.bar");
617 let fxa = FirefoxAccount::with_config(config);
618 assert_eq!(
619 fxa.get_pairing_authority_url().unwrap().as_str(),
620 "https://firefox.com.cn/pair"
621 )
622 }
623}