fxa_client/internal/
oauth.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5pub 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;
27// If a cached token has less than `OAUTH_MIN_TIME_LEFT` seconds left to live,
28// it will be considered already expired.
29const OAUTH_MIN_TIME_LEFT: u64 = 60;
30// Special redirect urn based on the OAuth native spec, signals that the
31// WebChannel flow is used
32pub const OAUTH_WEBCHANNEL_REDIRECT: &str = "urn:ietf:wg:oauth:2.0:oob:oauth-redirect-webchannel";
33
34impl FirefoxAccount {
35    /// Fetch a short-lived access token using the saved refresh token.
36    /// If there is no refresh token held or if it is not authorized for some of the requested
37    /// scopes, this method will error-out and a login flow will need to be initiated
38    /// using `begin_oauth_flow`.
39    ///
40    /// * `scopes` - Space-separated list of requested scopes.
41    /// * `ttl` - the ttl in seconds of the token requested from the server.
42    ///
43    /// **💾 This method may alter the persisted account state.**
44    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 the cached key is missing the required sync scoped key, try to fetch it again
51                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    /// Sets the user data (session token, email, uid)
95    pub fn set_user_data(&mut self, user_data: UserData) {
96        // for now, we only have use for the session token
97        // if we'd like to implement a "Signed in but not verified" state
98        // we would also consume the other parts of the user data
99        self.state.set_session_token(user_data.session_token)
100    }
101
102    /// Retrieve the current session token from state
103    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    /// Check whether user is authorized using our refresh token.
111    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    /// Initiate a pairing flow and return a URL that should be navigated to.
126    ///
127    /// * `pairing_url` - A pairing URL obtained by scanning a QR code produced by
128    ///   the pairing authority.
129    /// * `scopes` - Space-separated list of requested scopes by the pairing supplicant.
130    /// * `entrypoint` - The entrypoint to be used for data collection
131    /// * `metrics` - Optional parameters for metrics
132    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    /// Initiate an OAuth login flow and return a URL that should be navigated to.
153    ///
154    /// * `scopes` - Space-separated list of requested scopes.
155    /// * `entrypoint` - The entrypoint to be used for metrics
156    /// * `metrics` - Optional metrics parameters
157    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                // Union of the already held scopes and the one requested.
178                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    /// Fetch an OAuth code for a particular client using a session token from the account state.
193    ///
194    /// * `auth_params` Authorization parameters  which includes:
195    ///     *  `client_id` - OAuth client id.
196    ///     *  `scope` - list of requested scopes.
197    ///     *  `state` - OAuth state.
198    ///     *  `access_type` - Type of OAuth access, can be "offline" and "online"
199    ///     *  `pkce_params` - Optional PKCE parameters for public clients (`code_challenge` and `code_challenge_method`)
200    ///     *  `keys_jwk` - Optional JWK used to encrypt scoped keys
201    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        // Validate request to ensure that the client is actually allowed to request
208        // the scopes they requested
209        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    /// Complete an OAuth flow initiated in `begin_oauth_flow` or `begin_pairing_flow`.
310    /// The `code` and `state` parameters can be obtained by parsing out the
311    /// redirect URL after a successful login.
312    ///
313    /// **💾 This method alters the persisted account state.**
314    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    /// Cancel any in-progress oauth flows
330    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        // We are only interested in the refresh token at this time because we
373        // don't want to return an over-scoped access token.
374        // Let's be good citizens and destroy this access token.
375        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        // Destroying a refresh token also destroys its associated device,
386        // grab the device information for replication later.
387        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        // In order to keep 1 and only 1 refresh token alive per client instance,
398        // we also destroy the existing refresh token.
399        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    /// Typically called during a password change flow.
429    /// Invalidates all tokens and fetches a new refresh token.
430    /// Because the old refresh token is not valid anymore, we can't do like `handle_oauth_response`
431    /// and re-create the device, so it is the responsibility of the caller to do so after we're
432    /// done.
433    ///
434    /// **💾 This method alters the persisted account state.**
435    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    /// **💾 This method may alter the persisted account state.**
458    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; // 3 tokens every minute.
465
466#[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, // seconds since epoch
552}
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        // FIXME: this test shouldn't make network requests.
616        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        // FIXME: this test shouldn't make network requests.
710        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        // This copy-pasta (equivalent to `.returns(..).times(5)`) is there
888        // because `Error` is not cloneable :/
889        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        //mockall expects calls to be processed in the order they are registered. So, no need for to use a method like expect_check_refresh_token_status_calls_in_order()
910        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); // Requested scopes not allowed
972        } 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}