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 = util::parse_url(pairing_url, "begin_pairing_flow")?;
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 fn cancel_existing_oauth_flows(&mut self) {
331 self.state.clear_oauth_flows();
332 }
333
334 pub(crate) fn handle_oauth_response(
335 &mut self,
336 resp: OAuthTokenResponse,
337 scoped_keys_flow: Option<ScopedKeysFlow>,
338 ) -> Result<()> {
339 let sync_scope_granted = resp.scope.split(' ').any(|s| s == scopes::OLD_SYNC);
340 let scoped_keys = match resp.keys_jwe {
341 Some(ref jwe) => {
342 let scoped_keys_flow = scoped_keys_flow.ok_or(Error::ApiClientError(
343 "Got a JWE but have no JWK to decrypt it.",
344 ))?;
345 let decrypted_keys = scoped_keys_flow.decrypt_keys_jwe(jwe)?;
346 let scoped_keys: serde_json::Map<String, serde_json::Value> =
347 serde_json::from_str(&decrypted_keys)?;
348 if sync_scope_granted && !scoped_keys.contains_key(scopes::OLD_SYNC) {
349 error_support::report_error!(
350 "fxaclient-scoped-key",
351 "Sync scope granted, but no sync scoped key (scope granted: {}, key scopes: {})",
352 resp.scope,
353 scoped_keys.keys().map(|s| s.as_ref()).collect::<Vec<&str>>().join(", ")
354 );
355 }
356 scoped_keys
357 .into_iter()
358 .map(|(scope, key)| Ok((scope, serde_json::from_value(key)?)))
359 .collect::<Result<Vec<_>>>()?
360 }
361 None => {
362 if sync_scope_granted {
363 error_support::report_error!(
364 "fxaclient-scoped-key",
365 "Sync scope granted, but keys_jwe is None"
366 );
367 }
368 vec![]
369 }
370 };
371
372 if let Err(err) = self
376 .client
377 .destroy_access_token(self.state.config(), &resp.access_token)
378 {
379 warn!("Access token destruction failure: {:?}", err);
380 }
381 let old_refresh_token = self.state.refresh_token().cloned();
382 let new_refresh_token = resp
383 .refresh_token
384 .ok_or(Error::ApiClientError("No refresh token in response"))?;
385 let old_device_info = match old_refresh_token {
388 Some(_) => match self.get_current_device() {
389 Ok(maybe_device) => maybe_device,
390 Err(err) => {
391 warn!("Error while getting previous device information: {:?}", err);
392 None
393 }
394 },
395 None => None,
396 };
397 if let Some(ref refresh_token) = old_refresh_token {
400 if let Err(err) = self
401 .client
402 .destroy_refresh_token(self.state.config(), &refresh_token.token)
403 {
404 warn!("Refresh token destruction failure: {:?}", err);
405 }
406 }
407 if let Some(ref device_info) = old_device_info {
408 if let Err(err) = self.replace_device(
409 &device_info.display_name,
410 &device_info.device_type,
411 &device_info.push_subscription,
412 &device_info.available_commands,
413 ) {
414 warn!("Device information restoration failed: {:?}", err);
415 }
416 }
417 self.state.complete_oauth_flow(
418 scoped_keys,
419 RefreshToken {
420 token: new_refresh_token,
421 scopes: resp.scope.split(' ').map(ToString::to_string).collect(),
422 },
423 resp.session_token,
424 );
425 Ok(())
426 }
427
428 pub fn handle_session_token_change(&mut self, session_token: &str) -> Result<()> {
436 let old_refresh_token = self.state.refresh_token().ok_or(Error::NoRefreshToken)?;
437 let scopes: Vec<&str> = old_refresh_token.scopes.iter().map(AsRef::as_ref).collect();
438 let resp = self.client.create_refresh_token_using_session_token(
439 self.state.config(),
440 session_token,
441 &scopes,
442 )?;
443 let new_refresh_token = resp
444 .refresh_token
445 .ok_or(Error::ApiClientError("No refresh token in response"))?;
446 self.state.update_tokens(
447 session_token.to_owned(),
448 RefreshToken {
449 token: new_refresh_token,
450 scopes: resp.scope.split(' ').map(ToString::to_string).collect(),
451 },
452 );
453 self.clear_devices_and_attached_clients_cache();
454 Ok(())
455 }
456
457 pub fn clear_access_token_cache(&mut self) {
459 self.state.clear_access_token_cache();
460 }
461}
462
463const AUTH_CIRCUIT_BREAKER_CAPACITY: u8 = 5;
464const AUTH_CIRCUIT_BREAKER_RENEWAL_RATE: f32 = 3.0 / 60.0 / 1000.0; #[derive(Clone, Copy)]
467pub(crate) struct AuthCircuitBreaker {
468 rate_limiter: RateLimiter,
469}
470
471impl Default for AuthCircuitBreaker {
472 fn default() -> Self {
473 AuthCircuitBreaker {
474 rate_limiter: RateLimiter::new(
475 AUTH_CIRCUIT_BREAKER_CAPACITY,
476 AUTH_CIRCUIT_BREAKER_RENEWAL_RATE,
477 ),
478 }
479 }
480}
481
482impl AuthCircuitBreaker {
483 pub(crate) fn check(&mut self) -> Result<()> {
484 if !self.rate_limiter.check() {
485 return Err(Error::AuthCircuitBreakerError);
486 }
487 Ok(())
488 }
489}
490
491impl TryFrom<Url> for AuthorizationParameters {
492 type Error = Error;
493
494 fn try_from(url: Url) -> Result<Self> {
495 let query_map: HashMap<String, String> = url.query_pairs().into_owned().collect();
496 let scope = query_map
497 .get("scope")
498 .cloned()
499 .ok_or(Error::MissingUrlParameter("scope"))?;
500 let client_id = query_map
501 .get("client_id")
502 .cloned()
503 .ok_or(Error::MissingUrlParameter("client_id"))?;
504 let state = query_map
505 .get("state")
506 .cloned()
507 .ok_or(Error::MissingUrlParameter("state"))?;
508 let access_type = query_map
509 .get("access_type")
510 .cloned()
511 .ok_or(Error::MissingUrlParameter("access_type"))?;
512 let code_challenge = query_map.get("code_challenge").cloned();
513 let code_challenge_method = query_map.get("code_challenge_method").cloned();
514 let keys_jwk = query_map.get("keys_jwk").cloned();
515 Ok(Self {
516 client_id,
517 scope: scope.split_whitespace().map(|s| s.to_string()).collect(),
518 state,
519 access_type,
520 code_challenge,
521 code_challenge_method,
522 keys_jwk,
523 })
524 }
525}
526
527#[derive(Clone, Serialize, Deserialize)]
528pub struct RefreshToken {
529 pub token: String,
530 pub scopes: HashSet<String>,
531}
532
533impl std::fmt::Debug for RefreshToken {
534 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
535 f.debug_struct("RefreshToken")
536 .field("scopes", &self.scopes)
537 .finish()
538 }
539}
540
541pub struct OAuthFlow {
542 pub scoped_keys_flow: Option<ScopedKeysFlow>,
543 pub code_verifier: String,
544}
545
546#[derive(Clone, Serialize, Deserialize)]
547pub struct AccessTokenInfo {
548 pub scope: String,
549 pub token: String,
550 pub key: Option<ScopedKey>,
551 pub expires_at: u64, }
553
554impl AccessTokenInfo {
555 pub fn check_missing_sync_scoped_key(&self) -> Result<()> {
556 if self.scope == scopes::OLD_SYNC && self.key.is_none() {
557 Err(Error::SyncScopedKeyMissingInServerResponse)
558 } else {
559 Ok(())
560 }
561 }
562}
563
564impl TryFrom<AccessTokenInfo> for crate::AccessTokenInfo {
565 type Error = Error;
566 fn try_from(info: AccessTokenInfo) -> Result<Self> {
567 Ok(crate::AccessTokenInfo {
568 scope: info.scope,
569 token: info.token,
570 key: info.key,
571 expires_at: info.expires_at.try_into()?,
572 })
573 }
574}
575
576impl std::fmt::Debug for AccessTokenInfo {
577 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
578 f.debug_struct("AccessTokenInfo")
579 .field("scope", &self.scope)
580 .field("key", &self.key)
581 .field("expires_at", &self.expires_at)
582 .finish()
583 }
584}
585
586impl From<IntrospectInfo> for crate::AuthorizationInfo {
587 fn from(r: IntrospectInfo) -> Self {
588 crate::AuthorizationInfo { active: r.active }
589 }
590}
591
592#[cfg(test)]
593mod tests {
594 use super::super::{http_client::*, Config};
595 use super::*;
596 use mockall::predicate::always;
597 use mockall::predicate::eq;
598 use std::borrow::Cow;
599 use std::collections::HashMap;
600 use std::sync::Arc;
601
602 impl FirefoxAccount {
603 pub fn add_cached_token(&mut self, scope: &str, token_info: AccessTokenInfo) {
604 self.state.add_cached_access_token(scope, token_info);
605 }
606
607 pub fn set_session_token(&mut self, session_token: &str) {
608 self.state.set_session_token(session_token.to_owned());
609 }
610 }
611
612 #[test]
613 fn test_oauth_flow_url() {
614 nss::ensure_initialized();
615 let config = Config::new_with_mock_well_known_fxa_client_configuration(
616 "https://mock-fxa.example.com",
617 "12345678",
618 "https://foo.bar",
619 );
620 let mut fxa = FirefoxAccount::with_config(config);
621 let url = fxa
622 .begin_oauth_flow(&["profile"], "test_oauth_flow_url")
623 .unwrap();
624 let flow_url = Url::parse(&url).unwrap();
625
626 assert_eq!(flow_url.path(), "/authorization");
627
628 let mut pairs = flow_url.query_pairs();
629 assert_eq!(pairs.count(), 11);
630 assert_eq!(
631 pairs.next(),
632 Some((Cow::Borrowed("action"), Cow::Borrowed("email")))
633 );
634 assert_eq!(
635 pairs.next(),
636 Some((Cow::Borrowed("response_type"), Cow::Borrowed("code")))
637 );
638 assert_eq!(
639 pairs.next(),
640 Some((
641 Cow::Borrowed("entrypoint"),
642 Cow::Borrowed("test_oauth_flow_url")
643 ))
644 );
645 assert_eq!(
646 pairs.next(),
647 Some((Cow::Borrowed("client_id"), Cow::Borrowed("12345678")))
648 );
649
650 assert_eq!(
651 pairs.next(),
652 Some((Cow::Borrowed("scope"), Cow::Borrowed("profile")))
653 );
654 let state_param = pairs.next().unwrap();
655 assert_eq!(state_param.0, Cow::Borrowed("state"));
656 assert_eq!(state_param.1.len(), 22);
657 assert_eq!(
658 pairs.next(),
659 Some((
660 Cow::Borrowed("code_challenge_method"),
661 Cow::Borrowed("S256")
662 ))
663 );
664 let code_challenge_param = pairs.next().unwrap();
665 assert_eq!(code_challenge_param.0, Cow::Borrowed("code_challenge"));
666 assert_eq!(code_challenge_param.1.len(), 43);
667 assert_eq!(
668 pairs.next(),
669 Some((Cow::Borrowed("access_type"), Cow::Borrowed("offline")))
670 );
671 let keys_jwk = pairs.next().unwrap();
672 assert_eq!(keys_jwk.0, Cow::Borrowed("keys_jwk"));
673 assert_eq!(keys_jwk.1.len(), 168);
674
675 assert_eq!(
676 pairs.next(),
677 Some((
678 Cow::Borrowed("redirect_uri"),
679 Cow::Borrowed("https://foo.bar")
680 ))
681 );
682 }
683
684 #[test]
685 fn test_force_auth_url() {
686 nss::ensure_initialized();
687 let config = Config::stable_dev("12345678", "https://foo.bar");
688 let mut fxa = FirefoxAccount::with_config(config);
689 let email = "test@example.com";
690 fxa.add_cached_profile("123", email);
691 let url = fxa
692 .begin_oauth_flow(&["profile"], "test_force_auth_url")
693 .unwrap();
694 let url = Url::parse(&url).unwrap();
695 assert_eq!(url.path(), "/oauth/force_auth");
696 let mut pairs = url.query_pairs();
697 assert_eq!(
698 pairs.find(|e| e.0 == "email"),
699 Some((Cow::Borrowed("email"), Cow::Borrowed(email),))
700 );
701 }
702
703 #[test]
704 fn test_webchannel_context_url() {
705 nss::ensure_initialized();
706 const SCOPES: &[&str] = &["https://identity.mozilla.com/apps/oldsync"];
707 let config = Config::new_with_mock_well_known_fxa_client_configuration(
708 "https://mock-fxa.example.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_with_mock_well_known_fxa_client_configuration(
1090 "mock-fxa.example.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}