1pub mod attached_clients;
6use super::scopes;
7use super::{
8 http_client::{
9 AuthorizationRequestParameters, IntrospectResponse as IntrospectInfo, OAuthTokenResponse,
10 },
11 scoped_keys::ScopedKeysFlow,
12 util, FirefoxAccount,
13};
14use crate::auth::UserData;
15use crate::{warn, AuthorizationParameters, Error, FxaServer, Result, ScopedKey};
16use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
17use jwcrypto::{EncryptionAlgorithm, EncryptionParameters};
18use rate_limiter::RateLimiter;
19use rc_crypto::digest;
20use serde_derive::*;
21use std::{
22 collections::{HashMap, HashSet},
23 iter::FromIterator,
24 time::{SystemTime, UNIX_EPOCH},
25};
26use url::Url;
27const OAUTH_MIN_TIME_LEFT: u64 = 60;
30pub const OAUTH_WEBCHANNEL_REDIRECT: &str = "urn:ietf:wg:oauth:2.0:oob:oauth-redirect-webchannel";
33
34impl FirefoxAccount {
35 pub fn get_access_token(&mut self, scope: &str, use_cache: bool) -> Result<AccessTokenInfo> {
46 if scope.contains(' ') {
47 return Err(Error::MultipleScopesRequested);
48 }
49 if use_cache {
50 if let Some(oauth_info) = self.state.get_cached_access_token(scope) {
51 if oauth_info.expires_at > util::now_secs() + OAUTH_MIN_TIME_LEFT {
52 if oauth_info.check_missing_sync_scoped_key().is_ok() {
54 return Ok(oauth_info.clone());
55 }
56 }
57 }
58 }
59 let resp = match self.state.refresh_token() {
60 Some(refresh_token) => {
61 if refresh_token.scopes.contains(scope) {
62 self.client.create_access_token_using_refresh_token(
63 self.state.config(),
64 &refresh_token.token,
65 None,
66 &[scope],
67 )?
68 } else {
69 return Err(Error::NoCachedToken(scope.to_string()));
70 }
71 }
72 None => match self.state.session_token() {
73 Some(session_token) => self.client.create_access_token_using_session_token(
74 self.state.config(),
75 session_token,
76 &[scope],
77 )?,
78 None => return Err(Error::NoCachedToken(scope.to_string())),
79 },
80 };
81 let since_epoch = SystemTime::now()
82 .duration_since(UNIX_EPOCH)
83 .map_err(|_| Error::IllegalState("Current date before Unix Epoch."))?;
84 let expires_at = since_epoch.as_secs() + resp.expires_in;
85 let token_info = AccessTokenInfo {
86 scope: resp.scope,
87 token: resp.access_token,
88 key: self.state.get_scoped_key(scope).cloned(),
89 expires_at,
90 };
91 self.state
92 .add_cached_access_token(scope, token_info.clone());
93 token_info.check_missing_sync_scoped_key()?;
94 Ok(token_info)
95 }
96
97 pub fn set_user_data(&mut self, user_data: UserData) {
99 self.state.set_session_token(user_data.session_token)
103 }
104
105 pub fn get_session_token(&self) -> Result<String> {
107 match self.state.session_token() {
108 Some(session_token) => Ok(session_token.to_string()),
109 None => Err(Error::NoSessionToken),
110 }
111 }
112
113 pub fn check_authorization_status(&mut self) -> Result<IntrospectInfo> {
115 let resp = match self.state.refresh_token() {
116 Some(refresh_token) => {
117 self.auth_circuit_breaker.check()?;
118 self.client
119 .check_refresh_token_status(self.state.config(), &refresh_token.token)?
120 }
121 None => return Err(Error::NoRefreshToken),
122 };
123 Ok(IntrospectInfo {
124 active: resp.active,
125 })
126 }
127
128 pub fn begin_pairing_flow(
136 &mut self,
137 pairing_url: &str,
138 scopes: &[&str],
139 entrypoint: &str,
140 ) -> Result<String> {
141 let mut url = self.state.config().pair_supp_url()?;
142 url.query_pairs_mut().append_pair("entrypoint", entrypoint);
143 let pairing_url = util::parse_url(pairing_url, "begin_pairing_flow")?;
144 if url.host_str() != pairing_url.host_str() {
145 let fxa_server = FxaServer::from(&url);
146 let pairing_fxa_server = FxaServer::from(&pairing_url);
147 return Err(Error::OriginMismatch(format!(
148 "fxa-server: {fxa_server}, pairing-url-fxa-server: {pairing_fxa_server}"
149 )));
150 }
151 url.set_fragment(pairing_url.fragment());
152 self.oauth_flow(url, scopes)
153 }
154
155 pub fn begin_oauth_flow(&mut self, scopes: &[&str], entrypoint: &str) -> Result<String> {
161 self.state.on_begin_oauth();
162 let mut url = if self.state.last_seen_profile().is_some() {
163 self.state.config().oauth_force_auth_url()?
164 } else {
165 self.state.config().authorization_endpoint()?
166 };
167
168 url.query_pairs_mut()
169 .append_pair("action", "email")
170 .append_pair("response_type", "code")
171 .append_pair("entrypoint", entrypoint);
172
173 if let Some(cached_profile) = self.state.last_seen_profile() {
174 url.query_pairs_mut()
175 .append_pair("email", &cached_profile.response.email);
176 }
177
178 let scopes: Vec<String> = match self.state.refresh_token() {
179 Some(refresh_token) => {
180 let mut all_scopes: Vec<String> = vec![];
182 all_scopes.extend(scopes.iter().map(ToString::to_string));
183 let existing_scopes = refresh_token.scopes.clone();
184 all_scopes.extend(existing_scopes);
185 HashSet::<String>::from_iter(all_scopes)
186 .into_iter()
187 .collect()
188 }
189 None => scopes.iter().map(ToString::to_string).collect(),
190 };
191 let scopes: Vec<&str> = scopes.iter().map(<_>::as_ref).collect();
192 self.oauth_flow(url, &scopes)
193 }
194
195 pub fn authorize_code_using_session_token(
205 &self,
206 auth_params: AuthorizationParameters,
207 ) -> Result<String> {
208 let session_token = self.get_session_token()?;
209
210 let allowed_scopes = self.client.get_scoped_key_data(
213 self.state.config(),
214 &session_token,
215 &auth_params.client_id,
216 &auth_params.scope.join(" "),
217 )?;
218
219 if let Some(not_allowed_scope) = auth_params
220 .scope
221 .iter()
222 .find(|scope| !allowed_scopes.contains_key(*scope))
223 {
224 return Err(Error::ScopeNotAllowed(
225 auth_params.client_id.clone(),
226 not_allowed_scope.clone(),
227 ));
228 }
229
230 let keys_jwe = if let Some(keys_jwk) = auth_params.keys_jwk {
231 let mut scoped_keys = HashMap::new();
232 allowed_scopes
233 .iter()
234 .try_for_each(|(scope, _)| -> Result<()> {
235 scoped_keys.insert(
236 scope,
237 self.state
238 .get_scoped_key(scope)
239 .ok_or_else(|| Error::NoScopedKey(scope.clone()))?,
240 );
241 Ok(())
242 })?;
243 let scoped_keys = serde_json::to_string(&scoped_keys)?;
244 let keys_jwk = URL_SAFE_NO_PAD.decode(keys_jwk)?;
245 let jwk = serde_json::from_slice(&keys_jwk)?;
246 Some(jwcrypto::encrypt_to_jwe(
247 scoped_keys.as_bytes(),
248 EncryptionParameters::ECDH_ES {
249 enc: EncryptionAlgorithm::A256GCM,
250 peer_jwk: &jwk,
251 },
252 )?)
253 } else {
254 None
255 };
256 let auth_request_params = AuthorizationRequestParameters {
257 client_id: auth_params.client_id,
258 scope: auth_params.scope.join(" "),
259 state: auth_params.state,
260 access_type: auth_params.access_type,
261 code_challenge: auth_params.code_challenge,
262 code_challenge_method: auth_params.code_challenge_method,
263 keys_jwe,
264 };
265
266 let resp = self.client.create_authorization_code_using_session_token(
267 self.state.config(),
268 &session_token,
269 auth_request_params,
270 )?;
271
272 Ok(resp.code)
273 }
274
275 fn oauth_flow(&mut self, mut url: Url, scopes: &[&str]) -> Result<String> {
276 self.clear_access_token_cache();
277 let state = util::random_base64_url_string(16)?;
278 let code_verifier = util::random_base64_url_string(43)?;
279 let code_challenge = digest::digest(&digest::SHA256, code_verifier.as_bytes())?;
280 let code_challenge = URL_SAFE_NO_PAD.encode(code_challenge);
281 let scoped_keys_flow = ScopedKeysFlow::with_random_key()?;
282 let jwk = scoped_keys_flow.get_public_key_jwk()?;
283 let jwk_json = serde_json::to_string(&jwk)?;
284 let keys_jwk = URL_SAFE_NO_PAD.encode(jwk_json);
285 url.query_pairs_mut()
286 .append_pair("client_id", &self.state.config().client_id)
287 .append_pair("scope", &scopes.join(" "))
288 .append_pair("state", &state)
289 .append_pair("code_challenge_method", "S256")
290 .append_pair("code_challenge", &code_challenge)
291 .append_pair("access_type", "offline")
292 .append_pair("keys_jwk", &keys_jwk);
293
294 if self.state.config().redirect_uri == OAUTH_WEBCHANNEL_REDIRECT {
295 url.query_pairs_mut()
296 .append_pair("context", "oauth_webchannel_v1");
297 } else {
298 url.query_pairs_mut()
299 .append_pair("redirect_uri", &self.state.config().redirect_uri);
300 }
301
302 self.state.begin_oauth_flow(
303 state,
304 OAuthFlow {
305 scoped_keys_flow: Some(scoped_keys_flow),
306 code_verifier,
307 },
308 );
309 Ok(url.to_string())
310 }
311
312 pub fn complete_oauth_flow(&mut self, code: &str, state: &str) -> Result<()> {
318 self.clear_access_token_cache();
319 let oauth_flow = match self.state.pop_oauth_flow(state) {
320 Some(oauth_flow) => oauth_flow,
321 None => return Err(Error::UnknownOAuthState),
322 };
323 let resp = self.client.create_refresh_token_using_authorization_code(
324 self.state.config(),
325 self.state.session_token(),
326 code,
327 &oauth_flow.code_verifier,
328 )?;
329 self.handle_oauth_response(resp, oauth_flow.scoped_keys_flow)
330 }
331
332 pub fn cancel_existing_oauth_flows(&mut self) {
334 self.state.clear_oauth_flows();
335 }
336
337 pub(crate) fn handle_oauth_response(
338 &mut self,
339 resp: OAuthTokenResponse,
340 scoped_keys_flow: Option<ScopedKeysFlow>,
341 ) -> Result<()> {
342 let sync_scope_granted = resp.scope.split(' ').any(|s| s == scopes::OLD_SYNC);
343 let scoped_keys = match resp.keys_jwe {
344 Some(ref jwe) => {
345 let scoped_keys_flow = scoped_keys_flow.ok_or(Error::ApiClientError(
346 "Got a JWE but have no JWK to decrypt it.",
347 ))?;
348 let decrypted_keys = scoped_keys_flow.decrypt_keys_jwe(jwe)?;
349 let scoped_keys: serde_json::Map<String, serde_json::Value> =
350 serde_json::from_str(&decrypted_keys)?;
351 if sync_scope_granted && !scoped_keys.contains_key(scopes::OLD_SYNC) {
352 error_support::report_error!(
353 "fxaclient-scoped-key",
354 "Sync scope granted, but no sync scoped key (scope granted: {}, key scopes: {})",
355 resp.scope,
356 scoped_keys.keys().map(|s| s.as_ref()).collect::<Vec<&str>>().join(", ")
357 );
358 }
359 scoped_keys
360 .into_iter()
361 .map(|(scope, key)| Ok((scope, serde_json::from_value(key)?)))
362 .collect::<Result<Vec<_>>>()?
363 }
364 None => {
365 if sync_scope_granted {
366 error_support::report_error!(
367 "fxaclient-scoped-key",
368 "Sync scope granted, but keys_jwe is None"
369 );
370 }
371 vec![]
372 }
373 };
374
375 if let Err(err) = self
379 .client
380 .destroy_access_token(self.state.config(), &resp.access_token)
381 {
382 warn!("Access token destruction failure: {:?}", err);
383 }
384 let old_refresh_token = self.state.refresh_token().cloned();
385 let new_refresh_token = resp
386 .refresh_token
387 .ok_or(Error::ApiClientError("No refresh token in response"))?;
388 let old_device_info = match old_refresh_token {
391 Some(_) => match self.get_current_device() {
392 Ok(maybe_device) => maybe_device,
393 Err(err) => {
394 warn!("Error while getting previous device information: {:?}", err);
395 None
396 }
397 },
398 None => None,
399 };
400 if let Some(ref refresh_token) = old_refresh_token {
403 if let Err(err) = self
404 .client
405 .destroy_refresh_token(self.state.config(), &refresh_token.token)
406 {
407 warn!("Refresh token destruction failure: {:?}", err);
408 }
409 }
410 if let Some(ref device_info) = old_device_info {
411 if let Err(err) = self.replace_device(
412 &device_info.display_name,
413 &device_info.device_type,
414 &device_info.push_subscription,
415 &device_info.available_commands,
416 ) {
417 warn!("Device information restoration failed: {:?}", err);
418 }
419 }
420 self.state.complete_oauth_flow(
421 scoped_keys,
422 RefreshToken {
423 token: new_refresh_token,
424 scopes: resp.scope.split(' ').map(ToString::to_string).collect(),
425 },
426 resp.session_token,
427 );
428 Ok(())
429 }
430
431 pub fn handle_session_token_change(&mut self, session_token: &str) -> Result<()> {
439 let old_refresh_token = self.state.refresh_token().ok_or(Error::NoRefreshToken)?;
440 let scopes: Vec<&str> = old_refresh_token.scopes.iter().map(AsRef::as_ref).collect();
441 let resp = self.client.create_refresh_token_using_session_token(
442 self.state.config(),
443 session_token,
444 &scopes,
445 )?;
446 let new_refresh_token = resp
447 .refresh_token
448 .ok_or(Error::ApiClientError("No refresh token in response"))?;
449 self.state.update_tokens(
450 session_token.to_owned(),
451 RefreshToken {
452 token: new_refresh_token,
453 scopes: resp.scope.split(' ').map(ToString::to_string).collect(),
454 },
455 );
456 self.clear_devices_and_attached_clients_cache();
457 Ok(())
458 }
459
460 pub fn clear_access_token_cache(&mut self) {
462 self.state.clear_access_token_cache();
463 }
464}
465
466const AUTH_CIRCUIT_BREAKER_CAPACITY: u8 = 5;
467const AUTH_CIRCUIT_BREAKER_RENEWAL_RATE: f32 = 3.0 / 60.0 / 1000.0; #[derive(Clone, Copy)]
470pub(crate) struct AuthCircuitBreaker {
471 rate_limiter: RateLimiter,
472}
473
474impl Default for AuthCircuitBreaker {
475 fn default() -> Self {
476 AuthCircuitBreaker {
477 rate_limiter: RateLimiter::new(
478 AUTH_CIRCUIT_BREAKER_CAPACITY,
479 AUTH_CIRCUIT_BREAKER_RENEWAL_RATE,
480 ),
481 }
482 }
483}
484
485impl AuthCircuitBreaker {
486 pub(crate) fn check(&mut self) -> Result<()> {
487 if !self.rate_limiter.check() {
488 return Err(Error::AuthCircuitBreakerError);
489 }
490 Ok(())
491 }
492}
493
494impl TryFrom<Url> for AuthorizationParameters {
495 type Error = Error;
496
497 fn try_from(url: Url) -> Result<Self> {
498 let query_map: HashMap<String, String> = url.query_pairs().into_owned().collect();
499 let scope = query_map
500 .get("scope")
501 .cloned()
502 .ok_or(Error::MissingUrlParameter("scope"))?;
503 let client_id = query_map
504 .get("client_id")
505 .cloned()
506 .ok_or(Error::MissingUrlParameter("client_id"))?;
507 let state = query_map
508 .get("state")
509 .cloned()
510 .ok_or(Error::MissingUrlParameter("state"))?;
511 let access_type = query_map
512 .get("access_type")
513 .cloned()
514 .ok_or(Error::MissingUrlParameter("access_type"))?;
515 let code_challenge = query_map.get("code_challenge").cloned();
516 let code_challenge_method = query_map.get("code_challenge_method").cloned();
517 let keys_jwk = query_map.get("keys_jwk").cloned();
518 Ok(Self {
519 client_id,
520 scope: scope.split_whitespace().map(|s| s.to_string()).collect(),
521 state,
522 access_type,
523 code_challenge,
524 code_challenge_method,
525 keys_jwk,
526 })
527 }
528}
529
530#[derive(Clone, Serialize, Deserialize)]
531pub struct RefreshToken {
532 pub token: String,
533 pub scopes: HashSet<String>,
534}
535
536impl std::fmt::Debug for RefreshToken {
537 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
538 f.debug_struct("RefreshToken")
539 .field("scopes", &self.scopes)
540 .finish()
541 }
542}
543
544pub struct OAuthFlow {
545 pub scoped_keys_flow: Option<ScopedKeysFlow>,
546 pub code_verifier: String,
547}
548
549#[derive(Clone, Serialize, Deserialize)]
550pub struct AccessTokenInfo {
551 pub scope: String,
552 pub token: String,
553 pub key: Option<ScopedKey>,
554 pub expires_at: u64, }
556
557impl AccessTokenInfo {
558 pub fn check_missing_sync_scoped_key(&self) -> Result<()> {
559 if self.scope == scopes::OLD_SYNC && self.key.is_none() {
560 Err(Error::SyncScopedKeyMissingInServerResponse)
561 } else {
562 Ok(())
563 }
564 }
565}
566
567impl TryFrom<AccessTokenInfo> for crate::AccessTokenInfo {
568 type Error = Error;
569 fn try_from(info: AccessTokenInfo) -> Result<Self> {
570 Ok(crate::AccessTokenInfo {
571 scope: info.scope,
572 token: info.token,
573 key: info.key,
574 expires_at: info.expires_at.try_into()?,
575 })
576 }
577}
578
579impl std::fmt::Debug for AccessTokenInfo {
580 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
581 f.debug_struct("AccessTokenInfo")
582 .field("scope", &self.scope)
583 .field("key", &self.key)
584 .field("expires_at", &self.expires_at)
585 .finish()
586 }
587}
588
589impl From<IntrospectInfo> for crate::AuthorizationInfo {
590 fn from(r: IntrospectInfo) -> Self {
591 crate::AuthorizationInfo { active: r.active }
592 }
593}
594
595#[cfg(test)]
596mod tests {
597 use super::super::{http_client::*, Config};
598 use super::*;
599 use mockall::predicate::always;
600 use mockall::predicate::eq;
601 use std::borrow::Cow;
602 use std::collections::HashMap;
603 use std::sync::Arc;
604
605 impl FirefoxAccount {
606 pub fn add_cached_token(&mut self, scope: &str, token_info: AccessTokenInfo) {
607 self.state.add_cached_access_token(scope, token_info);
608 }
609
610 pub fn set_session_token(&mut self, session_token: &str) {
611 self.state.set_session_token(session_token.to_owned());
612 }
613 }
614
615 #[test]
616 fn test_oauth_flow_url() {
617 nss::ensure_initialized();
618 let config = Config::new_with_mock_well_known_fxa_client_configuration(
619 "https://mock-fxa.example.com",
620 "12345678",
621 "https://foo.bar",
622 );
623 let mut fxa = FirefoxAccount::with_config(config);
624 let url = fxa
625 .begin_oauth_flow(&["profile"], "test_oauth_flow_url")
626 .unwrap();
627 let flow_url = Url::parse(&url).unwrap();
628
629 assert_eq!(flow_url.path(), "/authorization");
630
631 let mut pairs = flow_url.query_pairs();
632 assert_eq!(pairs.count(), 11);
633 assert_eq!(
634 pairs.next(),
635 Some((Cow::Borrowed("action"), Cow::Borrowed("email")))
636 );
637 assert_eq!(
638 pairs.next(),
639 Some((Cow::Borrowed("response_type"), Cow::Borrowed("code")))
640 );
641 assert_eq!(
642 pairs.next(),
643 Some((
644 Cow::Borrowed("entrypoint"),
645 Cow::Borrowed("test_oauth_flow_url")
646 ))
647 );
648 assert_eq!(
649 pairs.next(),
650 Some((Cow::Borrowed("client_id"), Cow::Borrowed("12345678")))
651 );
652
653 assert_eq!(
654 pairs.next(),
655 Some((Cow::Borrowed("scope"), Cow::Borrowed("profile")))
656 );
657 let state_param = pairs.next().unwrap();
658 assert_eq!(state_param.0, Cow::Borrowed("state"));
659 assert_eq!(state_param.1.len(), 22);
660 assert_eq!(
661 pairs.next(),
662 Some((
663 Cow::Borrowed("code_challenge_method"),
664 Cow::Borrowed("S256")
665 ))
666 );
667 let code_challenge_param = pairs.next().unwrap();
668 assert_eq!(code_challenge_param.0, Cow::Borrowed("code_challenge"));
669 assert_eq!(code_challenge_param.1.len(), 43);
670 assert_eq!(
671 pairs.next(),
672 Some((Cow::Borrowed("access_type"), Cow::Borrowed("offline")))
673 );
674 let keys_jwk = pairs.next().unwrap();
675 assert_eq!(keys_jwk.0, Cow::Borrowed("keys_jwk"));
676 assert_eq!(keys_jwk.1.len(), 168);
677
678 assert_eq!(
679 pairs.next(),
680 Some((
681 Cow::Borrowed("redirect_uri"),
682 Cow::Borrowed("https://foo.bar")
683 ))
684 );
685 }
686
687 #[test]
688 fn test_force_auth_url() {
689 nss::ensure_initialized();
690 let config = Config::stable_dev("12345678", "https://foo.bar");
691 let mut fxa = FirefoxAccount::with_config(config);
692 let email = "test@example.com";
693 fxa.add_cached_profile("123", email);
694 let url = fxa
695 .begin_oauth_flow(&["profile"], "test_force_auth_url")
696 .unwrap();
697 let url = Url::parse(&url).unwrap();
698 assert_eq!(url.path(), "/oauth/force_auth");
699 let mut pairs = url.query_pairs();
700 assert_eq!(
701 pairs.find(|e| e.0 == "email"),
702 Some((Cow::Borrowed("email"), Cow::Borrowed(email),))
703 );
704 }
705
706 #[test]
707 fn test_webchannel_context_url() {
708 nss::ensure_initialized();
709 const SCOPES: &[&str] = &["https://identity.mozilla.com/apps/oldsync"];
710 let config = Config::new_with_mock_well_known_fxa_client_configuration(
711 "https://mock-fxa.example.com",
712 "12345678",
713 "urn:ietf:wg:oauth:2.0:oob:oauth-redirect-webchannel",
714 );
715 let mut fxa = FirefoxAccount::with_config(config);
716 let url = fxa
717 .begin_oauth_flow(SCOPES, "test_webchannel_context_url")
718 .unwrap();
719 let url = Url::parse(&url).unwrap();
720 let query_params: HashMap<_, _> = url.query_pairs().into_owned().collect();
721 let context = &query_params["context"];
722 assert_eq!(context, "oauth_webchannel_v1");
723 assert_eq!(query_params.get("redirect_uri"), None);
724 }
725
726 #[test]
727 fn test_webchannel_pairing_context_url() {
728 nss::ensure_initialized();
729 const SCOPES: &[&str] = &["https://identity.mozilla.com/apps/oldsync"];
730 const PAIRING_URL: &str = "https://accounts.firefox.com/pair#channel_id=658db7fe98b249a5897b884f98fb31b7&channel_key=1hIDzTj5oY2HDeSg_jA2DhcOcAn5Uqq0cAYlZRNUIo4";
731
732 let config = Config::new(
733 "https://accounts.firefox.com",
734 "12345678",
735 "urn:ietf:wg:oauth:2.0:oob:oauth-redirect-webchannel",
736 );
737 let mut fxa = FirefoxAccount::with_config(config);
738 let url = fxa
739 .begin_pairing_flow(PAIRING_URL, SCOPES, "test_webchannel_pairing_context_url")
740 .unwrap();
741 let url = Url::parse(&url).unwrap();
742 let query_params: HashMap<_, _> = url.query_pairs().into_owned().collect();
743 let context = &query_params["context"];
744 assert_eq!(context, "oauth_webchannel_v1");
745 assert_eq!(query_params.get("redirect_uri"), None);
746 }
747
748 #[test]
749 fn test_pairing_flow_url() {
750 nss::ensure_initialized();
751 const SCOPES: &[&str] = &["https://identity.mozilla.com/apps/oldsync"];
752 const PAIRING_URL: &str = "https://accounts.firefox.com/pair#channel_id=658db7fe98b249a5897b884f98fb31b7&channel_key=1hIDzTj5oY2HDeSg_jA2DhcOcAn5Uqq0cAYlZRNUIo4";
753 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";
754
755 let config = Config::new(
756 "https://accounts.firefox.com",
757 "12345678",
758 "https://foo.bar",
759 );
760
761 let mut fxa = FirefoxAccount::with_config(config);
762 let url = fxa
763 .begin_pairing_flow(PAIRING_URL, SCOPES, "test_pairing_flow_url")
764 .unwrap();
765 let flow_url = Url::parse(&url).unwrap();
766 let expected_parsed_url = Url::parse(EXPECTED_URL).unwrap();
767
768 assert_eq!(flow_url.host_str(), Some("accounts.firefox.com"));
769 assert_eq!(flow_url.path(), "/pair/supp");
770 assert_eq!(flow_url.fragment(), expected_parsed_url.fragment());
771
772 let mut pairs = flow_url.query_pairs();
773 assert_eq!(pairs.count(), 9);
774 assert_eq!(
775 pairs.next(),
776 Some((
777 Cow::Borrowed("entrypoint"),
778 Cow::Borrowed("test_pairing_flow_url")
779 ))
780 );
781 assert_eq!(
782 pairs.next(),
783 Some((Cow::Borrowed("client_id"), Cow::Borrowed("12345678")))
784 );
785 assert_eq!(
786 pairs.next(),
787 Some((
788 Cow::Borrowed("scope"),
789 Cow::Borrowed("https://identity.mozilla.com/apps/oldsync")
790 ))
791 );
792
793 let state_param = pairs.next().unwrap();
794 assert_eq!(state_param.0, Cow::Borrowed("state"));
795 assert_eq!(state_param.1.len(), 22);
796 assert_eq!(
797 pairs.next(),
798 Some((
799 Cow::Borrowed("code_challenge_method"),
800 Cow::Borrowed("S256")
801 ))
802 );
803 let code_challenge_param = pairs.next().unwrap();
804 assert_eq!(code_challenge_param.0, Cow::Borrowed("code_challenge"));
805 assert_eq!(code_challenge_param.1.len(), 43);
806 assert_eq!(
807 pairs.next(),
808 Some((Cow::Borrowed("access_type"), Cow::Borrowed("offline")))
809 );
810 let keys_jwk = pairs.next().unwrap();
811 assert_eq!(keys_jwk.0, Cow::Borrowed("keys_jwk"));
812 assert_eq!(keys_jwk.1.len(), 168);
813
814 assert_eq!(
815 pairs.next(),
816 Some((
817 Cow::Borrowed("redirect_uri"),
818 Cow::Borrowed("https://foo.bar")
819 ))
820 );
821 }
822
823 #[test]
824 fn test_pairing_flow_origin_mismatch() {
825 nss::ensure_initialized();
826 static PAIRING_URL: &str = "https://bad.origin.com/pair#channel_id=foo&channel_key=bar";
827 let config = Config::stable_dev("12345678", "https://foo.bar");
828 let mut fxa = FirefoxAccount::with_config(config);
829 let url = fxa.begin_pairing_flow(
830 PAIRING_URL,
831 &["https://identity.mozilla.com/apps/oldsync"],
832 "test_pairiong_flow_origin_mismatch",
833 );
834
835 assert!(url.is_err());
836
837 match url {
838 Ok(_) => {
839 panic!("should have error");
840 }
841 Err(err) => match err {
842 Error::OriginMismatch { .. } => {}
843 _ => panic!("error not OriginMismatch"),
844 },
845 }
846 }
847
848 #[test]
849 fn test_check_authorization_status() {
850 nss::ensure_initialized();
851 let config = Config::stable_dev("12345678", "https://foo.bar");
852 let mut fxa = FirefoxAccount::with_config(config);
853
854 let refresh_token_scopes = std::collections::HashSet::new();
855 fxa.state.force_refresh_token(RefreshToken {
856 token: "refresh_token".to_owned(),
857 scopes: refresh_token_scopes,
858 });
859
860 let mut client = MockFxAClient::new();
861 client
862 .expect_check_refresh_token_status()
863 .with(always(), eq("refresh_token"))
864 .times(1)
865 .returning(|_, _| Ok(IntrospectResponse { active: true }));
866 fxa.set_client(Arc::new(client));
867
868 let auth_status = fxa.check_authorization_status().unwrap();
869 assert!(auth_status.active);
870 }
871
872 #[test]
873 fn test_check_authorization_status_circuit_breaker() {
874 nss::ensure_initialized();
875 let config = Config::stable_dev("12345678", "https://foo.bar");
876 let mut fxa = FirefoxAccount::with_config(config);
877
878 let refresh_token_scopes = std::collections::HashSet::new();
879 fxa.state.force_refresh_token(RefreshToken {
880 token: "refresh_token".to_owned(),
881 scopes: refresh_token_scopes,
882 });
883
884 let mut client = MockFxAClient::new();
885 client
888 .expect_check_refresh_token_status()
889 .with(always(), eq("refresh_token"))
890 .returning(|_, _| Ok(IntrospectResponse { active: true }));
891 client
892 .expect_check_refresh_token_status()
893 .with(always(), eq("refresh_token"))
894 .returning(|_, _| Ok(IntrospectResponse { active: true }));
895 client
896 .expect_check_refresh_token_status()
897 .with(always(), eq("refresh_token"))
898 .returning(|_, _| Ok(IntrospectResponse { active: true }));
899 client
900 .expect_check_refresh_token_status()
901 .with(always(), eq("refresh_token"))
902 .returning(|_, _| Ok(IntrospectResponse { active: true }));
903 client
904 .expect_check_refresh_token_status()
905 .with(always(), eq("refresh_token"))
906 .returning(|_, _| Ok(IntrospectResponse { active: true }));
907 fxa.set_client(Arc::new(client));
909
910 for _ in 0..5 {
911 assert!(fxa.check_authorization_status().is_ok());
912 }
913 match fxa.check_authorization_status() {
914 Ok(_) => unreachable!("should not happen"),
915 Err(err) => assert!(matches!(err, Error::AuthCircuitBreakerError)),
916 }
917 }
918
919 use crate::internal::scopes::{self, OLD_SYNC};
920
921 #[test]
922 fn test_auth_code_pair_valid_not_allowed_scope() {
923 nss::ensure_initialized();
924 let config = Config::stable_dev("12345678", "https://foo.bar");
925 let mut fxa = FirefoxAccount::with_config(config);
926 fxa.set_session_token("session");
927 let mut client = MockFxAClient::new();
928 let not_allowed_scope = "https://identity.mozilla.com/apps/lockbox";
929 let expected_scopes = scopes::OLD_SYNC
930 .chars()
931 .chain(std::iter::once(' '))
932 .chain(not_allowed_scope.chars())
933 .collect::<String>();
934 client
935 .expect_get_scoped_key_data()
936 .with(always(), eq("session"), eq("12345678"), eq(expected_scopes))
937 .times(1)
938 .returning(|_, _, _, _| {
939 Err(Error::RemoteError {
940 code: 400,
941 errno: 163,
942 error: "Invalid Scopes".to_string(),
943 message: "Not allowed to request scopes".to_string(),
944 info: "fyi, there was a server error".to_string(),
945 })
946 });
947 fxa.set_client(Arc::new(client));
948 let auth_params = AuthorizationParameters {
949 client_id: "12345678".to_string(),
950 scope: vec![scopes::OLD_SYNC.to_string(), not_allowed_scope.to_string()],
951 state: "somestate".to_string(),
952 access_type: "offline".to_string(),
953 code_challenge: None,
954 code_challenge_method: None,
955 keys_jwk: None,
956 };
957 let res = fxa.authorize_code_using_session_token(auth_params);
958 assert!(res.is_err());
959 let err = res.unwrap_err();
960 if let Error::RemoteError {
961 code,
962 errno,
963 error: _,
964 message: _,
965 info: _,
966 } = err
967 {
968 assert_eq!(code, 400);
969 assert_eq!(errno, 163); } else {
971 panic!("Should return an error from the server specifying that the requested scopes are not allowed");
972 }
973 }
974
975 #[test]
976 fn test_auth_code_pair_invalid_scope_not_allowed() {
977 nss::ensure_initialized();
978 let config = Config::stable_dev("12345678", "https://foo.bar");
979 let mut fxa = FirefoxAccount::with_config(config);
980 fxa.set_session_token("session");
981 let mut client = MockFxAClient::new();
982 let invalid_scope = "IamAnInvalidScope";
983 let expected_scopes = scopes::OLD_SYNC
984 .chars()
985 .chain(std::iter::once(' '))
986 .chain(invalid_scope.chars())
987 .collect::<String>();
988 client
989 .expect_get_scoped_key_data()
990 .with(always(), eq("session"), eq("12345678"), eq(expected_scopes))
991 .times(1)
992 .returning(|_, _, _, _| {
993 let mut server_ret = HashMap::new();
994 server_ret.insert(
995 scopes::OLD_SYNC.to_string(),
996 ScopedKeyDataResponse {
997 key_rotation_secret: "IamASecret".to_string(),
998 key_rotation_timestamp: 100,
999 identifier: "".to_string(),
1000 },
1001 );
1002 Ok(server_ret)
1003 });
1004 fxa.set_client(Arc::new(client));
1005
1006 let auth_params = AuthorizationParameters {
1007 client_id: "12345678".to_string(),
1008 scope: vec![scopes::OLD_SYNC.to_string(), invalid_scope.to_string()],
1009 state: "somestate".to_string(),
1010 access_type: "offline".to_string(),
1011 code_challenge: None,
1012 code_challenge_method: None,
1013 keys_jwk: None,
1014 };
1015 let res = fxa.authorize_code_using_session_token(auth_params);
1016 assert!(res.is_err());
1017 let err = res.unwrap_err();
1018 if let Error::ScopeNotAllowed(client_id, scope) = err {
1019 assert_eq!(client_id, "12345678");
1020 assert_eq!(scope, "IamAnInvalidScope");
1021 } else {
1022 panic!("Should return an error that specifies the scope that is not allowed");
1023 }
1024 }
1025
1026 #[test]
1027 fn test_auth_code_pair_scope_not_in_state() {
1028 nss::ensure_initialized();
1029 let config = Config::stable_dev("12345678", "https://foo.bar");
1030 let mut fxa = FirefoxAccount::with_config(config);
1031 fxa.set_session_token("session");
1032 let mut client = MockFxAClient::new();
1033 client
1034 .expect_get_scoped_key_data()
1035 .with(
1036 always(),
1037 eq("session"),
1038 eq("12345678"),
1039 eq(scopes::OLD_SYNC),
1040 )
1041 .times(1)
1042 .returning(|_, _, _, _| {
1043 let mut server_ret = HashMap::new();
1044 server_ret.insert(
1045 scopes::OLD_SYNC.to_string(),
1046 ScopedKeyDataResponse {
1047 key_rotation_secret: "IamASecret".to_string(),
1048 key_rotation_timestamp: 100,
1049 identifier: "".to_string(),
1050 },
1051 );
1052 Ok(server_ret)
1053 });
1054 fxa.set_client(Arc::new(client));
1055 let auth_params = AuthorizationParameters {
1056 client_id: "12345678".to_string(),
1057 scope: vec![scopes::OLD_SYNC.to_string()],
1058 state: "somestate".to_string(),
1059 access_type: "offline".to_string(),
1060 code_challenge: None,
1061 code_challenge_method: None,
1062 keys_jwk: Some("IAmAVerySecretKeysJWkInBase64".to_string()),
1063 };
1064 let res = fxa.authorize_code_using_session_token(auth_params);
1065 assert!(res.is_err());
1066 let err = res.unwrap_err();
1067 if let Error::NoScopedKey(scope) = err {
1068 assert_eq!(scope, scopes::OLD_SYNC.to_string());
1069 } else {
1070 panic!("Should return an error that specifies the scope that is not in the state");
1071 }
1072 }
1073
1074 #[test]
1075 fn test_set_user_data_sets_session_token() {
1076 nss::ensure_initialized();
1077 let config = Config::stable_dev("12345678", "https://foo.bar");
1078 let mut fxa = FirefoxAccount::with_config(config);
1079 let user_data = UserData {
1080 session_token: String::from("mock_session_token"),
1081 uid: String::from("mock_uid_unused"),
1082 email: String::from("mock_email_usued"),
1083 verified: true,
1084 };
1085 fxa.set_user_data(user_data);
1086 assert_eq!(fxa.get_session_token().unwrap(), "mock_session_token");
1087 }
1088
1089 #[test]
1090 fn test_oauth_request_sent_with_session_when_available() {
1091 nss::ensure_initialized();
1092 let config = Config::new_with_mock_well_known_fxa_client_configuration(
1093 "mock-fxa.example.com",
1094 "12345678",
1095 "https://foo.bar",
1096 );
1097 let mut fxa = FirefoxAccount::with_config(config);
1098 let url = fxa
1099 .begin_oauth_flow(&[OLD_SYNC, "profile"], "test_entrypoint")
1100 .unwrap();
1101 let url = Url::parse(&url).unwrap();
1102 let state = url.query_pairs().find(|(name, _)| name == "state").unwrap();
1103 let user_data = UserData {
1104 session_token: String::from("mock_session_token"),
1105 uid: String::from("mock_uid_unused"),
1106 email: String::from("mock_email_usued"),
1107 verified: true,
1108 };
1109 let mut client = MockFxAClient::new();
1110
1111 client
1112 .expect_create_refresh_token_using_authorization_code()
1113 .withf(|_, session_token, code, _| {
1114 matches!(session_token, Some("mock_session_token")) && code == "mock_code"
1115 })
1116 .times(1)
1117 .returning(|_, _, _, _| {
1118 Ok(OAuthTokenResponse {
1119 keys_jwe: None,
1120 refresh_token: Some("refresh_token".to_string()),
1121 session_token: None,
1122 expires_in: 1,
1123 scope: "profile".to_string(),
1124 access_token: "access_token".to_string(),
1125 })
1126 });
1127 client
1128 .expect_destroy_access_token()
1129 .with(always(), always())
1130 .times(1)
1131 .returning(|_, _| Ok(()));
1132 fxa.set_client(Arc::new(client));
1133
1134 fxa.set_user_data(user_data);
1135
1136 fxa.complete_oauth_flow("mock_code", state.1.as_ref())
1137 .unwrap();
1138 }
1139}