1pub mod access_token;
6pub mod attached_clients;
7use super::scopes;
8use super::{
9 http_client::{
10 AuthorizationRequestParameters, IntrospectResponse as IntrospectInfo, OAuthTokenResponse,
11 },
12 scoped_keys::ScopedKeysFlow,
13 util, FirefoxAccount,
14};
15use crate::{debug, info, warn, AuthorizationParameters, Error, FxaServer, Result};
16pub use access_token::AccessTokenInfo;
17use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
18use jwcrypto::{EncryptionAlgorithm, EncryptionParameters};
19use rate_limiter::RateLimiter;
20use rc_crypto::digest;
21use serde_derive::*;
22use std::collections::{HashMap, HashSet};
23use url::Url;
24pub const OAUTH_WEBCHANNEL_REDIRECT: &str = "urn:ietf:wg:oauth:2.0:oob:oauth-redirect-webchannel";
27
28impl FirefoxAccount {
29 pub fn handle_web_channel_login(&mut self, json_payload: &str) -> Result<()> {
32 let data: serde_json::Value = serde_json::from_str(json_payload)?;
33 let token = data
34 .get("sessionToken")
35 .and_then(|v| v.as_str())
36 .ok_or(Error::NoSessionToken)?;
37 self.state.set_session_token(token.to_string());
38 Ok(())
39 }
40
41 pub fn handle_web_channel_password_change(&mut self, json_payload: &str) -> Result<()> {
44 let data: serde_json::Value = serde_json::from_str(json_payload)?;
45 let token = data
46 .get("sessionToken")
47 .and_then(|v| v.as_str())
48 .ok_or(Error::NoSessionToken)?;
49 let old_device_info = match self.get_current_device() {
52 Ok(maybe_device) => maybe_device,
53 Err(err) => {
54 warn!(
55 "Error fetching current device before password change: {:?}",
56 err
57 );
58 None
59 }
60 };
61 self.handle_session_token_change(token)?;
62 if let Some(ref device_info) = old_device_info {
63 if let Err(err) = self.replace_device(
64 &device_info.display_name,
65 &device_info.device_type,
66 &device_info.push_subscription,
67 &device_info.available_commands,
68 ) {
69 warn!(
70 "Device information restoration failed after password change: {:?}",
71 err
72 );
73 } else {
74 info!("Restored device information with new refresh token");
75 }
76 }
77 Ok(())
78 }
79
80 pub fn get_session_token(&self) -> Result<String> {
82 match self.state.session_token() {
83 Some(session_token) => Ok(session_token.to_string()),
84 None => Err(Error::NoSessionToken),
85 }
86 }
87
88 pub fn get_signed_in_user_for_web_channel(&self) -> Option<String> {
93 let token = self.state.session_token()?;
94 let profile = self.state.last_seen_profile();
95 let email = profile.map(|p| p.response.email.as_str());
96 let uid = profile.map(|p| p.response.uid.as_str());
97 Some(
98 serde_json::json!({
99 "sessionToken": token,
100 "email": email,
101 "uid": uid,
102 "verified": true,
103 })
104 .to_string(),
105 )
106 }
107
108 pub fn check_authorization_status(&mut self) -> Result<IntrospectInfo> {
110 let resp = match self.state.refresh_token() {
111 Some(refresh_token) => {
112 self.auth_circuit_breaker.check()?;
113 self.client
114 .check_refresh_token_status(self.state.config(), &refresh_token.token)?
115 }
116 None => return Err(Error::NoRefreshToken),
117 };
118 Ok(IntrospectInfo {
119 active: resp.active,
120 })
121 }
122
123 pub fn begin_pairing_flow(
131 &mut self,
132 pairing_url: &str,
133 service: &str,
134 scopes: &[&str],
135 entrypoint: &str,
136 ) -> Result<String> {
137 let mut url = self.state.config().pair_supp_url()?;
138 url.query_pairs_mut().append_pair("entrypoint", entrypoint);
139 if !service.is_empty() {
140 url.query_pairs_mut().append_pair("service", service);
141 }
142 let pairing_url = util::parse_url(pairing_url, "begin_pairing_flow")?;
143 if url.host_str() != pairing_url.host_str() {
144 let fxa_server = FxaServer::from(&url);
145 let pairing_fxa_server = FxaServer::from(&pairing_url);
146 return Err(Error::OriginMismatch(format!(
147 "fxa-server: {fxa_server}, pairing-url-fxa-server: {pairing_fxa_server}"
148 )));
149 }
150 url.set_fragment(pairing_url.fragment());
151 self.oauth_flow(url, scopes)
152 }
153
154 pub fn begin_oauth_flow(
169 &mut self,
170 service: &str,
171 scopes: &[&str],
172 entrypoint: &str,
173 ) -> Result<String> {
174 let needs_reauth =
175 self.state.last_seen_profile().is_some() && self.state.session_token().is_none();
176 let mut url = if needs_reauth {
177 self.state.config().oauth_force_auth_url()?
183 } else {
184 self.state.config().authorization_endpoint()?
185 };
186
187 info!("starting oauth flow via {url} for service={service:?}, scopes={scopes:?}, entrypoint={entrypoint:?}");
188 url.query_pairs_mut()
189 .append_pair("action", "email")
190 .append_pair("response_type", "code")
191 .append_pair("entrypoint", entrypoint);
192
193 if !service.is_empty() {
194 url.query_pairs_mut().append_pair("service", service);
195 }
196 if let Some(cached_profile) = self.state.last_seen_profile() {
197 url.query_pairs_mut()
198 .append_pair("email", &cached_profile.response.email);
199 }
200
201 debug!("oauth flow final set of requested scopes now {scopes:?}");
202 self.oauth_flow(url, scopes)
203 }
204
205 pub fn authorize_code_using_session_token(
215 &self,
216 auth_params: AuthorizationParameters,
217 ) -> Result<String> {
218 let session_token = self.get_session_token()?;
219
220 let allowed_scopes = self.client.get_scoped_key_data(
223 self.state.config(),
224 &session_token,
225 &auth_params.client_id,
226 &auth_params.scope.join(" "),
227 )?;
228
229 if let Some(not_allowed_scope) = auth_params
230 .scope
231 .iter()
232 .find(|scope| !allowed_scopes.contains_key(*scope))
233 {
234 return Err(Error::ScopeNotAllowed(
235 auth_params.client_id.clone(),
236 not_allowed_scope.clone(),
237 ));
238 }
239
240 let keys_jwe = if let Some(keys_jwk) = auth_params.keys_jwk {
241 let mut scoped_keys = HashMap::new();
242 allowed_scopes
243 .iter()
244 .try_for_each(|(scope, _)| -> Result<()> {
245 scoped_keys.insert(
246 scope,
247 self.state
248 .get_scoped_key(scope)
249 .ok_or_else(|| Error::NoScopedKey(scope.clone()))?,
250 );
251 Ok(())
252 })?;
253 let scoped_keys = serde_json::to_string(&scoped_keys)?;
254 let keys_jwk = URL_SAFE_NO_PAD.decode(keys_jwk)?;
255 let jwk = serde_json::from_slice(&keys_jwk)?;
256 Some(jwcrypto::encrypt_to_jwe(
257 scoped_keys.as_bytes(),
258 EncryptionParameters::ECDH_ES {
259 enc: EncryptionAlgorithm::A256GCM,
260 peer_jwk: &jwk,
261 },
262 )?)
263 } else {
264 None
265 };
266 let auth_request_params = AuthorizationRequestParameters {
267 client_id: auth_params.client_id,
268 scope: auth_params.scope.join(" "),
269 state: auth_params.state,
270 access_type: auth_params.access_type,
271 code_challenge: auth_params.code_challenge,
272 code_challenge_method: auth_params.code_challenge_method,
273 keys_jwe,
274 };
275
276 let resp = self.client.create_authorization_code_using_session_token(
277 self.state.config(),
278 &session_token,
279 auth_request_params,
280 )?;
281
282 Ok(resp.code)
283 }
284
285 fn oauth_flow(&mut self, mut url: Url, scopes: &[&str]) -> Result<String> {
286 self.clear_access_token_cache();
287 let state = util::random_base64_url_string(16)?;
288 let code_verifier = util::random_base64_url_string(43)?;
289 let code_challenge = digest::digest(&digest::SHA256, code_verifier.as_bytes())?;
290 let code_challenge = URL_SAFE_NO_PAD.encode(code_challenge);
291 let scoped_keys_flow = ScopedKeysFlow::with_random_key()?;
292 let jwk = scoped_keys_flow.get_public_key_jwk()?;
293 let jwk_json = serde_json::to_string(&jwk)?;
294 let keys_jwk = URL_SAFE_NO_PAD.encode(jwk_json);
295 url.query_pairs_mut()
296 .append_pair("client_id", &self.state.config().client_id)
297 .append_pair("scope", &scopes.join(" "))
298 .append_pair("state", &state)
299 .append_pair("code_challenge_method", "S256")
300 .append_pair("code_challenge", &code_challenge)
301 .append_pair("access_type", "offline")
302 .append_pair("keys_jwk", &keys_jwk);
303
304 if self.state.config().redirect_uri == OAUTH_WEBCHANNEL_REDIRECT {
305 url.query_pairs_mut()
306 .append_pair("context", "oauth_webchannel_v1");
307 } else {
308 url.query_pairs_mut()
309 .append_pair("redirect_uri", &self.state.config().redirect_uri);
310 }
311
312 self.state.begin_oauth_flow(
313 state,
314 OAuthFlow {
315 scoped_keys_flow: Some(scoped_keys_flow),
316 code_verifier,
317 },
318 );
319 Ok(url.to_string())
320 }
321
322 pub fn complete_oauth_flow(&mut self, code: &str, state: &str) -> Result<()> {
328 self.clear_access_token_cache();
329 let oauth_flow = match self.state.pop_oauth_flow(state) {
330 Some(oauth_flow) => oauth_flow,
331 None => return Err(Error::UnknownOAuthState),
332 };
333 let resp = self.client.create_refresh_token_using_authorization_code(
336 self.state.config(),
337 self.state.session_token(),
338 code,
339 &oauth_flow.code_verifier,
340 )?;
341 info!(
342 "complete oauth flow - new session token={}, new refresh token={}",
343 resp.session_token.is_some(),
344 resp.refresh_token.is_some()
345 );
346 self.handle_oauth_response(resp, oauth_flow.scoped_keys_flow)?;
347 Ok(())
348 }
349
350 pub fn cancel_existing_oauth_flows(&mut self) {
352 self.state.clear_oauth_flows();
353 }
354
355 pub(crate) fn handle_oauth_response(
356 &mut self,
357 resp: OAuthTokenResponse,
358 scoped_keys_flow: Option<ScopedKeysFlow>,
359 ) -> Result<()> {
360 let sync_scope_granted = resp.scope.split(' ').any(|s| s == scopes::OLD_SYNC);
361 let scoped_keys = match resp.keys_jwe {
362 Some(ref jwe) => {
363 let scoped_keys_flow = scoped_keys_flow.ok_or(Error::ApiClientError(
364 "Got a JWE but have no JWK to decrypt it.",
365 ))?;
366 let decrypted_keys = scoped_keys_flow.decrypt_keys_jwe(jwe)?;
367 let scoped_keys: serde_json::Map<String, serde_json::Value> =
368 serde_json::from_str(&decrypted_keys)?;
369 if sync_scope_granted && !scoped_keys.contains_key(scopes::OLD_SYNC) {
370 error_support::report_error!(
371 "fxaclient-scoped-key",
372 "Sync scope granted, but no sync scoped key (scope granted: {}, key scopes: {})",
373 resp.scope,
374 scoped_keys.keys().map(|s| s.as_ref()).collect::<Vec<&str>>().join(", ")
375 );
376 }
377 scoped_keys
378 .into_iter()
379 .map(|(scope, key)| Ok((scope, serde_json::from_value(key)?)))
380 .collect::<Result<Vec<_>>>()?
381 }
382 None => {
383 if sync_scope_granted {
384 error_support::report_error!(
385 "fxaclient-scoped-key",
386 "Sync scope granted, but keys_jwe is None"
387 );
388 }
389 vec![]
390 }
391 };
392
393 if let Err(err) = self
397 .client
398 .destroy_access_token(self.state.config(), &resp.access_token)
399 {
400 warn!("Access token destruction failure: {:?}", err);
401 }
402 let old_refresh_token = self.state.refresh_token().cloned();
403 let mut new_refresh_token = RefreshToken::new(
404 resp.refresh_token
405 .ok_or(Error::ApiClientError("No refresh token in response"))?,
406 resp.scope,
407 );
408 let old_device_info = match old_refresh_token {
411 Some(_) => match self.get_current_device() {
412 Ok(maybe_device) => maybe_device,
413 Err(err) => {
414 warn!("Error while getting previous device information: {:?}", err);
415 None
416 }
417 },
418 None => None,
419 };
420
421 if let Some(ref old_refresh_token) = old_refresh_token {
422 let existing_scopes = &old_refresh_token.scopes;
427 let all_scopes: HashSet<_> = existing_scopes
428 .union(&new_refresh_token.scopes)
429 .cloned()
430 .collect();
431 if all_scopes != new_refresh_token.scopes {
432 if let Some(session_token) = self.state.session_token() {
433 info!("New refresh token is missing some of our old scopes, upgrading");
434 let scopes_slice = all_scopes.iter().map(|s| s.as_ref()).collect::<Vec<&str>>();
439 let merged_refresh_token_resp =
440 self.client.create_refresh_token_using_session_token(
441 self.state.config(),
442 session_token,
443 &scopes_slice,
444 )?;
445 let Some(merged_refresh_token_str) = merged_refresh_token_resp.refresh_token
446 else {
447 log::error!("server failed to give a new refresh token");
448 return Err(Error::NoRefreshToken);
449 };
450
451 if let Err(err) = self
453 .client
454 .destroy_refresh_token(self.state.config(), &new_refresh_token.token)
455 {
456 warn!(
457 "Refresh token destruction failure of new refresh token: {:?}",
458 err
459 );
460 }
461
462 new_refresh_token = RefreshToken::new(
463 merged_refresh_token_str,
464 merged_refresh_token_resp.scope,
465 );
466 } else {
467 warn!("New refresh token is missing some of our old scopes, but don't have a session token to use to upgrade");
468 }
469 } else {
470 info!("New refresh token has the same scopes we started with");
472 }
473
474 if let Err(err) = self
477 .client
478 .destroy_refresh_token(self.state.config(), &old_refresh_token.token)
479 {
480 warn!(
481 "Refresh token destruction failure of old refresh token: {:?}",
482 err
483 );
484 }
485 self.state.clear_refresh_token();
488 }
489
490 self.state
491 .complete_oauth_flow(scoped_keys, new_refresh_token, resp.session_token);
492 if let Some(ref device_info) = old_device_info {
493 if let Err(err) = self.replace_device(
494 &device_info.display_name,
495 &device_info.device_type,
496 &device_info.push_subscription,
497 &device_info.available_commands,
498 ) {
499 warn!("Device information restoration failed: {:?}", err);
500 }
501 info!("restored device information with new refresh token");
502 }
503 Ok(())
504 }
505
506 pub fn handle_session_token_change(&mut self, session_token: &str) -> Result<()> {
514 let old_refresh_token = self.state.refresh_token().ok_or(Error::NoRefreshToken)?;
515 let scopes: Vec<&str> = old_refresh_token.scopes.iter().map(AsRef::as_ref).collect();
516 let resp = self.client.create_refresh_token_using_session_token(
517 self.state.config(),
518 session_token,
519 &scopes,
520 )?;
521 let new_refresh_token = resp
522 .refresh_token
523 .ok_or(Error::ApiClientError("No refresh token in response"))?;
524 self.state.update_tokens(
525 session_token.to_owned(),
526 RefreshToken {
527 token: new_refresh_token,
528 scopes: resp.scope.split(' ').map(ToString::to_string).collect(),
529 },
530 );
531 self.clear_devices_and_attached_clients_cache();
532 Ok(())
533 }
534}
535
536const AUTH_CIRCUIT_BREAKER_CAPACITY: u8 = 5;
537const AUTH_CIRCUIT_BREAKER_RENEWAL_RATE: f32 = 3.0 / 60.0 / 1000.0; #[derive(Clone, Copy)]
540pub(crate) struct AuthCircuitBreaker {
541 rate_limiter: RateLimiter,
542}
543
544impl Default for AuthCircuitBreaker {
545 fn default() -> Self {
546 AuthCircuitBreaker {
547 rate_limiter: RateLimiter::new(
548 AUTH_CIRCUIT_BREAKER_CAPACITY,
549 AUTH_CIRCUIT_BREAKER_RENEWAL_RATE,
550 ),
551 }
552 }
553}
554
555impl AuthCircuitBreaker {
556 pub(crate) fn check(&mut self) -> Result<()> {
557 if !self.rate_limiter.check() {
558 return Err(Error::AuthCircuitBreakerError);
559 }
560 Ok(())
561 }
562}
563
564impl TryFrom<Url> for AuthorizationParameters {
565 type Error = Error;
566
567 fn try_from(url: Url) -> Result<Self> {
568 let query_map: HashMap<String, String> = url.query_pairs().into_owned().collect();
569 let scope = query_map
570 .get("scope")
571 .cloned()
572 .ok_or(Error::MissingUrlParameter("scope"))?;
573 let client_id = query_map
574 .get("client_id")
575 .cloned()
576 .ok_or(Error::MissingUrlParameter("client_id"))?;
577 let state = query_map
578 .get("state")
579 .cloned()
580 .ok_or(Error::MissingUrlParameter("state"))?;
581 let access_type = query_map
582 .get("access_type")
583 .cloned()
584 .ok_or(Error::MissingUrlParameter("access_type"))?;
585 let code_challenge = query_map.get("code_challenge").cloned();
586 let code_challenge_method = query_map.get("code_challenge_method").cloned();
587 let keys_jwk = query_map.get("keys_jwk").cloned();
588 Ok(Self {
589 client_id,
590 scope: scope.split_whitespace().map(|s| s.to_string()).collect(),
591 state,
592 access_type,
593 code_challenge,
594 code_challenge_method,
595 keys_jwk,
596 })
597 }
598}
599
600#[derive(Clone, Serialize, Deserialize)]
601pub struct RefreshToken {
602 pub token: String,
603 pub scopes: HashSet<String>,
604}
605
606impl RefreshToken {
607 pub fn new(token: String, scopes: String) -> Self {
608 Self {
609 token,
610 scopes: scopes
611 .split_ascii_whitespace()
612 .map(ToString::to_string)
613 .collect(),
614 }
615 }
616}
617
618impl std::fmt::Debug for RefreshToken {
619 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
620 f.debug_struct("RefreshToken")
621 .field("scopes", &self.scopes)
622 .finish()
623 }
624}
625
626pub struct OAuthFlow {
627 pub scoped_keys_flow: Option<ScopedKeysFlow>,
628 pub code_verifier: String,
629}
630
631impl From<IntrospectInfo> for crate::AuthorizationInfo {
632 fn from(r: IntrospectInfo) -> Self {
633 crate::AuthorizationInfo { active: r.active }
634 }
635}
636
637#[cfg(test)]
638impl FirefoxAccount {
639 pub fn set_session_token(&mut self, session_token: &str) {
640 self.state.set_session_token(session_token.to_owned());
641 }
642}
643
644#[cfg(test)]
645mod tests {
646 use super::super::{http_client::*, Config};
647 use super::*;
648 use mockall::predicate::always;
649 use mockall::predicate::eq;
650 use std::borrow::Cow;
651 use std::collections::HashMap;
652 use std::sync::Arc;
653
654 #[test]
655 fn test_oauth_flow_url() {
656 nss_as::ensure_initialized();
657 let config = Config::new_with_mock_well_known_fxa_client_configuration(
658 "https://mock-fxa.example.com",
659 "12345678",
660 "https://foo.bar",
661 );
662 let mut fxa = FirefoxAccount::with_config(config);
663 let url = fxa
664 .begin_oauth_flow("", &["profile"], "test_oauth_flow_url")
665 .unwrap();
666 let flow_url = Url::parse(&url).unwrap();
667
668 assert_eq!(flow_url.path(), "/authorization");
669
670 let mut pairs = flow_url.query_pairs();
671 assert_eq!(pairs.count(), 11);
672 assert_eq!(
673 pairs.next(),
674 Some((Cow::Borrowed("action"), Cow::Borrowed("email")))
675 );
676 assert_eq!(
677 pairs.next(),
678 Some((Cow::Borrowed("response_type"), Cow::Borrowed("code")))
679 );
680 assert_eq!(
681 pairs.next(),
682 Some((
683 Cow::Borrowed("entrypoint"),
684 Cow::Borrowed("test_oauth_flow_url")
685 ))
686 );
687 assert_eq!(
688 pairs.next(),
689 Some((Cow::Borrowed("client_id"), Cow::Borrowed("12345678")))
690 );
691
692 assert_eq!(
693 pairs.next(),
694 Some((Cow::Borrowed("scope"), Cow::Borrowed("profile")))
695 );
696 let state_param = pairs.next().unwrap();
697 assert_eq!(state_param.0, Cow::Borrowed("state"));
698 assert_eq!(state_param.1.len(), 22);
699 assert_eq!(
700 pairs.next(),
701 Some((
702 Cow::Borrowed("code_challenge_method"),
703 Cow::Borrowed("S256")
704 ))
705 );
706 let code_challenge_param = pairs.next().unwrap();
707 assert_eq!(code_challenge_param.0, Cow::Borrowed("code_challenge"));
708 assert_eq!(code_challenge_param.1.len(), 43);
709 assert_eq!(
710 pairs.next(),
711 Some((Cow::Borrowed("access_type"), Cow::Borrowed("offline")))
712 );
713 let keys_jwk = pairs.next().unwrap();
714 assert_eq!(keys_jwk.0, Cow::Borrowed("keys_jwk"));
715 assert_eq!(keys_jwk.1.len(), 168);
716
717 assert_eq!(
718 pairs.next(),
719 Some((
720 Cow::Borrowed("redirect_uri"),
721 Cow::Borrowed("https://foo.bar")
722 ))
723 );
724 }
725
726 #[test]
727 fn test_force_auth_url() {
728 nss_as::ensure_initialized();
729 let config = Config::stable_dev("12345678", "https://foo.bar");
730 let mut fxa = FirefoxAccount::with_config(config);
731 let email = "test@example.com";
732 fxa.add_cached_profile("123", email);
733 let url = fxa
734 .begin_oauth_flow("", &["profile"], "test_force_auth_url")
735 .unwrap();
736 let url = Url::parse(&url).unwrap();
737 assert_eq!(url.path(), "/oauth/force_auth");
738 let mut pairs = url.query_pairs();
739 assert_eq!(
740 pairs.find(|e| e.0 == "email"),
741 Some((Cow::Borrowed("email"), Cow::Borrowed(email),))
742 );
743 }
744
745 #[test]
746 fn test_webchannel_context_url() {
747 nss_as::ensure_initialized();
748 const SCOPES: &[&str] = &["https://identity.mozilla.com/apps/oldsync"];
749 let config = Config::new_with_mock_well_known_fxa_client_configuration(
750 "https://mock-fxa.example.com",
751 "12345678",
752 "urn:ietf:wg:oauth:2.0:oob:oauth-redirect-webchannel",
753 );
754 let mut fxa = FirefoxAccount::with_config(config);
755 let url = fxa
756 .begin_oauth_flow("", SCOPES, "test_webchannel_context_url")
757 .unwrap();
758 let url = Url::parse(&url).unwrap();
759 let query_params: HashMap<_, _> = url.query_pairs().into_owned().collect();
760 let context = &query_params["context"];
761 assert_eq!(context, "oauth_webchannel_v1");
762 assert_eq!(query_params.get("redirect_uri"), None);
763 }
764
765 #[test]
766 fn test_webchannel_pairing_context_url() {
767 nss_as::ensure_initialized();
768 const SCOPES: &[&str] = &["https://identity.mozilla.com/apps/oldsync"];
769 const PAIRING_URL: &str = "https://accounts.firefox.com/pair#channel_id=658db7fe98b249a5897b884f98fb31b7&channel_key=1hIDzTj5oY2HDeSg_jA2DhcOcAn5Uqq0cAYlZRNUIo4";
770
771 let config = Config::new(
772 "https://accounts.firefox.com",
773 "12345678",
774 "urn:ietf:wg:oauth:2.0:oob:oauth-redirect-webchannel",
775 );
776 let mut fxa = FirefoxAccount::with_config(config);
777 let url = fxa
778 .begin_pairing_flow(
779 PAIRING_URL,
780 "service",
781 SCOPES,
782 "test_webchannel_pairing_context_url",
783 )
784 .unwrap();
785 let url = Url::parse(&url).unwrap();
786 let query_params: HashMap<_, _> = url.query_pairs().into_owned().collect();
787 let context = &query_params["context"];
788 assert_eq!(context, "oauth_webchannel_v1");
789 assert_eq!(query_params.get("redirect_uri"), None);
790 }
791
792 #[test]
793 fn test_pairing_flow_url() {
794 nss_as::ensure_initialized();
795 const SCOPES: &[&str] = &["https://identity.mozilla.com/apps/oldsync"];
796 const PAIRING_URL: &str = "https://accounts.firefox.com/pair#channel_id=658db7fe98b249a5897b884f98fb31b7&channel_key=1hIDzTj5oY2HDeSg_jA2DhcOcAn5Uqq0cAYlZRNUIo4";
797 const EXPECTED_URL: &str = "https://accounts.firefox.com/pair/supp?client_id=12345678&redirect_uri=https%3A%2F%2Ffoo.bar&scope=https%3A%2F%2Fidentity.mozilla.com%2Fapps%2Foldsync&state=SmbAA_9EA5v1R2bgIPeWWw&code_challenge_method=S256&code_challenge=ZgHLPPJ8XYbXpo7VIb7wFw0yXlTa6MUOVfGiADt0JSM&access_type=offline&keys_jwk=eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6Ing5LUltQjJveDM0LTV6c1VmbW5sNEp0Ti14elV2eFZlZXJHTFRXRV9BT0kiLCJ5IjoiNXBKbTB3WGQ4YXdHcm0zREl4T1pWMl9qdl9tZEx1TWlMb1RkZ1RucWJDZyJ9#channel_id=658db7fe98b249a5897b884f98fb31b7&channel_key=1hIDzTj5oY2HDeSg_jA2DhcOcAn5Uqq0cAYlZRNUIo4";
798
799 let config = Config::new(
800 "https://accounts.firefox.com",
801 "12345678",
802 "https://foo.bar",
803 );
804
805 let mut fxa = FirefoxAccount::with_config(config);
806 let url = fxa
807 .begin_pairing_flow(PAIRING_URL, "", SCOPES, "test_pairing_flow_url")
808 .unwrap();
809 let flow_url = Url::parse(&url).unwrap();
810 let expected_parsed_url = Url::parse(EXPECTED_URL).unwrap();
811
812 assert_eq!(flow_url.host_str(), Some("accounts.firefox.com"));
813 assert_eq!(flow_url.path(), "/pair/supp");
814 assert_eq!(flow_url.fragment(), expected_parsed_url.fragment());
815
816 let mut pairs = flow_url.query_pairs();
817 assert_eq!(pairs.count(), 9);
818 assert_eq!(
819 pairs.next(),
820 Some((
821 Cow::Borrowed("entrypoint"),
822 Cow::Borrowed("test_pairing_flow_url")
823 ))
824 );
825 assert_eq!(
826 pairs.next(),
827 Some((Cow::Borrowed("client_id"), Cow::Borrowed("12345678")))
828 );
829 assert_eq!(
830 pairs.next(),
831 Some((
832 Cow::Borrowed("scope"),
833 Cow::Borrowed("https://identity.mozilla.com/apps/oldsync")
834 ))
835 );
836
837 let state_param = pairs.next().unwrap();
838 assert_eq!(state_param.0, Cow::Borrowed("state"));
839 assert_eq!(state_param.1.len(), 22);
840 assert_eq!(
841 pairs.next(),
842 Some((
843 Cow::Borrowed("code_challenge_method"),
844 Cow::Borrowed("S256")
845 ))
846 );
847 let code_challenge_param = pairs.next().unwrap();
848 assert_eq!(code_challenge_param.0, Cow::Borrowed("code_challenge"));
849 assert_eq!(code_challenge_param.1.len(), 43);
850 assert_eq!(
851 pairs.next(),
852 Some((Cow::Borrowed("access_type"), Cow::Borrowed("offline")))
853 );
854 let keys_jwk = pairs.next().unwrap();
855 assert_eq!(keys_jwk.0, Cow::Borrowed("keys_jwk"));
856 assert_eq!(keys_jwk.1.len(), 168);
857
858 assert_eq!(
859 pairs.next(),
860 Some((
861 Cow::Borrowed("redirect_uri"),
862 Cow::Borrowed("https://foo.bar")
863 ))
864 );
865 }
866
867 #[test]
868 fn test_pairing_flow_origin_mismatch() {
869 nss_as::ensure_initialized();
870 static PAIRING_URL: &str = "https://bad.origin.com/pair#channel_id=foo&channel_key=bar";
871 let config = Config::stable_dev("12345678", "https://foo.bar");
872 let mut fxa = FirefoxAccount::with_config(config);
873 let url = fxa.begin_pairing_flow(
874 PAIRING_URL,
875 "service",
876 &["https://identity.mozilla.com/apps/oldsync"],
877 "test_pairiong_flow_origin_mismatch",
878 );
879
880 assert!(url.is_err());
881
882 match url {
883 Ok(_) => {
884 panic!("should have error");
885 }
886 Err(err) => match err {
887 Error::OriginMismatch { .. } => {}
888 _ => panic!("error not OriginMismatch"),
889 },
890 }
891 }
892
893 #[test]
894 fn test_check_authorization_status() {
895 nss_as::ensure_initialized();
896 let config = Config::stable_dev("12345678", "https://foo.bar");
897 let mut fxa = FirefoxAccount::with_config(config);
898
899 let refresh_token_scopes = std::collections::HashSet::new();
900 fxa.state.force_refresh_token(RefreshToken {
901 token: "refresh_token".to_owned(),
902 scopes: refresh_token_scopes,
903 });
904
905 let mut client = MockFxAClient::new();
906 client
907 .expect_check_refresh_token_status()
908 .with(always(), eq("refresh_token"))
909 .times(1)
910 .returning(|_, _| Ok(IntrospectResponse { active: true }));
911 fxa.set_client(Arc::new(client));
912
913 let auth_status = fxa.check_authorization_status().unwrap();
914 assert!(auth_status.active);
915 }
916
917 #[test]
918 fn test_check_authorization_status_circuit_breaker() {
919 nss_as::ensure_initialized();
920 let config = Config::stable_dev("12345678", "https://foo.bar");
921 let mut fxa = FirefoxAccount::with_config(config);
922
923 let refresh_token_scopes = std::collections::HashSet::new();
924 fxa.state.force_refresh_token(RefreshToken {
925 token: "refresh_token".to_owned(),
926 scopes: refresh_token_scopes,
927 });
928
929 let mut client = MockFxAClient::new();
930 client
933 .expect_check_refresh_token_status()
934 .with(always(), eq("refresh_token"))
935 .returning(|_, _| Ok(IntrospectResponse { active: true }));
936 client
937 .expect_check_refresh_token_status()
938 .with(always(), eq("refresh_token"))
939 .returning(|_, _| Ok(IntrospectResponse { active: true }));
940 client
941 .expect_check_refresh_token_status()
942 .with(always(), eq("refresh_token"))
943 .returning(|_, _| Ok(IntrospectResponse { active: true }));
944 client
945 .expect_check_refresh_token_status()
946 .with(always(), eq("refresh_token"))
947 .returning(|_, _| Ok(IntrospectResponse { active: true }));
948 client
949 .expect_check_refresh_token_status()
950 .with(always(), eq("refresh_token"))
951 .returning(|_, _| Ok(IntrospectResponse { active: true }));
952 fxa.set_client(Arc::new(client));
954
955 for _ in 0..5 {
956 assert!(fxa.check_authorization_status().is_ok());
957 }
958 match fxa.check_authorization_status() {
959 Ok(_) => unreachable!("should not happen"),
960 Err(err) => assert!(matches!(err, Error::AuthCircuitBreakerError)),
961 }
962 }
963
964 use crate::internal::scopes::{self, OLD_SYNC};
965
966 #[test]
967 fn test_auth_code_pair_valid_not_allowed_scope() {
968 nss_as::ensure_initialized();
969 let config = Config::stable_dev("12345678", "https://foo.bar");
970 let mut fxa = FirefoxAccount::with_config(config);
971 fxa.set_session_token("session");
972 let mut client = MockFxAClient::new();
973 let not_allowed_scope = "https://identity.mozilla.com/apps/lockbox";
974 let expected_scopes = scopes::OLD_SYNC
975 .chars()
976 .chain(std::iter::once(' '))
977 .chain(not_allowed_scope.chars())
978 .collect::<String>();
979 client
980 .expect_get_scoped_key_data()
981 .with(always(), eq("session"), eq("12345678"), eq(expected_scopes))
982 .times(1)
983 .returning(|_, _, _, _| {
984 Err(Error::RemoteError {
985 code: 400,
986 errno: 163,
987 error: "Invalid Scopes".to_string(),
988 message: "Not allowed to request scopes".to_string(),
989 info: "fyi, there was a server error".to_string(),
990 })
991 });
992 fxa.set_client(Arc::new(client));
993 let auth_params = AuthorizationParameters {
994 client_id: "12345678".to_string(),
995 scope: vec![scopes::OLD_SYNC.to_string(), not_allowed_scope.to_string()],
996 state: "somestate".to_string(),
997 access_type: "offline".to_string(),
998 code_challenge: None,
999 code_challenge_method: None,
1000 keys_jwk: None,
1001 };
1002 let res = fxa.authorize_code_using_session_token(auth_params);
1003 assert!(res.is_err());
1004 let err = res.unwrap_err();
1005 if let Error::RemoteError {
1006 code,
1007 errno,
1008 error: _,
1009 message: _,
1010 info: _,
1011 } = err
1012 {
1013 assert_eq!(code, 400);
1014 assert_eq!(errno, 163); } else {
1016 panic!("Should return an error from the server specifying that the requested scopes are not allowed");
1017 }
1018 }
1019
1020 #[test]
1021 fn test_auth_code_pair_invalid_scope_not_allowed() {
1022 nss_as::ensure_initialized();
1023 let config = Config::stable_dev("12345678", "https://foo.bar");
1024 let mut fxa = FirefoxAccount::with_config(config);
1025 fxa.set_session_token("session");
1026 let mut client = MockFxAClient::new();
1027 let invalid_scope = "IamAnInvalidScope";
1028 let expected_scopes = scopes::OLD_SYNC
1029 .chars()
1030 .chain(std::iter::once(' '))
1031 .chain(invalid_scope.chars())
1032 .collect::<String>();
1033 client
1034 .expect_get_scoped_key_data()
1035 .with(always(), eq("session"), eq("12345678"), eq(expected_scopes))
1036 .times(1)
1037 .returning(|_, _, _, _| {
1038 let mut server_ret = HashMap::new();
1039 server_ret.insert(
1040 scopes::OLD_SYNC.to_string(),
1041 ScopedKeyDataResponse {
1042 key_rotation_secret: "IamASecret".to_string(),
1043 key_rotation_timestamp: 100,
1044 identifier: "".to_string(),
1045 },
1046 );
1047 Ok(server_ret)
1048 });
1049 fxa.set_client(Arc::new(client));
1050
1051 let auth_params = AuthorizationParameters {
1052 client_id: "12345678".to_string(),
1053 scope: vec![scopes::OLD_SYNC.to_string(), invalid_scope.to_string()],
1054 state: "somestate".to_string(),
1055 access_type: "offline".to_string(),
1056 code_challenge: None,
1057 code_challenge_method: None,
1058 keys_jwk: None,
1059 };
1060 let res = fxa.authorize_code_using_session_token(auth_params);
1061 assert!(res.is_err());
1062 let err = res.unwrap_err();
1063 if let Error::ScopeNotAllowed(client_id, scope) = err {
1064 assert_eq!(client_id, "12345678");
1065 assert_eq!(scope, "IamAnInvalidScope");
1066 } else {
1067 panic!("Should return an error that specifies the scope that is not allowed");
1068 }
1069 }
1070
1071 #[test]
1072 fn test_auth_code_pair_scope_not_in_state() {
1073 nss_as::ensure_initialized();
1074 let config = Config::stable_dev("12345678", "https://foo.bar");
1075 let mut fxa = FirefoxAccount::with_config(config);
1076 fxa.set_session_token("session");
1077 let mut client = MockFxAClient::new();
1078 client
1079 .expect_get_scoped_key_data()
1080 .with(
1081 always(),
1082 eq("session"),
1083 eq("12345678"),
1084 eq(scopes::OLD_SYNC),
1085 )
1086 .times(1)
1087 .returning(|_, _, _, _| {
1088 let mut server_ret = HashMap::new();
1089 server_ret.insert(
1090 scopes::OLD_SYNC.to_string(),
1091 ScopedKeyDataResponse {
1092 key_rotation_secret: "IamASecret".to_string(),
1093 key_rotation_timestamp: 100,
1094 identifier: "".to_string(),
1095 },
1096 );
1097 Ok(server_ret)
1098 });
1099 fxa.set_client(Arc::new(client));
1100 let auth_params = AuthorizationParameters {
1101 client_id: "12345678".to_string(),
1102 scope: vec![scopes::OLD_SYNC.to_string()],
1103 state: "somestate".to_string(),
1104 access_type: "offline".to_string(),
1105 code_challenge: None,
1106 code_challenge_method: None,
1107 keys_jwk: Some("IAmAVerySecretKeysJWkInBase64".to_string()),
1108 };
1109 let res = fxa.authorize_code_using_session_token(auth_params);
1110 assert!(res.is_err());
1111 let err = res.unwrap_err();
1112 if let Error::NoScopedKey(scope) = err {
1113 assert_eq!(scope, scopes::OLD_SYNC.to_string());
1114 } else {
1115 panic!("Should return an error that specifies the scope that is not in the state");
1116 }
1117 }
1118
1119 #[test]
1120 fn test_handle_web_channel_login_sets_session_token() {
1121 nss_as::ensure_initialized();
1122 let config = Config::stable_dev("12345678", "https://foo.bar");
1123 let mut fxa = FirefoxAccount::with_config(config);
1124 fxa.handle_web_channel_login(
1125 r#"{"sessionToken":"mock_session_token","uid":"mock_uid","email":"mock@example.com","verified":true}"#,
1126 )
1127 .unwrap();
1128 assert_eq!(fxa.get_session_token().unwrap(), "mock_session_token");
1129 }
1130
1131 #[test]
1132 fn test_oauth_request_sent_with_session_when_available() {
1133 nss_as::ensure_initialized();
1134 let config = Config::new_with_mock_well_known_fxa_client_configuration(
1135 "mock-fxa.example.com",
1136 "12345678",
1137 "https://foo.bar",
1138 );
1139 let mut fxa = FirefoxAccount::with_config(config);
1140 let url = fxa
1141 .begin_oauth_flow("", &[OLD_SYNC, "profile"], "test_entrypoint")
1142 .unwrap();
1143 let url = Url::parse(&url).unwrap();
1144 let state = url.query_pairs().find(|(name, _)| name == "state").unwrap();
1145 let mut client = MockFxAClient::new();
1146
1147 client
1148 .expect_create_refresh_token_using_authorization_code()
1149 .withf(|_, session_token, code, _| {
1150 matches!(session_token, Some("mock_session_token")) && code == "mock_code"
1151 })
1152 .times(1)
1153 .returning(|_, _, _, _| {
1154 Ok(OAuthTokenResponse {
1155 keys_jwe: None,
1156 refresh_token: Some("refresh_token".to_string()),
1157 session_token: None,
1158 expires_in: 1,
1159 scope: "profile".to_string(),
1160 access_token: "access_token".to_string(),
1161 })
1162 });
1163 client
1164 .expect_destroy_access_token()
1165 .with(always(), always())
1166 .times(1)
1167 .returning(|_, _| Ok(()));
1168 fxa.set_client(Arc::new(client));
1169 fxa.set_session_token("mock_session_token");
1170
1171 fxa.complete_oauth_flow("mock_code", state.1.as_ref())
1172 .unwrap();
1173 }
1174
1175 fn make_mock_device(name: &str) -> GetDeviceResponse {
1176 use sync15::DeviceType;
1177 GetDeviceResponse {
1178 common: DeviceResponseCommon {
1179 id: "device1".into(),
1180 display_name: name.to_string(),
1181 device_type: DeviceType::Desktop,
1182 push_subscription: None,
1183 available_commands: HashMap::new(),
1184 push_endpoint_expired: false,
1185 },
1186 is_current_device: true,
1187 location: DeviceLocation {
1188 city: None,
1189 country: None,
1190 state: None,
1191 state_code: None,
1192 },
1193 last_access_time: None,
1194 }
1195 }
1196
1197 fn make_mock_update_device_response() -> UpdateDeviceResponse {
1198 use sync15::DeviceType;
1199 UpdateDeviceResponse {
1200 id: "device1".into(),
1201 display_name: "Test Device".to_string(),
1202 device_type: DeviceType::Desktop,
1203 push_subscription: None,
1204 available_commands: HashMap::new(),
1205 push_endpoint_expired: false,
1206 }
1207 }
1208
1209 #[test]
1212 fn test_complete_oauth_flow_merges_scopes_and_restores_device() {
1213 nss_as::ensure_initialized();
1214 let config = Config::new_with_mock_well_known_fxa_client_configuration(
1215 "mock-fxa.example.com",
1216 "12345678",
1217 "https://foo.bar",
1218 );
1219 let mut fxa = FirefoxAccount::with_config(config);
1220
1221 let url = fxa
1223 .begin_oauth_flow("", &["new_scope"], "test_entrypoint")
1224 .unwrap();
1225 let url = Url::parse(&url).unwrap();
1226 let state = url.query_pairs().find(|(name, _)| name == "state").unwrap();
1227
1228 fxa.state.force_refresh_token(RefreshToken {
1230 token: "old_refresh".to_string(),
1231 scopes: ["profile".to_string()].into(),
1232 });
1233 fxa.set_session_token("mock_session_token");
1234
1235 let mut client = MockFxAClient::new();
1236
1237 client
1239 .expect_create_refresh_token_using_authorization_code()
1240 .times(1)
1241 .returning(|_, _, _, _| {
1242 Ok(OAuthTokenResponse {
1243 keys_jwe: None,
1244 refresh_token: Some("new_narrow_refresh".to_string()),
1245 session_token: None,
1246 expires_in: 3600,
1247 scope: "new_scope".to_string(),
1248 access_token: "access_token".to_string(),
1249 })
1250 });
1251
1252 client
1254 .expect_destroy_access_token()
1255 .with(always(), always())
1256 .times(1)
1257 .returning(|_, _| Ok(()));
1258
1259 client
1261 .expect_get_devices()
1262 .with(always(), eq("old_refresh"))
1263 .times(1)
1264 .returning(|_, _| Ok(vec![make_mock_device("Test Device")]));
1265
1266 client
1268 .expect_create_refresh_token_using_session_token()
1269 .withf(|_, session_token, _| session_token == "mock_session_token")
1270 .times(1)
1271 .returning(|_, _, _| {
1272 Ok(OAuthTokenResponse {
1273 keys_jwe: None,
1274 refresh_token: Some("merged_refresh".to_string()),
1275 session_token: None,
1276 expires_in: 3600,
1277 scope: "profile new_scope".to_string(),
1278 access_token: "access_token2".to_string(),
1279 })
1280 });
1281
1282 client
1284 .expect_destroy_refresh_token()
1285 .with(always(), eq("new_narrow_refresh"))
1286 .times(1)
1287 .returning(|_, _| Ok(()));
1288
1289 client
1291 .expect_destroy_refresh_token()
1292 .with(always(), eq("old_refresh"))
1293 .times(1)
1294 .returning(|_, _| Ok(()));
1295
1296 client
1298 .expect_update_device_record()
1299 .times(1)
1300 .returning(|_, _, _| Ok(make_mock_update_device_response()));
1301
1302 fxa.set_client(Arc::new(client));
1303
1304 fxa.complete_oauth_flow("mock_code", state.1.as_ref())
1305 .unwrap();
1306
1307 let scopes = &fxa.state.refresh_token().unwrap().scopes;
1308 assert!(
1309 scopes.contains("profile"),
1310 "expected profile scope, got {scopes:?}"
1311 );
1312 assert!(
1313 scopes.contains("new_scope"),
1314 "expected new_scope, got {scopes:?}"
1315 );
1316 assert_eq!(scopes.len(), 2);
1317 }
1318
1319 #[test]
1323 fn test_complete_oauth_flow_no_merge_when_scopes_match() {
1324 nss_as::ensure_initialized();
1325 let config = Config::new_with_mock_well_known_fxa_client_configuration(
1326 "mock-fxa.example.com",
1327 "12345678",
1328 "https://foo.bar",
1329 );
1330 let mut fxa = FirefoxAccount::with_config(config);
1331
1332 let url = fxa
1333 .begin_oauth_flow("", &["profile"], "test_entrypoint")
1334 .unwrap();
1335 let url = Url::parse(&url).unwrap();
1336 let state = url.query_pairs().find(|(name, _)| name == "state").unwrap();
1337
1338 fxa.state.force_refresh_token(RefreshToken {
1339 token: "old_refresh".to_string(),
1340 scopes: ["profile".to_string()].into(),
1341 });
1342 fxa.set_session_token("mock_session_token");
1343
1344 let mut client = MockFxAClient::new();
1345
1346 client
1348 .expect_create_refresh_token_using_authorization_code()
1349 .times(1)
1350 .returning(|_, _, _, _| {
1351 Ok(OAuthTokenResponse {
1352 keys_jwe: None,
1353 refresh_token: Some("new_refresh".to_string()),
1354 session_token: None,
1355 expires_in: 3600,
1356 scope: "profile".to_string(),
1357 access_token: "access_token".to_string(),
1358 })
1359 });
1360
1361 client
1363 .expect_destroy_access_token()
1364 .with(always(), always())
1365 .times(1)
1366 .returning(|_, _| Ok(()));
1367
1368 client
1370 .expect_get_devices()
1371 .with(always(), eq("old_refresh"))
1372 .times(1)
1373 .returning(|_, _| Ok(vec![make_mock_device("Test Device")]));
1374
1375 client
1380 .expect_destroy_refresh_token()
1381 .with(always(), eq("old_refresh"))
1382 .times(1)
1383 .returning(|_, _| Ok(()));
1384
1385 client
1387 .expect_update_device_record()
1388 .times(1)
1389 .returning(|_, _, _| Ok(make_mock_update_device_response()));
1390
1391 fxa.set_client(Arc::new(client));
1392
1393 fxa.complete_oauth_flow("mock_code", state.1.as_ref())
1394 .unwrap();
1395
1396 let scopes = &fxa.state.refresh_token().unwrap().scopes;
1397 assert_eq!(scopes, &["profile".to_string()].into());
1398 }
1399}