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