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    pub(crate) fn handle_oauth_response(
330        &mut self,
331        resp: OAuthTokenResponse,
332        scoped_keys_flow: Option<ScopedKeysFlow>,
333    ) -> Result<()> {
334        let sync_scope_granted = resp.scope.split(' ').any(|s| s == scopes::OLD_SYNC);
335        let scoped_keys = match resp.keys_jwe {
336            Some(ref jwe) => {
337                let scoped_keys_flow = scoped_keys_flow.ok_or(Error::ApiClientError(
338                    "Got a JWE but have no JWK to decrypt it.",
339                ))?;
340                let decrypted_keys = scoped_keys_flow.decrypt_keys_jwe(jwe)?;
341                let scoped_keys: serde_json::Map<String, serde_json::Value> =
342                    serde_json::from_str(&decrypted_keys)?;
343                if sync_scope_granted && !scoped_keys.contains_key(scopes::OLD_SYNC) {
344                    error_support::report_error!(
345                        "fxaclient-scoped-key",
346                        "Sync scope granted, but no sync scoped key (scope granted: {}, key scopes: {})",
347                        resp.scope,
348                        scoped_keys.keys().map(|s| s.as_ref()).collect::<Vec<&str>>().join(", ")
349                    );
350                }
351                scoped_keys
352                    .into_iter()
353                    .map(|(scope, key)| Ok((scope, serde_json::from_value(key)?)))
354                    .collect::<Result<Vec<_>>>()?
355            }
356            None => {
357                if sync_scope_granted {
358                    error_support::report_error!(
359                        "fxaclient-scoped-key",
360                        "Sync scope granted, but keys_jwe is None"
361                    );
362                }
363                vec![]
364            }
365        };
366
367        // We are only interested in the refresh token at this time because we
368        // don't want to return an over-scoped access token.
369        // Let's be good citizens and destroy this access token.
370        if let Err(err) = self
371            .client
372            .destroy_access_token(self.state.config(), &resp.access_token)
373        {
374            warn!("Access token destruction failure: {:?}", err);
375        }
376        let old_refresh_token = self.state.refresh_token().cloned();
377        let new_refresh_token = resp
378            .refresh_token
379            .ok_or(Error::ApiClientError("No refresh token in response"))?;
380        // Destroying a refresh token also destroys its associated device,
381        // grab the device information for replication later.
382        let old_device_info = match old_refresh_token {
383            Some(_) => match self.get_current_device() {
384                Ok(maybe_device) => maybe_device,
385                Err(err) => {
386                    warn!("Error while getting previous device information: {:?}", err);
387                    None
388                }
389            },
390            None => None,
391        };
392        // In order to keep 1 and only 1 refresh token alive per client instance,
393        // we also destroy the existing refresh token.
394        if let Some(ref refresh_token) = old_refresh_token {
395            if let Err(err) = self
396                .client
397                .destroy_refresh_token(self.state.config(), &refresh_token.token)
398            {
399                warn!("Refresh token destruction failure: {:?}", err);
400            }
401        }
402        if let Some(ref device_info) = old_device_info {
403            if let Err(err) = self.replace_device(
404                &device_info.display_name,
405                &device_info.device_type,
406                &device_info.push_subscription,
407                &device_info.available_commands,
408            ) {
409                warn!("Device information restoration failed: {:?}", err);
410            }
411        }
412        self.state.complete_oauth_flow(
413            scoped_keys,
414            RefreshToken {
415                token: new_refresh_token,
416                scopes: resp.scope.split(' ').map(ToString::to_string).collect(),
417            },
418            resp.session_token,
419        );
420        Ok(())
421    }
422
423    /// Typically called during a password change flow.
424    /// Invalidates all tokens and fetches a new refresh token.
425    /// Because the old refresh token is not valid anymore, we can't do like `handle_oauth_response`
426    /// and re-create the device, so it is the responsibility of the caller to do so after we're
427    /// done.
428    ///
429    /// **💾 This method alters the persisted account state.**
430    pub fn handle_session_token_change(&mut self, session_token: &str) -> Result<()> {
431        let old_refresh_token = self.state.refresh_token().ok_or(Error::NoRefreshToken)?;
432        let scopes: Vec<&str> = old_refresh_token.scopes.iter().map(AsRef::as_ref).collect();
433        let resp = self.client.create_refresh_token_using_session_token(
434            self.state.config(),
435            session_token,
436            &scopes,
437        )?;
438        let new_refresh_token = resp
439            .refresh_token
440            .ok_or(Error::ApiClientError("No refresh token in response"))?;
441        self.state.update_tokens(
442            session_token.to_owned(),
443            RefreshToken {
444                token: new_refresh_token,
445                scopes: resp.scope.split(' ').map(ToString::to_string).collect(),
446            },
447        );
448        self.clear_devices_and_attached_clients_cache();
449        Ok(())
450    }
451
452    /// **💾 This method may alter the persisted account state.**
453    pub fn clear_access_token_cache(&mut self) {
454        self.state.clear_access_token_cache();
455    }
456}
457
458const AUTH_CIRCUIT_BREAKER_CAPACITY: u8 = 5;
459const AUTH_CIRCUIT_BREAKER_RENEWAL_RATE: f32 = 3.0 / 60.0 / 1000.0; // 3 tokens every minute.
460
461#[derive(Clone, Copy)]
462pub(crate) struct AuthCircuitBreaker {
463    rate_limiter: RateLimiter,
464}
465
466impl Default for AuthCircuitBreaker {
467    fn default() -> Self {
468        AuthCircuitBreaker {
469            rate_limiter: RateLimiter::new(
470                AUTH_CIRCUIT_BREAKER_CAPACITY,
471                AUTH_CIRCUIT_BREAKER_RENEWAL_RATE,
472            ),
473        }
474    }
475}
476
477impl AuthCircuitBreaker {
478    pub(crate) fn check(&mut self) -> Result<()> {
479        if !self.rate_limiter.check() {
480            return Err(Error::AuthCircuitBreakerError);
481        }
482        Ok(())
483    }
484}
485
486impl TryFrom<Url> for AuthorizationParameters {
487    type Error = Error;
488
489    fn try_from(url: Url) -> Result<Self> {
490        let query_map: HashMap<String, String> = url.query_pairs().into_owned().collect();
491        let scope = query_map
492            .get("scope")
493            .cloned()
494            .ok_or(Error::MissingUrlParameter("scope"))?;
495        let client_id = query_map
496            .get("client_id")
497            .cloned()
498            .ok_or(Error::MissingUrlParameter("client_id"))?;
499        let state = query_map
500            .get("state")
501            .cloned()
502            .ok_or(Error::MissingUrlParameter("state"))?;
503        let access_type = query_map
504            .get("access_type")
505            .cloned()
506            .ok_or(Error::MissingUrlParameter("access_type"))?;
507        let code_challenge = query_map.get("code_challenge").cloned();
508        let code_challenge_method = query_map.get("code_challenge_method").cloned();
509        let keys_jwk = query_map.get("keys_jwk").cloned();
510        Ok(Self {
511            client_id,
512            scope: scope.split_whitespace().map(|s| s.to_string()).collect(),
513            state,
514            access_type,
515            code_challenge,
516            code_challenge_method,
517            keys_jwk,
518        })
519    }
520}
521
522#[derive(Clone, Serialize, Deserialize)]
523pub struct RefreshToken {
524    pub token: String,
525    pub scopes: HashSet<String>,
526}
527
528impl std::fmt::Debug for RefreshToken {
529    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
530        f.debug_struct("RefreshToken")
531            .field("scopes", &self.scopes)
532            .finish()
533    }
534}
535
536pub struct OAuthFlow {
537    pub scoped_keys_flow: Option<ScopedKeysFlow>,
538    pub code_verifier: String,
539}
540
541#[derive(Clone, Serialize, Deserialize)]
542pub struct AccessTokenInfo {
543    pub scope: String,
544    pub token: String,
545    pub key: Option<ScopedKey>,
546    pub expires_at: u64, // seconds since epoch
547}
548
549impl AccessTokenInfo {
550    pub fn check_missing_sync_scoped_key(&self) -> Result<()> {
551        if self.scope == scopes::OLD_SYNC && self.key.is_none() {
552            Err(Error::SyncScopedKeyMissingInServerResponse)
553        } else {
554            Ok(())
555        }
556    }
557}
558
559impl TryFrom<AccessTokenInfo> for crate::AccessTokenInfo {
560    type Error = Error;
561    fn try_from(info: AccessTokenInfo) -> Result<Self> {
562        Ok(crate::AccessTokenInfo {
563            scope: info.scope,
564            token: info.token,
565            key: info.key,
566            expires_at: info.expires_at.try_into()?,
567        })
568    }
569}
570
571impl std::fmt::Debug for AccessTokenInfo {
572    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
573        f.debug_struct("AccessTokenInfo")
574            .field("scope", &self.scope)
575            .field("key", &self.key)
576            .field("expires_at", &self.expires_at)
577            .finish()
578    }
579}
580
581impl From<IntrospectInfo> for crate::AuthorizationInfo {
582    fn from(r: IntrospectInfo) -> Self {
583        crate::AuthorizationInfo { active: r.active }
584    }
585}
586
587#[cfg(test)]
588mod tests {
589    use super::super::{http_client::*, Config};
590    use super::*;
591    use mockall::predicate::always;
592    use mockall::predicate::eq;
593    use std::borrow::Cow;
594    use std::collections::HashMap;
595    use std::sync::Arc;
596
597    impl FirefoxAccount {
598        pub fn add_cached_token(&mut self, scope: &str, token_info: AccessTokenInfo) {
599            self.state.add_cached_access_token(scope, token_info);
600        }
601
602        pub fn set_session_token(&mut self, session_token: &str) {
603            self.state.set_session_token(session_token.to_owned());
604        }
605    }
606
607    #[test]
608    fn test_oauth_flow_url() {
609        nss::ensure_initialized();
610        // FIXME: this test shouldn't make network requests.
611        viaduct_reqwest::use_reqwest_backend();
612        let config = Config::new(
613            "https://accounts.firefox.com",
614            "12345678",
615            "https://foo.bar",
616        );
617        let mut fxa = FirefoxAccount::with_config(config);
618        let url = fxa
619            .begin_oauth_flow(&["profile"], "test_oauth_flow_url")
620            .unwrap();
621        let flow_url = Url::parse(&url).unwrap();
622
623        assert_eq!(flow_url.host_str(), Some("accounts.firefox.com"));
624        assert_eq!(flow_url.path(), "/authorization");
625
626        let mut pairs = flow_url.query_pairs();
627        assert_eq!(pairs.count(), 11);
628        assert_eq!(
629            pairs.next(),
630            Some((Cow::Borrowed("action"), Cow::Borrowed("email")))
631        );
632        assert_eq!(
633            pairs.next(),
634            Some((Cow::Borrowed("response_type"), Cow::Borrowed("code")))
635        );
636        assert_eq!(
637            pairs.next(),
638            Some((
639                Cow::Borrowed("entrypoint"),
640                Cow::Borrowed("test_oauth_flow_url")
641            ))
642        );
643        assert_eq!(
644            pairs.next(),
645            Some((Cow::Borrowed("client_id"), Cow::Borrowed("12345678")))
646        );
647
648        assert_eq!(
649            pairs.next(),
650            Some((Cow::Borrowed("scope"), Cow::Borrowed("profile")))
651        );
652        let state_param = pairs.next().unwrap();
653        assert_eq!(state_param.0, Cow::Borrowed("state"));
654        assert_eq!(state_param.1.len(), 22);
655        assert_eq!(
656            pairs.next(),
657            Some((
658                Cow::Borrowed("code_challenge_method"),
659                Cow::Borrowed("S256")
660            ))
661        );
662        let code_challenge_param = pairs.next().unwrap();
663        assert_eq!(code_challenge_param.0, Cow::Borrowed("code_challenge"));
664        assert_eq!(code_challenge_param.1.len(), 43);
665        assert_eq!(
666            pairs.next(),
667            Some((Cow::Borrowed("access_type"), Cow::Borrowed("offline")))
668        );
669        let keys_jwk = pairs.next().unwrap();
670        assert_eq!(keys_jwk.0, Cow::Borrowed("keys_jwk"));
671        assert_eq!(keys_jwk.1.len(), 168);
672
673        assert_eq!(
674            pairs.next(),
675            Some((
676                Cow::Borrowed("redirect_uri"),
677                Cow::Borrowed("https://foo.bar")
678            ))
679        );
680    }
681
682    #[test]
683    fn test_force_auth_url() {
684        nss::ensure_initialized();
685        let config = Config::stable_dev("12345678", "https://foo.bar");
686        let mut fxa = FirefoxAccount::with_config(config);
687        let email = "test@example.com";
688        fxa.add_cached_profile("123", email);
689        let url = fxa
690            .begin_oauth_flow(&["profile"], "test_force_auth_url")
691            .unwrap();
692        let url = Url::parse(&url).unwrap();
693        assert_eq!(url.path(), "/oauth/force_auth");
694        let mut pairs = url.query_pairs();
695        assert_eq!(
696            pairs.find(|e| e.0 == "email"),
697            Some((Cow::Borrowed("email"), Cow::Borrowed(email),))
698        );
699    }
700
701    #[test]
702    fn test_webchannel_context_url() {
703        nss::ensure_initialized();
704        // FIXME: this test shouldn't make network requests.
705        viaduct_reqwest::use_reqwest_backend();
706        const SCOPES: &[&str] = &["https://identity.mozilla.com/apps/oldsync"];
707        let config = Config::new(
708            "https://accounts.firefox.com",
709            "12345678",
710            "urn:ietf:wg:oauth:2.0:oob:oauth-redirect-webchannel",
711        );
712        let mut fxa = FirefoxAccount::with_config(config);
713        let url = fxa
714            .begin_oauth_flow(SCOPES, "test_webchannel_context_url")
715            .unwrap();
716        let url = Url::parse(&url).unwrap();
717        let query_params: HashMap<_, _> = url.query_pairs().into_owned().collect();
718        let context = &query_params["context"];
719        assert_eq!(context, "oauth_webchannel_v1");
720        assert_eq!(query_params.get("redirect_uri"), None);
721    }
722
723    #[test]
724    fn test_webchannel_pairing_context_url() {
725        nss::ensure_initialized();
726        const SCOPES: &[&str] = &["https://identity.mozilla.com/apps/oldsync"];
727        const PAIRING_URL: &str = "https://accounts.firefox.com/pair#channel_id=658db7fe98b249a5897b884f98fb31b7&channel_key=1hIDzTj5oY2HDeSg_jA2DhcOcAn5Uqq0cAYlZRNUIo4";
728
729        let config = Config::new(
730            "https://accounts.firefox.com",
731            "12345678",
732            "urn:ietf:wg:oauth:2.0:oob:oauth-redirect-webchannel",
733        );
734        let mut fxa = FirefoxAccount::with_config(config);
735        let url = fxa
736            .begin_pairing_flow(PAIRING_URL, SCOPES, "test_webchannel_pairing_context_url")
737            .unwrap();
738        let url = Url::parse(&url).unwrap();
739        let query_params: HashMap<_, _> = url.query_pairs().into_owned().collect();
740        let context = &query_params["context"];
741        assert_eq!(context, "oauth_webchannel_v1");
742        assert_eq!(query_params.get("redirect_uri"), None);
743    }
744
745    #[test]
746    fn test_pairing_flow_url() {
747        nss::ensure_initialized();
748        const SCOPES: &[&str] = &["https://identity.mozilla.com/apps/oldsync"];
749        const PAIRING_URL: &str = "https://accounts.firefox.com/pair#channel_id=658db7fe98b249a5897b884f98fb31b7&channel_key=1hIDzTj5oY2HDeSg_jA2DhcOcAn5Uqq0cAYlZRNUIo4";
750        const EXPECTED_URL: &str = "https://accounts.firefox.com/pair/supp?client_id=12345678&redirect_uri=https%3A%2F%2Ffoo.bar&scope=https%3A%2F%2Fidentity.mozilla.com%2Fapps%2Foldsync&state=SmbAA_9EA5v1R2bgIPeWWw&code_challenge_method=S256&code_challenge=ZgHLPPJ8XYbXpo7VIb7wFw0yXlTa6MUOVfGiADt0JSM&access_type=offline&keys_jwk=eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6Ing5LUltQjJveDM0LTV6c1VmbW5sNEp0Ti14elV2eFZlZXJHTFRXRV9BT0kiLCJ5IjoiNXBKbTB3WGQ4YXdHcm0zREl4T1pWMl9qdl9tZEx1TWlMb1RkZ1RucWJDZyJ9#channel_id=658db7fe98b249a5897b884f98fb31b7&channel_key=1hIDzTj5oY2HDeSg_jA2DhcOcAn5Uqq0cAYlZRNUIo4";
751
752        let config = Config::new(
753            "https://accounts.firefox.com",
754            "12345678",
755            "https://foo.bar",
756        );
757
758        let mut fxa = FirefoxAccount::with_config(config);
759        let url = fxa
760            .begin_pairing_flow(PAIRING_URL, SCOPES, "test_pairing_flow_url")
761            .unwrap();
762        let flow_url = Url::parse(&url).unwrap();
763        let expected_parsed_url = Url::parse(EXPECTED_URL).unwrap();
764
765        assert_eq!(flow_url.host_str(), Some("accounts.firefox.com"));
766        assert_eq!(flow_url.path(), "/pair/supp");
767        assert_eq!(flow_url.fragment(), expected_parsed_url.fragment());
768
769        let mut pairs = flow_url.query_pairs();
770        assert_eq!(pairs.count(), 9);
771        assert_eq!(
772            pairs.next(),
773            Some((
774                Cow::Borrowed("entrypoint"),
775                Cow::Borrowed("test_pairing_flow_url")
776            ))
777        );
778        assert_eq!(
779            pairs.next(),
780            Some((Cow::Borrowed("client_id"), Cow::Borrowed("12345678")))
781        );
782        assert_eq!(
783            pairs.next(),
784            Some((
785                Cow::Borrowed("scope"),
786                Cow::Borrowed("https://identity.mozilla.com/apps/oldsync")
787            ))
788        );
789
790        let state_param = pairs.next().unwrap();
791        assert_eq!(state_param.0, Cow::Borrowed("state"));
792        assert_eq!(state_param.1.len(), 22);
793        assert_eq!(
794            pairs.next(),
795            Some((
796                Cow::Borrowed("code_challenge_method"),
797                Cow::Borrowed("S256")
798            ))
799        );
800        let code_challenge_param = pairs.next().unwrap();
801        assert_eq!(code_challenge_param.0, Cow::Borrowed("code_challenge"));
802        assert_eq!(code_challenge_param.1.len(), 43);
803        assert_eq!(
804            pairs.next(),
805            Some((Cow::Borrowed("access_type"), Cow::Borrowed("offline")))
806        );
807        let keys_jwk = pairs.next().unwrap();
808        assert_eq!(keys_jwk.0, Cow::Borrowed("keys_jwk"));
809        assert_eq!(keys_jwk.1.len(), 168);
810
811        assert_eq!(
812            pairs.next(),
813            Some((
814                Cow::Borrowed("redirect_uri"),
815                Cow::Borrowed("https://foo.bar")
816            ))
817        );
818    }
819
820    #[test]
821    fn test_pairing_flow_origin_mismatch() {
822        nss::ensure_initialized();
823        static PAIRING_URL: &str = "https://bad.origin.com/pair#channel_id=foo&channel_key=bar";
824        let config = Config::stable_dev("12345678", "https://foo.bar");
825        let mut fxa = FirefoxAccount::with_config(config);
826        let url = fxa.begin_pairing_flow(
827            PAIRING_URL,
828            &["https://identity.mozilla.com/apps/oldsync"],
829            "test_pairiong_flow_origin_mismatch",
830        );
831
832        assert!(url.is_err());
833
834        match url {
835            Ok(_) => {
836                panic!("should have error");
837            }
838            Err(err) => match err {
839                Error::OriginMismatch { .. } => {}
840                _ => panic!("error not OriginMismatch"),
841            },
842        }
843    }
844
845    #[test]
846    fn test_check_authorization_status() {
847        nss::ensure_initialized();
848        let config = Config::stable_dev("12345678", "https://foo.bar");
849        let mut fxa = FirefoxAccount::with_config(config);
850
851        let refresh_token_scopes = std::collections::HashSet::new();
852        fxa.state.force_refresh_token(RefreshToken {
853            token: "refresh_token".to_owned(),
854            scopes: refresh_token_scopes,
855        });
856
857        let mut client = MockFxAClient::new();
858        client
859            .expect_check_refresh_token_status()
860            .with(always(), eq("refresh_token"))
861            .times(1)
862            .returning(|_, _| Ok(IntrospectResponse { active: true }));
863        fxa.set_client(Arc::new(client));
864
865        let auth_status = fxa.check_authorization_status().unwrap();
866        assert!(auth_status.active);
867    }
868
869    #[test]
870    fn test_check_authorization_status_circuit_breaker() {
871        nss::ensure_initialized();
872        let config = Config::stable_dev("12345678", "https://foo.bar");
873        let mut fxa = FirefoxAccount::with_config(config);
874
875        let refresh_token_scopes = std::collections::HashSet::new();
876        fxa.state.force_refresh_token(RefreshToken {
877            token: "refresh_token".to_owned(),
878            scopes: refresh_token_scopes,
879        });
880
881        let mut client = MockFxAClient::new();
882        // This copy-pasta (equivalent to `.returns(..).times(5)`) is there
883        // because `Error` is not cloneable :/
884        client
885            .expect_check_refresh_token_status()
886            .with(always(), eq("refresh_token"))
887            .returning(|_, _| Ok(IntrospectResponse { active: true }));
888        client
889            .expect_check_refresh_token_status()
890            .with(always(), eq("refresh_token"))
891            .returning(|_, _| Ok(IntrospectResponse { active: true }));
892        client
893            .expect_check_refresh_token_status()
894            .with(always(), eq("refresh_token"))
895            .returning(|_, _| Ok(IntrospectResponse { active: true }));
896        client
897            .expect_check_refresh_token_status()
898            .with(always(), eq("refresh_token"))
899            .returning(|_, _| Ok(IntrospectResponse { active: true }));
900        client
901            .expect_check_refresh_token_status()
902            .with(always(), eq("refresh_token"))
903            .returning(|_, _| Ok(IntrospectResponse { active: true }));
904        //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()
905        fxa.set_client(Arc::new(client));
906
907        for _ in 0..5 {
908            assert!(fxa.check_authorization_status().is_ok());
909        }
910        match fxa.check_authorization_status() {
911            Ok(_) => unreachable!("should not happen"),
912            Err(err) => assert!(matches!(err, Error::AuthCircuitBreakerError)),
913        }
914    }
915
916    use crate::internal::scopes::{self, OLD_SYNC};
917
918    #[test]
919    fn test_auth_code_pair_valid_not_allowed_scope() {
920        nss::ensure_initialized();
921        let config = Config::stable_dev("12345678", "https://foo.bar");
922        let mut fxa = FirefoxAccount::with_config(config);
923        fxa.set_session_token("session");
924        let mut client = MockFxAClient::new();
925        let not_allowed_scope = "https://identity.mozilla.com/apps/lockbox";
926        let expected_scopes = scopes::OLD_SYNC
927            .chars()
928            .chain(std::iter::once(' '))
929            .chain(not_allowed_scope.chars())
930            .collect::<String>();
931        client
932            .expect_get_scoped_key_data()
933            .with(always(), eq("session"), eq("12345678"), eq(expected_scopes))
934            .times(1)
935            .returning(|_, _, _, _| {
936                Err(Error::RemoteError {
937                    code: 400,
938                    errno: 163,
939                    error: "Invalid Scopes".to_string(),
940                    message: "Not allowed to request scopes".to_string(),
941                    info: "fyi, there was a server error".to_string(),
942                })
943            });
944        fxa.set_client(Arc::new(client));
945        let auth_params = AuthorizationParameters {
946            client_id: "12345678".to_string(),
947            scope: vec![scopes::OLD_SYNC.to_string(), not_allowed_scope.to_string()],
948            state: "somestate".to_string(),
949            access_type: "offline".to_string(),
950            code_challenge: None,
951            code_challenge_method: None,
952            keys_jwk: None,
953        };
954        let res = fxa.authorize_code_using_session_token(auth_params);
955        assert!(res.is_err());
956        let err = res.unwrap_err();
957        if let Error::RemoteError {
958            code,
959            errno,
960            error: _,
961            message: _,
962            info: _,
963        } = err
964        {
965            assert_eq!(code, 400);
966            assert_eq!(errno, 163); // Requested scopes not allowed
967        } else {
968            panic!("Should return an error from the server specifying that the requested scopes are not allowed");
969        }
970    }
971
972    #[test]
973    fn test_auth_code_pair_invalid_scope_not_allowed() {
974        nss::ensure_initialized();
975        let config = Config::stable_dev("12345678", "https://foo.bar");
976        let mut fxa = FirefoxAccount::with_config(config);
977        fxa.set_session_token("session");
978        let mut client = MockFxAClient::new();
979        let invalid_scope = "IamAnInvalidScope";
980        let expected_scopes = scopes::OLD_SYNC
981            .chars()
982            .chain(std::iter::once(' '))
983            .chain(invalid_scope.chars())
984            .collect::<String>();
985        client
986            .expect_get_scoped_key_data()
987            .with(always(), eq("session"), eq("12345678"), eq(expected_scopes))
988            .times(1)
989            .returning(|_, _, _, _| {
990                let mut server_ret = HashMap::new();
991                server_ret.insert(
992                    scopes::OLD_SYNC.to_string(),
993                    ScopedKeyDataResponse {
994                        key_rotation_secret: "IamASecret".to_string(),
995                        key_rotation_timestamp: 100,
996                        identifier: "".to_string(),
997                    },
998                );
999                Ok(server_ret)
1000            });
1001        fxa.set_client(Arc::new(client));
1002
1003        let auth_params = AuthorizationParameters {
1004            client_id: "12345678".to_string(),
1005            scope: vec![scopes::OLD_SYNC.to_string(), invalid_scope.to_string()],
1006            state: "somestate".to_string(),
1007            access_type: "offline".to_string(),
1008            code_challenge: None,
1009            code_challenge_method: None,
1010            keys_jwk: None,
1011        };
1012        let res = fxa.authorize_code_using_session_token(auth_params);
1013        assert!(res.is_err());
1014        let err = res.unwrap_err();
1015        if let Error::ScopeNotAllowed(client_id, scope) = err {
1016            assert_eq!(client_id, "12345678");
1017            assert_eq!(scope, "IamAnInvalidScope");
1018        } else {
1019            panic!("Should return an error that specifies the scope that is not allowed");
1020        }
1021    }
1022
1023    #[test]
1024    fn test_auth_code_pair_scope_not_in_state() {
1025        nss::ensure_initialized();
1026        let config = Config::stable_dev("12345678", "https://foo.bar");
1027        let mut fxa = FirefoxAccount::with_config(config);
1028        fxa.set_session_token("session");
1029        let mut client = MockFxAClient::new();
1030        client
1031            .expect_get_scoped_key_data()
1032            .with(
1033                always(),
1034                eq("session"),
1035                eq("12345678"),
1036                eq(scopes::OLD_SYNC),
1037            )
1038            .times(1)
1039            .returning(|_, _, _, _| {
1040                let mut server_ret = HashMap::new();
1041                server_ret.insert(
1042                    scopes::OLD_SYNC.to_string(),
1043                    ScopedKeyDataResponse {
1044                        key_rotation_secret: "IamASecret".to_string(),
1045                        key_rotation_timestamp: 100,
1046                        identifier: "".to_string(),
1047                    },
1048                );
1049                Ok(server_ret)
1050            });
1051        fxa.set_client(Arc::new(client));
1052        let auth_params = AuthorizationParameters {
1053            client_id: "12345678".to_string(),
1054            scope: vec![scopes::OLD_SYNC.to_string()],
1055            state: "somestate".to_string(),
1056            access_type: "offline".to_string(),
1057            code_challenge: None,
1058            code_challenge_method: None,
1059            keys_jwk: Some("IAmAVerySecretKeysJWkInBase64".to_string()),
1060        };
1061        let res = fxa.authorize_code_using_session_token(auth_params);
1062        assert!(res.is_err());
1063        let err = res.unwrap_err();
1064        if let Error::NoScopedKey(scope) = err {
1065            assert_eq!(scope, scopes::OLD_SYNC.to_string());
1066        } else {
1067            panic!("Should return an error that specifies the scope that is not in the state");
1068        }
1069    }
1070
1071    #[test]
1072    fn test_set_user_data_sets_session_token() {
1073        nss::ensure_initialized();
1074        let config = Config::stable_dev("12345678", "https://foo.bar");
1075        let mut fxa = FirefoxAccount::with_config(config);
1076        let user_data = UserData {
1077            session_token: String::from("mock_session_token"),
1078            uid: String::from("mock_uid_unused"),
1079            email: String::from("mock_email_usued"),
1080            verified: true,
1081        };
1082        fxa.set_user_data(user_data);
1083        assert_eq!(fxa.get_session_token().unwrap(), "mock_session_token");
1084    }
1085
1086    #[test]
1087    fn test_oauth_request_sent_with_session_when_available() {
1088        nss::ensure_initialized();
1089        let config = Config::new(
1090            "https://accounts.firefox.com",
1091            "12345678",
1092            "https://foo.bar",
1093        );
1094        let mut fxa = FirefoxAccount::with_config(config);
1095        let url = fxa
1096            .begin_oauth_flow(&[OLD_SYNC, "profile"], "test_entrypoint")
1097            .unwrap();
1098        let url = Url::parse(&url).unwrap();
1099        let state = url.query_pairs().find(|(name, _)| name == "state").unwrap();
1100        let user_data = UserData {
1101            session_token: String::from("mock_session_token"),
1102            uid: String::from("mock_uid_unused"),
1103            email: String::from("mock_email_usued"),
1104            verified: true,
1105        };
1106        let mut client = MockFxAClient::new();
1107
1108        client
1109            .expect_create_refresh_token_using_authorization_code()
1110            .withf(|_, session_token, code, _| {
1111                matches!(session_token, Some("mock_session_token")) && code == "mock_code"
1112            })
1113            .times(1)
1114            .returning(|_, _, _, _| {
1115                Ok(OAuthTokenResponse {
1116                    keys_jwe: None,
1117                    refresh_token: Some("refresh_token".to_string()),
1118                    session_token: None,
1119                    expires_in: 1,
1120                    scope: "profile".to_string(),
1121                    access_token: "access_token".to_string(),
1122                })
1123            });
1124        client
1125            .expect_destroy_access_token()
1126            .with(always(), always())
1127            .times(1)
1128            .returning(|_, _| Ok(()));
1129        fxa.set_client(Arc::new(client));
1130
1131        fxa.set_user_data(user_data);
1132
1133        fxa.complete_oauth_flow("mock_code", state.1.as_ref())
1134            .unwrap();
1135    }
1136}