fxa_client/internal/
profile.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 use super::http_client::ProfileResponse as Profile;
6use super::{scopes, util, CachedResponse, FirefoxAccount};
7use crate::{Error, Result};
8
9// A cached profile response is considered fresh for `PROFILE_FRESHNESS_THRESHOLD` ms.
10const PROFILE_FRESHNESS_THRESHOLD: u64 = 120_000; // 2 minutes
11
12impl FirefoxAccount {
13    /// Fetch the profile for the user.
14    /// This method will error-out if the `profile` scope is not
15    /// authorized for the current refresh token or or if we do
16    /// not have a valid refresh token.
17    ///
18    /// * `ignore_cache` - If set to true, bypass the in-memory cache
19    ///   and fetch the entire profile data from the server.
20    ///
21    /// **💾 This method alters the persisted account state.**
22    pub fn get_profile(&mut self, ignore_cache: bool) -> Result<Profile> {
23        match self.get_profile_helper(ignore_cache) {
24            Ok(res) => Ok(res),
25            Err(e) => match e {
26                Error::RemoteError { code: 401, .. } => {
27                    crate::warn!(
28                        "Access token rejected, clearing the tokens cache and trying again."
29                    );
30                    self.clear_access_token_cache();
31                    self.clear_devices_and_attached_clients_cache();
32                    self.get_profile_helper(ignore_cache)
33                }
34                _ => Err(e),
35            },
36        }
37    }
38
39    fn get_profile_helper(&mut self, ignore_cache: bool) -> Result<Profile> {
40        let mut etag = None;
41        if let Some(cached_profile) = self.state.last_seen_profile() {
42            if !ignore_cache && util::now() < cached_profile.cached_at + PROFILE_FRESHNESS_THRESHOLD
43            {
44                return Ok(cached_profile.response.clone());
45            }
46            etag = Some(cached_profile.etag.clone());
47        }
48        let profile_access_token = self.get_access_token(scopes::PROFILE, None)?.token;
49        match self
50            .client
51            .get_profile(self.state.config(), &profile_access_token, etag)?
52        {
53            Some(response_and_etag) => {
54                if let Some(etag) = response_and_etag.etag {
55                    self.state.set_last_seen_profile(CachedResponse {
56                        response: response_and_etag.response.clone(),
57                        cached_at: util::now(),
58                        etag,
59                    });
60                }
61                Ok(response_and_etag.response)
62            }
63            None => {
64                match self.state.last_seen_profile() {
65                    Some(cached_profile) => {
66                        let response = cached_profile.response.clone();
67                        // Update `cached_at` timestamp.
68                        let new_cached_profile = CachedResponse {
69                            response: cached_profile.response.clone(),
70                            cached_at: util::now(),
71                            etag: cached_profile.etag.clone(),
72                        };
73                        self.state.set_last_seen_profile(new_cached_profile);
74                        Ok(response)
75                    }
76                    None => Err(Error::ApiClientError(
77                        "Got a 304 without having sent an eTag.",
78                    )),
79                }
80            }
81        }
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88    use crate::internal::{
89        http_client::*,
90        oauth::{AccessTokenInfo, RefreshToken},
91        Config,
92    };
93    use mockall::predicate::always;
94    use mockall::predicate::eq;
95    use std::sync::Arc;
96
97    impl FirefoxAccount {
98        pub fn add_cached_profile(&mut self, uid: &str, email: &str) {
99            self.state.set_last_seen_profile(CachedResponse {
100                response: Profile {
101                    uid: uid.into(),
102                    email: email.into(),
103                    display_name: None,
104                    avatar: "".into(),
105                    avatar_default: true,
106                },
107                cached_at: util::now(),
108                etag: "fake etag".into(),
109            });
110        }
111    }
112
113    #[test]
114    fn test_fetch_profile() {
115        let config = Config::stable_dev("12345678", "https://foo.bar");
116        let mut fxa = FirefoxAccount::with_config(config);
117
118        fxa.add_cached_token(
119            "profile",
120            AccessTokenInfo {
121                scope: "profile".to_string(),
122                token: "profiletok".to_string(),
123                key: None,
124                expires_at: u64::MAX,
125            },
126        );
127
128        let mut client = MockFxAClient::new();
129        client
130            .expect_get_profile()
131            .with(always(), eq("profiletok"), always())
132            .times(1)
133            .returning(|_, _, _| {
134                Ok(Some(ResponseAndETag {
135                    response: ProfileResponse {
136                        uid: "12345ab".to_string(),
137                        email: "foo@bar.com".to_string(),
138                        display_name: None,
139                        avatar: "https://foo.avatar".to_string(),
140                        avatar_default: true,
141                    },
142                    etag: None,
143                }))
144            });
145        fxa.set_client(Arc::new(client));
146
147        let p = fxa.get_profile(false).unwrap();
148        assert_eq!(p.email, "foo@bar.com");
149    }
150
151    #[test]
152    fn test_expired_access_token_refetch() {
153        let config = Config::stable_dev("12345678", "https://foo.bar");
154        let mut fxa = FirefoxAccount::with_config(config);
155
156        fxa.add_cached_token(
157            "profile",
158            AccessTokenInfo {
159                scope: "profile".to_string(),
160                token: "bad_access_token".to_string(),
161                key: None,
162                expires_at: u64::MAX,
163            },
164        );
165        let mut refresh_token_scopes = std::collections::HashSet::new();
166        refresh_token_scopes.insert("profile".to_owned());
167        fxa.state.force_refresh_token(RefreshToken {
168            token: "refreshtok".to_owned(),
169            scopes: refresh_token_scopes,
170        });
171
172        let mut client = MockFxAClient::new();
173        // First call to profile() we fail with 401.
174        client
175            .expect_get_profile()
176            .with(always(), eq("bad_access_token"), always())
177            .times(1)
178            .returning(|_, _, _| Err(Error::RemoteError{
179                code: 401,
180                errno: 110,
181                error: "Unauthorized".to_owned(),
182                message: "Invalid authentication token in request signature".to_owned(),
183                info: "https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md#response-format".to_owned(),
184            }));
185        // Then we'll try to get a new access token.
186        client
187            .expect_create_access_token_using_refresh_token()
188            .with(always(), eq("refreshtok"), always(), always())
189            .times(1)
190            .returning(|_, _, _, _| {
191                Ok(OAuthTokenResponse {
192                    keys_jwe: None,
193                    refresh_token: None,
194                    expires_in: 6_000_000,
195                    scope: "profile".to_owned(),
196                    access_token: "good_profile_token".to_owned(),
197                    session_token: None,
198                })
199            });
200        // Then hooray it works!
201        client
202            .expect_get_profile()
203            .with(always(), eq("good_profile_token"), always())
204            .times(1)
205            .returning(|_, _, _| {
206                Ok(Some(ResponseAndETag {
207                    response: ProfileResponse {
208                        uid: "12345ab".to_string(),
209                        email: "foo@bar.com".to_string(),
210                        display_name: None,
211                        avatar: "https://foo.avatar".to_string(),
212                        avatar_default: true,
213                    },
214                    etag: None,
215                }))
216            });
217        fxa.set_client(Arc::new(client));
218
219        let p = fxa.get_profile(false).unwrap();
220        assert_eq!(p.email, "foo@bar.com");
221    }
222}