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