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 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 viaduct_dev::use_dev_backend();
617 let config = Config::new(
618 "https://accounts.firefox.com",
619 "12345678",
620 "https://foo.bar",
621 );
622 let mut fxa = FirefoxAccount::with_config(config);
623 let url = fxa
624 .begin_oauth_flow(&["profile"], "test_oauth_flow_url")
625 .unwrap();
626 let flow_url = Url::parse(&url).unwrap();
627
628 assert_eq!(flow_url.host_str(), Some("accounts.firefox.com"));
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 viaduct_dev::use_dev_backend();
711 const SCOPES: &[&str] = &["https://identity.mozilla.com/apps/oldsync"];
712 let config = Config::new(
713 "https://accounts.firefox.com",
714 "12345678",
715 "urn:ietf:wg:oauth:2.0:oob:oauth-redirect-webchannel",
716 );
717 let mut fxa = FirefoxAccount::with_config(config);
718 let url = fxa
719 .begin_oauth_flow(SCOPES, "test_webchannel_context_url")
720 .unwrap();
721 let url = Url::parse(&url).unwrap();
722 let query_params: HashMap<_, _> = url.query_pairs().into_owned().collect();
723 let context = &query_params["context"];
724 assert_eq!(context, "oauth_webchannel_v1");
725 assert_eq!(query_params.get("redirect_uri"), None);
726 }
727
728 #[test]
729 fn test_webchannel_pairing_context_url() {
730 nss::ensure_initialized();
731 const SCOPES: &[&str] = &["https://identity.mozilla.com/apps/oldsync"];
732 const PAIRING_URL: &str = "https://accounts.firefox.com/pair#channel_id=658db7fe98b249a5897b884f98fb31b7&channel_key=1hIDzTj5oY2HDeSg_jA2DhcOcAn5Uqq0cAYlZRNUIo4";
733
734 let config = Config::new(
735 "https://accounts.firefox.com",
736 "12345678",
737 "urn:ietf:wg:oauth:2.0:oob:oauth-redirect-webchannel",
738 );
739 let mut fxa = FirefoxAccount::with_config(config);
740 let url = fxa
741 .begin_pairing_flow(PAIRING_URL, SCOPES, "test_webchannel_pairing_context_url")
742 .unwrap();
743 let url = Url::parse(&url).unwrap();
744 let query_params: HashMap<_, _> = url.query_pairs().into_owned().collect();
745 let context = &query_params["context"];
746 assert_eq!(context, "oauth_webchannel_v1");
747 assert_eq!(query_params.get("redirect_uri"), None);
748 }
749
750 #[test]
751 fn test_pairing_flow_url() {
752 nss::ensure_initialized();
753 const SCOPES: &[&str] = &["https://identity.mozilla.com/apps/oldsync"];
754 const PAIRING_URL: &str = "https://accounts.firefox.com/pair#channel_id=658db7fe98b249a5897b884f98fb31b7&channel_key=1hIDzTj5oY2HDeSg_jA2DhcOcAn5Uqq0cAYlZRNUIo4";
755 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";
756
757 let config = Config::new(
758 "https://accounts.firefox.com",
759 "12345678",
760 "https://foo.bar",
761 );
762
763 let mut fxa = FirefoxAccount::with_config(config);
764 let url = fxa
765 .begin_pairing_flow(PAIRING_URL, SCOPES, "test_pairing_flow_url")
766 .unwrap();
767 let flow_url = Url::parse(&url).unwrap();
768 let expected_parsed_url = Url::parse(EXPECTED_URL).unwrap();
769
770 assert_eq!(flow_url.host_str(), Some("accounts.firefox.com"));
771 assert_eq!(flow_url.path(), "/pair/supp");
772 assert_eq!(flow_url.fragment(), expected_parsed_url.fragment());
773
774 let mut pairs = flow_url.query_pairs();
775 assert_eq!(pairs.count(), 9);
776 assert_eq!(
777 pairs.next(),
778 Some((
779 Cow::Borrowed("entrypoint"),
780 Cow::Borrowed("test_pairing_flow_url")
781 ))
782 );
783 assert_eq!(
784 pairs.next(),
785 Some((Cow::Borrowed("client_id"), Cow::Borrowed("12345678")))
786 );
787 assert_eq!(
788 pairs.next(),
789 Some((
790 Cow::Borrowed("scope"),
791 Cow::Borrowed("https://identity.mozilla.com/apps/oldsync")
792 ))
793 );
794
795 let state_param = pairs.next().unwrap();
796 assert_eq!(state_param.0, Cow::Borrowed("state"));
797 assert_eq!(state_param.1.len(), 22);
798 assert_eq!(
799 pairs.next(),
800 Some((
801 Cow::Borrowed("code_challenge_method"),
802 Cow::Borrowed("S256")
803 ))
804 );
805 let code_challenge_param = pairs.next().unwrap();
806 assert_eq!(code_challenge_param.0, Cow::Borrowed("code_challenge"));
807 assert_eq!(code_challenge_param.1.len(), 43);
808 assert_eq!(
809 pairs.next(),
810 Some((Cow::Borrowed("access_type"), Cow::Borrowed("offline")))
811 );
812 let keys_jwk = pairs.next().unwrap();
813 assert_eq!(keys_jwk.0, Cow::Borrowed("keys_jwk"));
814 assert_eq!(keys_jwk.1.len(), 168);
815
816 assert_eq!(
817 pairs.next(),
818 Some((
819 Cow::Borrowed("redirect_uri"),
820 Cow::Borrowed("https://foo.bar")
821 ))
822 );
823 }
824
825 #[test]
826 fn test_pairing_flow_origin_mismatch() {
827 nss::ensure_initialized();
828 static PAIRING_URL: &str = "https://bad.origin.com/pair#channel_id=foo&channel_key=bar";
829 let config = Config::stable_dev("12345678", "https://foo.bar");
830 let mut fxa = FirefoxAccount::with_config(config);
831 let url = fxa.begin_pairing_flow(
832 PAIRING_URL,
833 &["https://identity.mozilla.com/apps/oldsync"],
834 "test_pairiong_flow_origin_mismatch",
835 );
836
837 assert!(url.is_err());
838
839 match url {
840 Ok(_) => {
841 panic!("should have error");
842 }
843 Err(err) => match err {
844 Error::OriginMismatch { .. } => {}
845 _ => panic!("error not OriginMismatch"),
846 },
847 }
848 }
849
850 #[test]
851 fn test_check_authorization_status() {
852 nss::ensure_initialized();
853 let config = Config::stable_dev("12345678", "https://foo.bar");
854 let mut fxa = FirefoxAccount::with_config(config);
855
856 let refresh_token_scopes = std::collections::HashSet::new();
857 fxa.state.force_refresh_token(RefreshToken {
858 token: "refresh_token".to_owned(),
859 scopes: refresh_token_scopes,
860 });
861
862 let mut client = MockFxAClient::new();
863 client
864 .expect_check_refresh_token_status()
865 .with(always(), eq("refresh_token"))
866 .times(1)
867 .returning(|_, _| Ok(IntrospectResponse { active: true }));
868 fxa.set_client(Arc::new(client));
869
870 let auth_status = fxa.check_authorization_status().unwrap();
871 assert!(auth_status.active);
872 }
873
874 #[test]
875 fn test_check_authorization_status_circuit_breaker() {
876 nss::ensure_initialized();
877 let config = Config::stable_dev("12345678", "https://foo.bar");
878 let mut fxa = FirefoxAccount::with_config(config);
879
880 let refresh_token_scopes = std::collections::HashSet::new();
881 fxa.state.force_refresh_token(RefreshToken {
882 token: "refresh_token".to_owned(),
883 scopes: refresh_token_scopes,
884 });
885
886 let mut client = MockFxAClient::new();
887 client
890 .expect_check_refresh_token_status()
891 .with(always(), eq("refresh_token"))
892 .returning(|_, _| Ok(IntrospectResponse { active: true }));
893 client
894 .expect_check_refresh_token_status()
895 .with(always(), eq("refresh_token"))
896 .returning(|_, _| Ok(IntrospectResponse { active: true }));
897 client
898 .expect_check_refresh_token_status()
899 .with(always(), eq("refresh_token"))
900 .returning(|_, _| Ok(IntrospectResponse { active: true }));
901 client
902 .expect_check_refresh_token_status()
903 .with(always(), eq("refresh_token"))
904 .returning(|_, _| Ok(IntrospectResponse { active: true }));
905 client
906 .expect_check_refresh_token_status()
907 .with(always(), eq("refresh_token"))
908 .returning(|_, _| Ok(IntrospectResponse { active: true }));
909 fxa.set_client(Arc::new(client));
911
912 for _ in 0..5 {
913 assert!(fxa.check_authorization_status().is_ok());
914 }
915 match fxa.check_authorization_status() {
916 Ok(_) => unreachable!("should not happen"),
917 Err(err) => assert!(matches!(err, Error::AuthCircuitBreakerError)),
918 }
919 }
920
921 use crate::internal::scopes::{self, OLD_SYNC};
922
923 #[test]
924 fn test_auth_code_pair_valid_not_allowed_scope() {
925 nss::ensure_initialized();
926 let config = Config::stable_dev("12345678", "https://foo.bar");
927 let mut fxa = FirefoxAccount::with_config(config);
928 fxa.set_session_token("session");
929 let mut client = MockFxAClient::new();
930 let not_allowed_scope = "https://identity.mozilla.com/apps/lockbox";
931 let expected_scopes = scopes::OLD_SYNC
932 .chars()
933 .chain(std::iter::once(' '))
934 .chain(not_allowed_scope.chars())
935 .collect::<String>();
936 client
937 .expect_get_scoped_key_data()
938 .with(always(), eq("session"), eq("12345678"), eq(expected_scopes))
939 .times(1)
940 .returning(|_, _, _, _| {
941 Err(Error::RemoteError {
942 code: 400,
943 errno: 163,
944 error: "Invalid Scopes".to_string(),
945 message: "Not allowed to request scopes".to_string(),
946 info: "fyi, there was a server error".to_string(),
947 })
948 });
949 fxa.set_client(Arc::new(client));
950 let auth_params = AuthorizationParameters {
951 client_id: "12345678".to_string(),
952 scope: vec![scopes::OLD_SYNC.to_string(), not_allowed_scope.to_string()],
953 state: "somestate".to_string(),
954 access_type: "offline".to_string(),
955 code_challenge: None,
956 code_challenge_method: None,
957 keys_jwk: None,
958 };
959 let res = fxa.authorize_code_using_session_token(auth_params);
960 assert!(res.is_err());
961 let err = res.unwrap_err();
962 if let Error::RemoteError {
963 code,
964 errno,
965 error: _,
966 message: _,
967 info: _,
968 } = err
969 {
970 assert_eq!(code, 400);
971 assert_eq!(errno, 163); } else {
973 panic!("Should return an error from the server specifying that the requested scopes are not allowed");
974 }
975 }
976
977 #[test]
978 fn test_auth_code_pair_invalid_scope_not_allowed() {
979 nss::ensure_initialized();
980 let config = Config::stable_dev("12345678", "https://foo.bar");
981 let mut fxa = FirefoxAccount::with_config(config);
982 fxa.set_session_token("session");
983 let mut client = MockFxAClient::new();
984 let invalid_scope = "IamAnInvalidScope";
985 let expected_scopes = scopes::OLD_SYNC
986 .chars()
987 .chain(std::iter::once(' '))
988 .chain(invalid_scope.chars())
989 .collect::<String>();
990 client
991 .expect_get_scoped_key_data()
992 .with(always(), eq("session"), eq("12345678"), eq(expected_scopes))
993 .times(1)
994 .returning(|_, _, _, _| {
995 let mut server_ret = HashMap::new();
996 server_ret.insert(
997 scopes::OLD_SYNC.to_string(),
998 ScopedKeyDataResponse {
999 key_rotation_secret: "IamASecret".to_string(),
1000 key_rotation_timestamp: 100,
1001 identifier: "".to_string(),
1002 },
1003 );
1004 Ok(server_ret)
1005 });
1006 fxa.set_client(Arc::new(client));
1007
1008 let auth_params = AuthorizationParameters {
1009 client_id: "12345678".to_string(),
1010 scope: vec![scopes::OLD_SYNC.to_string(), invalid_scope.to_string()],
1011 state: "somestate".to_string(),
1012 access_type: "offline".to_string(),
1013 code_challenge: None,
1014 code_challenge_method: None,
1015 keys_jwk: None,
1016 };
1017 let res = fxa.authorize_code_using_session_token(auth_params);
1018 assert!(res.is_err());
1019 let err = res.unwrap_err();
1020 if let Error::ScopeNotAllowed(client_id, scope) = err {
1021 assert_eq!(client_id, "12345678");
1022 assert_eq!(scope, "IamAnInvalidScope");
1023 } else {
1024 panic!("Should return an error that specifies the scope that is not allowed");
1025 }
1026 }
1027
1028 #[test]
1029 fn test_auth_code_pair_scope_not_in_state() {
1030 nss::ensure_initialized();
1031 let config = Config::stable_dev("12345678", "https://foo.bar");
1032 let mut fxa = FirefoxAccount::with_config(config);
1033 fxa.set_session_token("session");
1034 let mut client = MockFxAClient::new();
1035 client
1036 .expect_get_scoped_key_data()
1037 .with(
1038 always(),
1039 eq("session"),
1040 eq("12345678"),
1041 eq(scopes::OLD_SYNC),
1042 )
1043 .times(1)
1044 .returning(|_, _, _, _| {
1045 let mut server_ret = HashMap::new();
1046 server_ret.insert(
1047 scopes::OLD_SYNC.to_string(),
1048 ScopedKeyDataResponse {
1049 key_rotation_secret: "IamASecret".to_string(),
1050 key_rotation_timestamp: 100,
1051 identifier: "".to_string(),
1052 },
1053 );
1054 Ok(server_ret)
1055 });
1056 fxa.set_client(Arc::new(client));
1057 let auth_params = AuthorizationParameters {
1058 client_id: "12345678".to_string(),
1059 scope: vec![scopes::OLD_SYNC.to_string()],
1060 state: "somestate".to_string(),
1061 access_type: "offline".to_string(),
1062 code_challenge: None,
1063 code_challenge_method: None,
1064 keys_jwk: Some("IAmAVerySecretKeysJWkInBase64".to_string()),
1065 };
1066 let res = fxa.authorize_code_using_session_token(auth_params);
1067 assert!(res.is_err());
1068 let err = res.unwrap_err();
1069 if let Error::NoScopedKey(scope) = err {
1070 assert_eq!(scope, scopes::OLD_SYNC.to_string());
1071 } else {
1072 panic!("Should return an error that specifies the scope that is not in the state");
1073 }
1074 }
1075
1076 #[test]
1077 fn test_set_user_data_sets_session_token() {
1078 nss::ensure_initialized();
1079 let config = Config::stable_dev("12345678", "https://foo.bar");
1080 let mut fxa = FirefoxAccount::with_config(config);
1081 let user_data = UserData {
1082 session_token: String::from("mock_session_token"),
1083 uid: String::from("mock_uid_unused"),
1084 email: String::from("mock_email_usued"),
1085 verified: true,
1086 };
1087 fxa.set_user_data(user_data);
1088 assert_eq!(fxa.get_session_token().unwrap(), "mock_session_token");
1089 }
1090
1091 #[test]
1092 fn test_oauth_request_sent_with_session_when_available() {
1093 nss::ensure_initialized();
1094 viaduct_dev::use_dev_backend();
1095 let config = Config::new(
1096 "https://accounts.firefox.com",
1097 "12345678",
1098 "https://foo.bar",
1099 );
1100 let mut fxa = FirefoxAccount::with_config(config);
1101 let url = fxa
1102 .begin_oauth_flow(&[OLD_SYNC, "profile"], "test_entrypoint")
1103 .unwrap();
1104 let url = Url::parse(&url).unwrap();
1105 let state = url.query_pairs().find(|(name, _)| name == "state").unwrap();
1106 let user_data = UserData {
1107 session_token: String::from("mock_session_token"),
1108 uid: String::from("mock_uid_unused"),
1109 email: String::from("mock_email_usued"),
1110 verified: true,
1111 };
1112 let mut client = MockFxAClient::new();
1113
1114 client
1115 .expect_create_refresh_token_using_authorization_code()
1116 .withf(|_, session_token, code, _| {
1117 matches!(session_token, Some("mock_session_token")) && code == "mock_code"
1118 })
1119 .times(1)
1120 .returning(|_, _, _, _| {
1121 Ok(OAuthTokenResponse {
1122 keys_jwe: None,
1123 refresh_token: Some("refresh_token".to_string()),
1124 session_token: None,
1125 expires_in: 1,
1126 scope: "profile".to_string(),
1127 access_token: "access_token".to_string(),
1128 })
1129 });
1130 client
1131 .expect_destroy_access_token()
1132 .with(always(), always())
1133 .times(1)
1134 .returning(|_, _| Ok(()));
1135 fxa.set_client(Arc::new(client));
1136
1137 fxa.set_user_data(user_data);
1138
1139 fxa.complete_oauth_flow("mock_code", state.1.as_ref())
1140 .unwrap();
1141 }
1142}