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    /// * `use_cache` - optionally set to false to force a new token request.  The fetched
42    ///   token will still be cached for later `get_access_token` calls.
43    ///
44    /// **💾 This method may alter the persisted account state.**
45    pub fn get_access_token(&mut self, scope: &str, use_cache: bool) -> Result<AccessTokenInfo> {
46        if scope.contains(' ') {
47            return Err(Error::MultipleScopesRequested);
48        }
49        if use_cache {
50            if let Some(oauth_info) = self.state.get_cached_access_token(scope) {
51                if oauth_info.expires_at > util::now_secs() + OAUTH_MIN_TIME_LEFT {
52                    // If the cached key is missing the required sync scoped key, try to fetch it again
53                    if oauth_info.check_missing_sync_scoped_key().is_ok() {
54                        return Ok(oauth_info.clone());
55                    }
56                }
57            }
58        }
59        let resp = match self.state.refresh_token() {
60            Some(refresh_token) => {
61                if refresh_token.scopes.contains(scope) {
62                    self.client.create_access_token_using_refresh_token(
63                        self.state.config(),
64                        &refresh_token.token,
65                        None,
66                        &[scope],
67                    )?
68                } else {
69                    return Err(Error::NoCachedToken(scope.to_string()));
70                }
71            }
72            None => match self.state.session_token() {
73                Some(session_token) => self.client.create_access_token_using_session_token(
74                    self.state.config(),
75                    session_token,
76                    &[scope],
77                )?,
78                None => return Err(Error::NoCachedToken(scope.to_string())),
79            },
80        };
81        let since_epoch = SystemTime::now()
82            .duration_since(UNIX_EPOCH)
83            .map_err(|_| Error::IllegalState("Current date before Unix Epoch."))?;
84        let expires_at = since_epoch.as_secs() + resp.expires_in;
85        let token_info = AccessTokenInfo {
86            scope: resp.scope,
87            token: resp.access_token,
88            key: self.state.get_scoped_key(scope).cloned(),
89            expires_at,
90        };
91        self.state
92            .add_cached_access_token(scope, token_info.clone());
93        token_info.check_missing_sync_scoped_key()?;
94        Ok(token_info)
95    }
96
97    /// Sets the user data (session token, email, uid)
98    pub fn set_user_data(&mut self, user_data: UserData) {
99        // for now, we only have use for the session token
100        // if we'd like to implement a "Signed in but not verified" state
101        // we would also consume the other parts of the user data
102        self.state.set_session_token(user_data.session_token)
103    }
104
105    /// Retrieve the current session token from state
106    pub fn get_session_token(&self) -> Result<String> {
107        match self.state.session_token() {
108            Some(session_token) => Ok(session_token.to_string()),
109            None => Err(Error::NoSessionToken),
110        }
111    }
112
113    /// Check whether user is authorized using our refresh token.
114    pub fn check_authorization_status(&mut self) -> Result<IntrospectInfo> {
115        let resp = match self.state.refresh_token() {
116            Some(refresh_token) => {
117                self.auth_circuit_breaker.check()?;
118                self.client
119                    .check_refresh_token_status(self.state.config(), &refresh_token.token)?
120            }
121            None => return Err(Error::NoRefreshToken),
122        };
123        Ok(IntrospectInfo {
124            active: resp.active,
125        })
126    }
127
128    /// Initiate a pairing flow and return a URL that should be navigated to.
129    ///
130    /// * `pairing_url` - A pairing URL obtained by scanning a QR code produced by
131    ///   the pairing authority.
132    /// * `scopes` - Space-separated list of requested scopes by the pairing supplicant.
133    /// * `entrypoint` - The entrypoint to be used for data collection
134    /// * `metrics` - Optional parameters for metrics
135    pub fn begin_pairing_flow(
136        &mut self,
137        pairing_url: &str,
138        scopes: &[&str],
139        entrypoint: &str,
140    ) -> Result<String> {
141        let mut url = self.state.config().pair_supp_url()?;
142        url.query_pairs_mut().append_pair("entrypoint", entrypoint);
143        let pairing_url = util::parse_url(pairing_url, "begin_pairing_flow")?;
144        if url.host_str() != pairing_url.host_str() {
145            let fxa_server = FxaServer::from(&url);
146            let pairing_fxa_server = FxaServer::from(&pairing_url);
147            return Err(Error::OriginMismatch(format!(
148                "fxa-server: {fxa_server}, pairing-url-fxa-server: {pairing_fxa_server}"
149            )));
150        }
151        url.set_fragment(pairing_url.fragment());
152        self.oauth_flow(url, scopes)
153    }
154
155    /// Initiate an OAuth login flow and return a URL that should be navigated to.
156    ///
157    /// * `scopes` - Space-separated list of requested scopes.
158    /// * `entrypoint` - The entrypoint to be used for metrics
159    /// * `metrics` - Optional metrics parameters
160    pub fn begin_oauth_flow(&mut self, scopes: &[&str], entrypoint: &str) -> Result<String> {
161        self.state.on_begin_oauth();
162        let mut url = if self.state.last_seen_profile().is_some() {
163            self.state.config().oauth_force_auth_url()?
164        } else {
165            self.state.config().authorization_endpoint()?
166        };
167
168        url.query_pairs_mut()
169            .append_pair("action", "email")
170            .append_pair("response_type", "code")
171            .append_pair("entrypoint", entrypoint);
172
173        if let Some(cached_profile) = self.state.last_seen_profile() {
174            url.query_pairs_mut()
175                .append_pair("email", &cached_profile.response.email);
176        }
177
178        let scopes: Vec<String> = match self.state.refresh_token() {
179            Some(refresh_token) => {
180                // Union of the already held scopes and the one requested.
181                let mut all_scopes: Vec<String> = vec![];
182                all_scopes.extend(scopes.iter().map(ToString::to_string));
183                let existing_scopes = refresh_token.scopes.clone();
184                all_scopes.extend(existing_scopes);
185                HashSet::<String>::from_iter(all_scopes)
186                    .into_iter()
187                    .collect()
188            }
189            None => scopes.iter().map(ToString::to_string).collect(),
190        };
191        let scopes: Vec<&str> = scopes.iter().map(<_>::as_ref).collect();
192        self.oauth_flow(url, &scopes)
193    }
194
195    /// Fetch an OAuth code for a particular client using a session token from the account state.
196    ///
197    /// * `auth_params` Authorization parameters  which includes:
198    ///     *  `client_id` - OAuth client id.
199    ///     *  `scope` - list of requested scopes.
200    ///     *  `state` - OAuth state.
201    ///     *  `access_type` - Type of OAuth access, can be "offline" and "online"
202    ///     *  `pkce_params` - Optional PKCE parameters for public clients (`code_challenge` and `code_challenge_method`)
203    ///     *  `keys_jwk` - Optional JWK used to encrypt scoped keys
204    pub fn authorize_code_using_session_token(
205        &self,
206        auth_params: AuthorizationParameters,
207    ) -> Result<String> {
208        let session_token = self.get_session_token()?;
209
210        // Validate request to ensure that the client is actually allowed to request
211        // the scopes they requested
212        let allowed_scopes = self.client.get_scoped_key_data(
213            self.state.config(),
214            &session_token,
215            &auth_params.client_id,
216            &auth_params.scope.join(" "),
217        )?;
218
219        if let Some(not_allowed_scope) = auth_params
220            .scope
221            .iter()
222            .find(|scope| !allowed_scopes.contains_key(*scope))
223        {
224            return Err(Error::ScopeNotAllowed(
225                auth_params.client_id.clone(),
226                not_allowed_scope.clone(),
227            ));
228        }
229
230        let keys_jwe = if let Some(keys_jwk) = auth_params.keys_jwk {
231            let mut scoped_keys = HashMap::new();
232            allowed_scopes
233                .iter()
234                .try_for_each(|(scope, _)| -> Result<()> {
235                    scoped_keys.insert(
236                        scope,
237                        self.state
238                            .get_scoped_key(scope)
239                            .ok_or_else(|| Error::NoScopedKey(scope.clone()))?,
240                    );
241                    Ok(())
242                })?;
243            let scoped_keys = serde_json::to_string(&scoped_keys)?;
244            let keys_jwk = URL_SAFE_NO_PAD.decode(keys_jwk)?;
245            let jwk = serde_json::from_slice(&keys_jwk)?;
246            Some(jwcrypto::encrypt_to_jwe(
247                scoped_keys.as_bytes(),
248                EncryptionParameters::ECDH_ES {
249                    enc: EncryptionAlgorithm::A256GCM,
250                    peer_jwk: &jwk,
251                },
252            )?)
253        } else {
254            None
255        };
256        let auth_request_params = AuthorizationRequestParameters {
257            client_id: auth_params.client_id,
258            scope: auth_params.scope.join(" "),
259            state: auth_params.state,
260            access_type: auth_params.access_type,
261            code_challenge: auth_params.code_challenge,
262            code_challenge_method: auth_params.code_challenge_method,
263            keys_jwe,
264        };
265
266        let resp = self.client.create_authorization_code_using_session_token(
267            self.state.config(),
268            &session_token,
269            auth_request_params,
270        )?;
271
272        Ok(resp.code)
273    }
274
275    fn oauth_flow(&mut self, mut url: Url, scopes: &[&str]) -> Result<String> {
276        self.clear_access_token_cache();
277        let state = util::random_base64_url_string(16)?;
278        let code_verifier = util::random_base64_url_string(43)?;
279        let code_challenge = digest::digest(&digest::SHA256, code_verifier.as_bytes())?;
280        let code_challenge = URL_SAFE_NO_PAD.encode(code_challenge);
281        let scoped_keys_flow = ScopedKeysFlow::with_random_key()?;
282        let jwk = scoped_keys_flow.get_public_key_jwk()?;
283        let jwk_json = serde_json::to_string(&jwk)?;
284        let keys_jwk = URL_SAFE_NO_PAD.encode(jwk_json);
285        url.query_pairs_mut()
286            .append_pair("client_id", &self.state.config().client_id)
287            .append_pair("scope", &scopes.join(" "))
288            .append_pair("state", &state)
289            .append_pair("code_challenge_method", "S256")
290            .append_pair("code_challenge", &code_challenge)
291            .append_pair("access_type", "offline")
292            .append_pair("keys_jwk", &keys_jwk);
293
294        if self.state.config().redirect_uri == OAUTH_WEBCHANNEL_REDIRECT {
295            url.query_pairs_mut()
296                .append_pair("context", "oauth_webchannel_v1");
297        } else {
298            url.query_pairs_mut()
299                .append_pair("redirect_uri", &self.state.config().redirect_uri);
300        }
301
302        self.state.begin_oauth_flow(
303            state,
304            OAuthFlow {
305                scoped_keys_flow: Some(scoped_keys_flow),
306                code_verifier,
307            },
308        );
309        Ok(url.to_string())
310    }
311
312    /// Complete an OAuth flow initiated in `begin_oauth_flow` or `begin_pairing_flow`.
313    /// The `code` and `state` parameters can be obtained by parsing out the
314    /// redirect URL after a successful login.
315    ///
316    /// **💾 This method alters the persisted account state.**
317    pub fn complete_oauth_flow(&mut self, code: &str, state: &str) -> Result<()> {
318        self.clear_access_token_cache();
319        let oauth_flow = match self.state.pop_oauth_flow(state) {
320            Some(oauth_flow) => oauth_flow,
321            None => return Err(Error::UnknownOAuthState),
322        };
323        let resp = self.client.create_refresh_token_using_authorization_code(
324            self.state.config(),
325            self.state.session_token(),
326            code,
327            &oauth_flow.code_verifier,
328        )?;
329        self.handle_oauth_response(resp, oauth_flow.scoped_keys_flow)
330    }
331
332    /// Cancel any in-progress oauth flows
333    pub fn cancel_existing_oauth_flows(&mut self) {
334        self.state.clear_oauth_flows();
335    }
336
337    pub(crate) fn handle_oauth_response(
338        &mut self,
339        resp: OAuthTokenResponse,
340        scoped_keys_flow: Option<ScopedKeysFlow>,
341    ) -> Result<()> {
342        let sync_scope_granted = resp.scope.split(' ').any(|s| s == scopes::OLD_SYNC);
343        let scoped_keys = match resp.keys_jwe {
344            Some(ref jwe) => {
345                let scoped_keys_flow = scoped_keys_flow.ok_or(Error::ApiClientError(
346                    "Got a JWE but have no JWK to decrypt it.",
347                ))?;
348                let decrypted_keys = scoped_keys_flow.decrypt_keys_jwe(jwe)?;
349                let scoped_keys: serde_json::Map<String, serde_json::Value> =
350                    serde_json::from_str(&decrypted_keys)?;
351                if sync_scope_granted && !scoped_keys.contains_key(scopes::OLD_SYNC) {
352                    error_support::report_error!(
353                        "fxaclient-scoped-key",
354                        "Sync scope granted, but no sync scoped key (scope granted: {}, key scopes: {})",
355                        resp.scope,
356                        scoped_keys.keys().map(|s| s.as_ref()).collect::<Vec<&str>>().join(", ")
357                    );
358                }
359                scoped_keys
360                    .into_iter()
361                    .map(|(scope, key)| Ok((scope, serde_json::from_value(key)?)))
362                    .collect::<Result<Vec<_>>>()?
363            }
364            None => {
365                if sync_scope_granted {
366                    error_support::report_error!(
367                        "fxaclient-scoped-key",
368                        "Sync scope granted, but keys_jwe is None"
369                    );
370                }
371                vec![]
372            }
373        };
374
375        // We are only interested in the refresh token at this time because we
376        // don't want to return an over-scoped access token.
377        // Let's be good citizens and destroy this access token.
378        if let Err(err) = self
379            .client
380            .destroy_access_token(self.state.config(), &resp.access_token)
381        {
382            warn!("Access token destruction failure: {:?}", err);
383        }
384        let old_refresh_token = self.state.refresh_token().cloned();
385        let new_refresh_token = resp
386            .refresh_token
387            .ok_or(Error::ApiClientError("No refresh token in response"))?;
388        // Destroying a refresh token also destroys its associated device,
389        // grab the device information for replication later.
390        let old_device_info = match old_refresh_token {
391            Some(_) => match self.get_current_device() {
392                Ok(maybe_device) => maybe_device,
393                Err(err) => {
394                    warn!("Error while getting previous device information: {:?}", err);
395                    None
396                }
397            },
398            None => None,
399        };
400        // In order to keep 1 and only 1 refresh token alive per client instance,
401        // we also destroy the existing refresh token.
402        if let Some(ref refresh_token) = old_refresh_token {
403            if let Err(err) = self
404                .client
405                .destroy_refresh_token(self.state.config(), &refresh_token.token)
406            {
407                warn!("Refresh token destruction failure: {:?}", err);
408            }
409        }
410        if let Some(ref device_info) = old_device_info {
411            if let Err(err) = self.replace_device(
412                &device_info.display_name,
413                &device_info.device_type,
414                &device_info.push_subscription,
415                &device_info.available_commands,
416            ) {
417                warn!("Device information restoration failed: {:?}", err);
418            }
419        }
420        self.state.complete_oauth_flow(
421            scoped_keys,
422            RefreshToken {
423                token: new_refresh_token,
424                scopes: resp.scope.split(' ').map(ToString::to_string).collect(),
425            },
426            resp.session_token,
427        );
428        Ok(())
429    }
430
431    /// Typically called during a password change flow.
432    /// Invalidates all tokens and fetches a new refresh token.
433    /// Because the old refresh token is not valid anymore, we can't do like `handle_oauth_response`
434    /// and re-create the device, so it is the responsibility of the caller to do so after we're
435    /// done.
436    ///
437    /// **💾 This method alters the persisted account state.**
438    pub fn handle_session_token_change(&mut self, session_token: &str) -> Result<()> {
439        let old_refresh_token = self.state.refresh_token().ok_or(Error::NoRefreshToken)?;
440        let scopes: Vec<&str> = old_refresh_token.scopes.iter().map(AsRef::as_ref).collect();
441        let resp = self.client.create_refresh_token_using_session_token(
442            self.state.config(),
443            session_token,
444            &scopes,
445        )?;
446        let new_refresh_token = resp
447            .refresh_token
448            .ok_or(Error::ApiClientError("No refresh token in response"))?;
449        self.state.update_tokens(
450            session_token.to_owned(),
451            RefreshToken {
452                token: new_refresh_token,
453                scopes: resp.scope.split(' ').map(ToString::to_string).collect(),
454            },
455        );
456        self.clear_devices_and_attached_clients_cache();
457        Ok(())
458    }
459
460    /// **💾 This method may alter the persisted account state.**
461    pub fn clear_access_token_cache(&mut self) {
462        self.state.clear_access_token_cache();
463    }
464}
465
466const AUTH_CIRCUIT_BREAKER_CAPACITY: u8 = 5;
467const AUTH_CIRCUIT_BREAKER_RENEWAL_RATE: f32 = 3.0 / 60.0 / 1000.0; // 3 tokens every minute.
468
469#[derive(Clone, Copy)]
470pub(crate) struct AuthCircuitBreaker {
471    rate_limiter: RateLimiter,
472}
473
474impl Default for AuthCircuitBreaker {
475    fn default() -> Self {
476        AuthCircuitBreaker {
477            rate_limiter: RateLimiter::new(
478                AUTH_CIRCUIT_BREAKER_CAPACITY,
479                AUTH_CIRCUIT_BREAKER_RENEWAL_RATE,
480            ),
481        }
482    }
483}
484
485impl AuthCircuitBreaker {
486    pub(crate) fn check(&mut self) -> Result<()> {
487        if !self.rate_limiter.check() {
488            return Err(Error::AuthCircuitBreakerError);
489        }
490        Ok(())
491    }
492}
493
494impl TryFrom<Url> for AuthorizationParameters {
495    type Error = Error;
496
497    fn try_from(url: Url) -> Result<Self> {
498        let query_map: HashMap<String, String> = url.query_pairs().into_owned().collect();
499        let scope = query_map
500            .get("scope")
501            .cloned()
502            .ok_or(Error::MissingUrlParameter("scope"))?;
503        let client_id = query_map
504            .get("client_id")
505            .cloned()
506            .ok_or(Error::MissingUrlParameter("client_id"))?;
507        let state = query_map
508            .get("state")
509            .cloned()
510            .ok_or(Error::MissingUrlParameter("state"))?;
511        let access_type = query_map
512            .get("access_type")
513            .cloned()
514            .ok_or(Error::MissingUrlParameter("access_type"))?;
515        let code_challenge = query_map.get("code_challenge").cloned();
516        let code_challenge_method = query_map.get("code_challenge_method").cloned();
517        let keys_jwk = query_map.get("keys_jwk").cloned();
518        Ok(Self {
519            client_id,
520            scope: scope.split_whitespace().map(|s| s.to_string()).collect(),
521            state,
522            access_type,
523            code_challenge,
524            code_challenge_method,
525            keys_jwk,
526        })
527    }
528}
529
530#[derive(Clone, Serialize, Deserialize)]
531pub struct RefreshToken {
532    pub token: String,
533    pub scopes: HashSet<String>,
534}
535
536impl std::fmt::Debug for RefreshToken {
537    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
538        f.debug_struct("RefreshToken")
539            .field("scopes", &self.scopes)
540            .finish()
541    }
542}
543
544pub struct OAuthFlow {
545    pub scoped_keys_flow: Option<ScopedKeysFlow>,
546    pub code_verifier: String,
547}
548
549#[derive(Clone, Serialize, Deserialize)]
550pub struct AccessTokenInfo {
551    pub scope: String,
552    pub token: String,
553    pub key: Option<ScopedKey>,
554    pub expires_at: u64, // seconds since epoch
555}
556
557impl AccessTokenInfo {
558    pub fn check_missing_sync_scoped_key(&self) -> Result<()> {
559        if self.scope == scopes::OLD_SYNC && self.key.is_none() {
560            Err(Error::SyncScopedKeyMissingInServerResponse)
561        } else {
562            Ok(())
563        }
564    }
565}
566
567impl TryFrom<AccessTokenInfo> for crate::AccessTokenInfo {
568    type Error = Error;
569    fn try_from(info: AccessTokenInfo) -> Result<Self> {
570        Ok(crate::AccessTokenInfo {
571            scope: info.scope,
572            token: info.token,
573            key: info.key,
574            expires_at: info.expires_at.try_into()?,
575        })
576    }
577}
578
579impl std::fmt::Debug for AccessTokenInfo {
580    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
581        f.debug_struct("AccessTokenInfo")
582            .field("scope", &self.scope)
583            .field("key", &self.key)
584            .field("expires_at", &self.expires_at)
585            .finish()
586    }
587}
588
589impl From<IntrospectInfo> for crate::AuthorizationInfo {
590    fn from(r: IntrospectInfo) -> Self {
591        crate::AuthorizationInfo { active: r.active }
592    }
593}
594
595#[cfg(test)]
596mod tests {
597    use super::super::{http_client::*, Config};
598    use super::*;
599    use mockall::predicate::always;
600    use mockall::predicate::eq;
601    use std::borrow::Cow;
602    use std::collections::HashMap;
603    use std::sync::Arc;
604
605    impl FirefoxAccount {
606        pub fn add_cached_token(&mut self, scope: &str, token_info: AccessTokenInfo) {
607            self.state.add_cached_access_token(scope, token_info);
608        }
609
610        pub fn set_session_token(&mut self, session_token: &str) {
611            self.state.set_session_token(session_token.to_owned());
612        }
613    }
614
615    #[test]
616    fn test_oauth_flow_url() {
617        nss::ensure_initialized();
618        let config = Config::new_with_mock_well_known_fxa_client_configuration(
619            "https://mock-fxa.example.com",
620            "12345678",
621            "https://foo.bar",
622        );
623        let mut fxa = FirefoxAccount::with_config(config);
624        let url = fxa
625            .begin_oauth_flow(&["profile"], "test_oauth_flow_url")
626            .unwrap();
627        let flow_url = Url::parse(&url).unwrap();
628
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        const SCOPES: &[&str] = &["https://identity.mozilla.com/apps/oldsync"];
710        let config = Config::new_with_mock_well_known_fxa_client_configuration(
711            "https://mock-fxa.example.com",
712            "12345678",
713            "urn:ietf:wg:oauth:2.0:oob:oauth-redirect-webchannel",
714        );
715        let mut fxa = FirefoxAccount::with_config(config);
716        let url = fxa
717            .begin_oauth_flow(SCOPES, "test_webchannel_context_url")
718            .unwrap();
719        let url = Url::parse(&url).unwrap();
720        let query_params: HashMap<_, _> = url.query_pairs().into_owned().collect();
721        let context = &query_params["context"];
722        assert_eq!(context, "oauth_webchannel_v1");
723        assert_eq!(query_params.get("redirect_uri"), None);
724    }
725
726    #[test]
727    fn test_webchannel_pairing_context_url() {
728        nss::ensure_initialized();
729        const SCOPES: &[&str] = &["https://identity.mozilla.com/apps/oldsync"];
730        const PAIRING_URL: &str = "https://accounts.firefox.com/pair#channel_id=658db7fe98b249a5897b884f98fb31b7&channel_key=1hIDzTj5oY2HDeSg_jA2DhcOcAn5Uqq0cAYlZRNUIo4";
731
732        let config = Config::new(
733            "https://accounts.firefox.com",
734            "12345678",
735            "urn:ietf:wg:oauth:2.0:oob:oauth-redirect-webchannel",
736        );
737        let mut fxa = FirefoxAccount::with_config(config);
738        let url = fxa
739            .begin_pairing_flow(PAIRING_URL, SCOPES, "test_webchannel_pairing_context_url")
740            .unwrap();
741        let url = Url::parse(&url).unwrap();
742        let query_params: HashMap<_, _> = url.query_pairs().into_owned().collect();
743        let context = &query_params["context"];
744        assert_eq!(context, "oauth_webchannel_v1");
745        assert_eq!(query_params.get("redirect_uri"), None);
746    }
747
748    #[test]
749    fn test_pairing_flow_url() {
750        nss::ensure_initialized();
751        const SCOPES: &[&str] = &["https://identity.mozilla.com/apps/oldsync"];
752        const PAIRING_URL: &str = "https://accounts.firefox.com/pair#channel_id=658db7fe98b249a5897b884f98fb31b7&channel_key=1hIDzTj5oY2HDeSg_jA2DhcOcAn5Uqq0cAYlZRNUIo4";
753        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";
754
755        let config = Config::new(
756            "https://accounts.firefox.com",
757            "12345678",
758            "https://foo.bar",
759        );
760
761        let mut fxa = FirefoxAccount::with_config(config);
762        let url = fxa
763            .begin_pairing_flow(PAIRING_URL, SCOPES, "test_pairing_flow_url")
764            .unwrap();
765        let flow_url = Url::parse(&url).unwrap();
766        let expected_parsed_url = Url::parse(EXPECTED_URL).unwrap();
767
768        assert_eq!(flow_url.host_str(), Some("accounts.firefox.com"));
769        assert_eq!(flow_url.path(), "/pair/supp");
770        assert_eq!(flow_url.fragment(), expected_parsed_url.fragment());
771
772        let mut pairs = flow_url.query_pairs();
773        assert_eq!(pairs.count(), 9);
774        assert_eq!(
775            pairs.next(),
776            Some((
777                Cow::Borrowed("entrypoint"),
778                Cow::Borrowed("test_pairing_flow_url")
779            ))
780        );
781        assert_eq!(
782            pairs.next(),
783            Some((Cow::Borrowed("client_id"), Cow::Borrowed("12345678")))
784        );
785        assert_eq!(
786            pairs.next(),
787            Some((
788                Cow::Borrowed("scope"),
789                Cow::Borrowed("https://identity.mozilla.com/apps/oldsync")
790            ))
791        );
792
793        let state_param = pairs.next().unwrap();
794        assert_eq!(state_param.0, Cow::Borrowed("state"));
795        assert_eq!(state_param.1.len(), 22);
796        assert_eq!(
797            pairs.next(),
798            Some((
799                Cow::Borrowed("code_challenge_method"),
800                Cow::Borrowed("S256")
801            ))
802        );
803        let code_challenge_param = pairs.next().unwrap();
804        assert_eq!(code_challenge_param.0, Cow::Borrowed("code_challenge"));
805        assert_eq!(code_challenge_param.1.len(), 43);
806        assert_eq!(
807            pairs.next(),
808            Some((Cow::Borrowed("access_type"), Cow::Borrowed("offline")))
809        );
810        let keys_jwk = pairs.next().unwrap();
811        assert_eq!(keys_jwk.0, Cow::Borrowed("keys_jwk"));
812        assert_eq!(keys_jwk.1.len(), 168);
813
814        assert_eq!(
815            pairs.next(),
816            Some((
817                Cow::Borrowed("redirect_uri"),
818                Cow::Borrowed("https://foo.bar")
819            ))
820        );
821    }
822
823    #[test]
824    fn test_pairing_flow_origin_mismatch() {
825        nss::ensure_initialized();
826        static PAIRING_URL: &str = "https://bad.origin.com/pair#channel_id=foo&channel_key=bar";
827        let config = Config::stable_dev("12345678", "https://foo.bar");
828        let mut fxa = FirefoxAccount::with_config(config);
829        let url = fxa.begin_pairing_flow(
830            PAIRING_URL,
831            &["https://identity.mozilla.com/apps/oldsync"],
832            "test_pairiong_flow_origin_mismatch",
833        );
834
835        assert!(url.is_err());
836
837        match url {
838            Ok(_) => {
839                panic!("should have error");
840            }
841            Err(err) => match err {
842                Error::OriginMismatch { .. } => {}
843                _ => panic!("error not OriginMismatch"),
844            },
845        }
846    }
847
848    #[test]
849    fn test_check_authorization_status() {
850        nss::ensure_initialized();
851        let config = Config::stable_dev("12345678", "https://foo.bar");
852        let mut fxa = FirefoxAccount::with_config(config);
853
854        let refresh_token_scopes = std::collections::HashSet::new();
855        fxa.state.force_refresh_token(RefreshToken {
856            token: "refresh_token".to_owned(),
857            scopes: refresh_token_scopes,
858        });
859
860        let mut client = MockFxAClient::new();
861        client
862            .expect_check_refresh_token_status()
863            .with(always(), eq("refresh_token"))
864            .times(1)
865            .returning(|_, _| Ok(IntrospectResponse { active: true }));
866        fxa.set_client(Arc::new(client));
867
868        let auth_status = fxa.check_authorization_status().unwrap();
869        assert!(auth_status.active);
870    }
871
872    #[test]
873    fn test_check_authorization_status_circuit_breaker() {
874        nss::ensure_initialized();
875        let config = Config::stable_dev("12345678", "https://foo.bar");
876        let mut fxa = FirefoxAccount::with_config(config);
877
878        let refresh_token_scopes = std::collections::HashSet::new();
879        fxa.state.force_refresh_token(RefreshToken {
880            token: "refresh_token".to_owned(),
881            scopes: refresh_token_scopes,
882        });
883
884        let mut client = MockFxAClient::new();
885        // This copy-pasta (equivalent to `.returns(..).times(5)`) is there
886        // because `Error` is not cloneable :/
887        client
888            .expect_check_refresh_token_status()
889            .with(always(), eq("refresh_token"))
890            .returning(|_, _| Ok(IntrospectResponse { active: true }));
891        client
892            .expect_check_refresh_token_status()
893            .with(always(), eq("refresh_token"))
894            .returning(|_, _| Ok(IntrospectResponse { active: true }));
895        client
896            .expect_check_refresh_token_status()
897            .with(always(), eq("refresh_token"))
898            .returning(|_, _| Ok(IntrospectResponse { active: true }));
899        client
900            .expect_check_refresh_token_status()
901            .with(always(), eq("refresh_token"))
902            .returning(|_, _| Ok(IntrospectResponse { active: true }));
903        client
904            .expect_check_refresh_token_status()
905            .with(always(), eq("refresh_token"))
906            .returning(|_, _| Ok(IntrospectResponse { active: true }));
907        //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()
908        fxa.set_client(Arc::new(client));
909
910        for _ in 0..5 {
911            assert!(fxa.check_authorization_status().is_ok());
912        }
913        match fxa.check_authorization_status() {
914            Ok(_) => unreachable!("should not happen"),
915            Err(err) => assert!(matches!(err, Error::AuthCircuitBreakerError)),
916        }
917    }
918
919    use crate::internal::scopes::{self, OLD_SYNC};
920
921    #[test]
922    fn test_auth_code_pair_valid_not_allowed_scope() {
923        nss::ensure_initialized();
924        let config = Config::stable_dev("12345678", "https://foo.bar");
925        let mut fxa = FirefoxAccount::with_config(config);
926        fxa.set_session_token("session");
927        let mut client = MockFxAClient::new();
928        let not_allowed_scope = "https://identity.mozilla.com/apps/lockbox";
929        let expected_scopes = scopes::OLD_SYNC
930            .chars()
931            .chain(std::iter::once(' '))
932            .chain(not_allowed_scope.chars())
933            .collect::<String>();
934        client
935            .expect_get_scoped_key_data()
936            .with(always(), eq("session"), eq("12345678"), eq(expected_scopes))
937            .times(1)
938            .returning(|_, _, _, _| {
939                Err(Error::RemoteError {
940                    code: 400,
941                    errno: 163,
942                    error: "Invalid Scopes".to_string(),
943                    message: "Not allowed to request scopes".to_string(),
944                    info: "fyi, there was a server error".to_string(),
945                })
946            });
947        fxa.set_client(Arc::new(client));
948        let auth_params = AuthorizationParameters {
949            client_id: "12345678".to_string(),
950            scope: vec![scopes::OLD_SYNC.to_string(), not_allowed_scope.to_string()],
951            state: "somestate".to_string(),
952            access_type: "offline".to_string(),
953            code_challenge: None,
954            code_challenge_method: None,
955            keys_jwk: None,
956        };
957        let res = fxa.authorize_code_using_session_token(auth_params);
958        assert!(res.is_err());
959        let err = res.unwrap_err();
960        if let Error::RemoteError {
961            code,
962            errno,
963            error: _,
964            message: _,
965            info: _,
966        } = err
967        {
968            assert_eq!(code, 400);
969            assert_eq!(errno, 163); // Requested scopes not allowed
970        } else {
971            panic!("Should return an error from the server specifying that the requested scopes are not allowed");
972        }
973    }
974
975    #[test]
976    fn test_auth_code_pair_invalid_scope_not_allowed() {
977        nss::ensure_initialized();
978        let config = Config::stable_dev("12345678", "https://foo.bar");
979        let mut fxa = FirefoxAccount::with_config(config);
980        fxa.set_session_token("session");
981        let mut client = MockFxAClient::new();
982        let invalid_scope = "IamAnInvalidScope";
983        let expected_scopes = scopes::OLD_SYNC
984            .chars()
985            .chain(std::iter::once(' '))
986            .chain(invalid_scope.chars())
987            .collect::<String>();
988        client
989            .expect_get_scoped_key_data()
990            .with(always(), eq("session"), eq("12345678"), eq(expected_scopes))
991            .times(1)
992            .returning(|_, _, _, _| {
993                let mut server_ret = HashMap::new();
994                server_ret.insert(
995                    scopes::OLD_SYNC.to_string(),
996                    ScopedKeyDataResponse {
997                        key_rotation_secret: "IamASecret".to_string(),
998                        key_rotation_timestamp: 100,
999                        identifier: "".to_string(),
1000                    },
1001                );
1002                Ok(server_ret)
1003            });
1004        fxa.set_client(Arc::new(client));
1005
1006        let auth_params = AuthorizationParameters {
1007            client_id: "12345678".to_string(),
1008            scope: vec![scopes::OLD_SYNC.to_string(), invalid_scope.to_string()],
1009            state: "somestate".to_string(),
1010            access_type: "offline".to_string(),
1011            code_challenge: None,
1012            code_challenge_method: None,
1013            keys_jwk: None,
1014        };
1015        let res = fxa.authorize_code_using_session_token(auth_params);
1016        assert!(res.is_err());
1017        let err = res.unwrap_err();
1018        if let Error::ScopeNotAllowed(client_id, scope) = err {
1019            assert_eq!(client_id, "12345678");
1020            assert_eq!(scope, "IamAnInvalidScope");
1021        } else {
1022            panic!("Should return an error that specifies the scope that is not allowed");
1023        }
1024    }
1025
1026    #[test]
1027    fn test_auth_code_pair_scope_not_in_state() {
1028        nss::ensure_initialized();
1029        let config = Config::stable_dev("12345678", "https://foo.bar");
1030        let mut fxa = FirefoxAccount::with_config(config);
1031        fxa.set_session_token("session");
1032        let mut client = MockFxAClient::new();
1033        client
1034            .expect_get_scoped_key_data()
1035            .with(
1036                always(),
1037                eq("session"),
1038                eq("12345678"),
1039                eq(scopes::OLD_SYNC),
1040            )
1041            .times(1)
1042            .returning(|_, _, _, _| {
1043                let mut server_ret = HashMap::new();
1044                server_ret.insert(
1045                    scopes::OLD_SYNC.to_string(),
1046                    ScopedKeyDataResponse {
1047                        key_rotation_secret: "IamASecret".to_string(),
1048                        key_rotation_timestamp: 100,
1049                        identifier: "".to_string(),
1050                    },
1051                );
1052                Ok(server_ret)
1053            });
1054        fxa.set_client(Arc::new(client));
1055        let auth_params = AuthorizationParameters {
1056            client_id: "12345678".to_string(),
1057            scope: vec![scopes::OLD_SYNC.to_string()],
1058            state: "somestate".to_string(),
1059            access_type: "offline".to_string(),
1060            code_challenge: None,
1061            code_challenge_method: None,
1062            keys_jwk: Some("IAmAVerySecretKeysJWkInBase64".to_string()),
1063        };
1064        let res = fxa.authorize_code_using_session_token(auth_params);
1065        assert!(res.is_err());
1066        let err = res.unwrap_err();
1067        if let Error::NoScopedKey(scope) = err {
1068            assert_eq!(scope, scopes::OLD_SYNC.to_string());
1069        } else {
1070            panic!("Should return an error that specifies the scope that is not in the state");
1071        }
1072    }
1073
1074    #[test]
1075    fn test_set_user_data_sets_session_token() {
1076        nss::ensure_initialized();
1077        let config = Config::stable_dev("12345678", "https://foo.bar");
1078        let mut fxa = FirefoxAccount::with_config(config);
1079        let user_data = UserData {
1080            session_token: String::from("mock_session_token"),
1081            uid: String::from("mock_uid_unused"),
1082            email: String::from("mock_email_usued"),
1083            verified: true,
1084        };
1085        fxa.set_user_data(user_data);
1086        assert_eq!(fxa.get_session_token().unwrap(), "mock_session_token");
1087    }
1088
1089    #[test]
1090    fn test_oauth_request_sent_with_session_when_available() {
1091        nss::ensure_initialized();
1092        let config = Config::new_with_mock_well_known_fxa_client_configuration(
1093            "mock-fxa.example.com",
1094            "12345678",
1095            "https://foo.bar",
1096        );
1097        let mut fxa = FirefoxAccount::with_config(config);
1098        let url = fxa
1099            .begin_oauth_flow(&[OLD_SYNC, "profile"], "test_entrypoint")
1100            .unwrap();
1101        let url = Url::parse(&url).unwrap();
1102        let state = url.query_pairs().find(|(name, _)| name == "state").unwrap();
1103        let user_data = UserData {
1104            session_token: String::from("mock_session_token"),
1105            uid: String::from("mock_uid_unused"),
1106            email: String::from("mock_email_usued"),
1107            verified: true,
1108        };
1109        let mut client = MockFxAClient::new();
1110
1111        client
1112            .expect_create_refresh_token_using_authorization_code()
1113            .withf(|_, session_token, code, _| {
1114                matches!(session_token, Some("mock_session_token")) && code == "mock_code"
1115            })
1116            .times(1)
1117            .returning(|_, _, _, _| {
1118                Ok(OAuthTokenResponse {
1119                    keys_jwe: None,
1120                    refresh_token: Some("refresh_token".to_string()),
1121                    session_token: None,
1122                    expires_in: 1,
1123                    scope: "profile".to_string(),
1124                    access_token: "access_token".to_string(),
1125                })
1126            });
1127        client
1128            .expect_destroy_access_token()
1129            .with(always(), always())
1130            .times(1)
1131            .returning(|_, _| Ok(()));
1132        fxa.set_client(Arc::new(client));
1133
1134        fxa.set_user_data(user_data);
1135
1136        fxa.complete_oauth_flow("mock_code", state.1.as_ref())
1137            .unwrap();
1138    }
1139}