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