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