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