diff options
Diffstat (limited to 'third_party/rust/fxa-client/src/profile.rs')
-rw-r--r-- | third_party/rust/fxa-client/src/profile.rs | 228 |
1 files changed, 228 insertions, 0 deletions
diff --git a/third_party/rust/fxa-client/src/profile.rs b/third_party/rust/fxa-client/src/profile.rs new file mode 100644 index 0000000000..d6628a2266 --- /dev/null +++ b/third_party/rust/fxa-client/src/profile.rs @@ -0,0 +1,228 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +pub use crate::http_client::ProfileResponse as Profile; +use crate::{error::*, scopes, util, CachedResponse, FirefoxAccount}; + +// A cached profile response is considered fresh for `PROFILE_FRESHNESS_THRESHOLD` ms. +const PROFILE_FRESHNESS_THRESHOLD: u64 = 120_000; // 2 minutes + +impl FirefoxAccount { + /// Fetch the profile for the user. + /// This method will error-out if the `profile` scope is not + /// authorized for the current refresh token or or if we do + /// not have a valid refresh token. + /// + /// * `ignore_cache` - If set to true, bypass the in-memory cache + /// and fetch the entire profile data from the server. + /// + /// **💾 This method alters the persisted account state.** + pub fn get_profile(&mut self, ignore_cache: bool) -> Result<Profile> { + match self.get_profile_helper(ignore_cache) { + Ok(res) => Ok(res), + Err(e) => match e.kind() { + ErrorKind::RemoteError { code: 401, .. } => { + log::warn!( + "Access token rejected, clearing the tokens cache and trying again." + ); + self.clear_access_token_cache(); + self.clear_devices_and_attached_clients_cache(); + self.get_profile_helper(ignore_cache) + } + _ => Err(e), + }, + } + } + + fn get_profile_helper(&mut self, ignore_cache: bool) -> Result<Profile> { + let mut etag = None; + if let Some(ref cached_profile) = self.state.last_seen_profile { + if !ignore_cache && util::now() < cached_profile.cached_at + PROFILE_FRESHNESS_THRESHOLD + { + return Ok(cached_profile.response.clone()); + } + etag = Some(cached_profile.etag.clone()); + } + let profile_access_token = self.get_access_token(scopes::PROFILE, None)?.token; + match self + .client + .profile(&self.state.config, &profile_access_token, etag)? + { + Some(response_and_etag) => { + if let Some(etag) = response_and_etag.etag { + self.state.last_seen_profile = Some(CachedResponse { + response: response_and_etag.response.clone(), + cached_at: util::now(), + etag, + }); + } + Ok(response_and_etag.response) + } + None => { + match self.state.last_seen_profile.take() { + Some(ref cached_profile) => { + // Update `cached_at` timestamp. + self.state.last_seen_profile.replace(CachedResponse { + response: cached_profile.response.clone(), + cached_at: util::now(), + etag: cached_profile.etag.clone(), + }); + Ok(cached_profile.response.clone()) + } + None => Err(ErrorKind::UnrecoverableServerError( + "Got a 304 without having sent an eTag.", + ) + .into()), + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + http_client::*, + oauth::{AccessTokenInfo, RefreshToken}, + Config, + }; + use std::sync::Arc; + + impl FirefoxAccount { + pub fn add_cached_profile(&mut self, uid: &str, email: &str) { + self.state.last_seen_profile = Some(CachedResponse { + response: Profile { + uid: uid.into(), + email: email.into(), + display_name: None, + avatar: "".into(), + avatar_default: true, + ecosystem_anon_id: None, + }, + cached_at: util::now(), + etag: "fake etag".into(), + }); + } + } + + #[test] + fn test_fetch_profile() { + let config = Config::stable_dev("12345678", "https://foo.bar"); + let mut fxa = FirefoxAccount::with_config(config); + + fxa.add_cached_token( + "profile", + AccessTokenInfo { + scope: "profile".to_string(), + token: "profiletok".to_string(), + key: None, + expires_at: u64::max_value(), + }, + ); + + let mut client = FxAClientMock::new(); + client + .expect_profile( + mockiato::Argument::any, + |token| token.partial_eq("profiletok"), + mockiato::Argument::any, + ) + .times(1) + .returns_once(Ok(Some(ResponseAndETag { + response: ProfileResponse { + uid: "12345ab".to_string(), + email: "foo@bar.com".to_string(), + display_name: None, + avatar: "https://foo.avatar".to_string(), + avatar_default: true, + ecosystem_anon_id: None, + }, + etag: None, + }))); + fxa.set_client(Arc::new(client)); + + let p = fxa.get_profile(false).unwrap(); + assert_eq!(p.email, "foo@bar.com"); + } + + #[test] + fn test_expired_access_token_refetch() { + let config = Config::stable_dev("12345678", "https://foo.bar"); + let mut fxa = FirefoxAccount::with_config(config); + + fxa.add_cached_token( + "profile", + AccessTokenInfo { + scope: "profile".to_string(), + token: "bad_access_token".to_string(), + key: None, + expires_at: u64::max_value(), + }, + ); + let mut refresh_token_scopes = std::collections::HashSet::new(); + refresh_token_scopes.insert("profile".to_owned()); + fxa.state.refresh_token = Some(RefreshToken { + token: "refreshtok".to_owned(), + scopes: refresh_token_scopes, + }); + + let mut client = FxAClientMock::new(); + // First call to profile() we fail with 401. + client + .expect_profile( + mockiato::Argument::any, + |token| token.partial_eq("bad_access_token"), + mockiato::Argument::any, + ) + .times(1) + .returns_once(Err(ErrorKind::RemoteError{ + code: 401, + errno: 110, + error: "Unauthorized".to_owned(), + message: "Invalid authentication token in request signature".to_owned(), + info: "https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md#response-format".to_owned(), + }.into())); + // Then we'll try to get a new access token. + client + .expect_access_token_with_refresh_token( + mockiato::Argument::any, + |token| token.partial_eq("refreshtok"), + mockiato::Argument::any, + mockiato::Argument::any, + ) + .times(1) + .returns_once(Ok(OAuthTokenResponse { + keys_jwe: None, + refresh_token: None, + expires_in: 6_000_000, + scope: "profile".to_owned(), + access_token: "good_profile_token".to_owned(), + session_token: None, + })); + // Then hooray it works! + client + .expect_profile( + mockiato::Argument::any, + |token| token.partial_eq("good_profile_token"), + mockiato::Argument::any, + ) + .times(1) + .returns_once(Ok(Some(ResponseAndETag { + response: ProfileResponse { + uid: "12345ab".to_string(), + email: "foo@bar.com".to_string(), + display_name: None, + avatar: "https://foo.avatar".to_string(), + avatar_default: true, + ecosystem_anon_id: None, + }, + etag: None, + }))); + fxa.set_client(Arc::new(client)); + + let p = fxa.get_profile(false).unwrap(); + assert_eq!(p.email, "foo@bar.com"); + } +} |