fxa_client/internal/
http_client.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
5//! Low-level API for talking to the FxA server.
6//!
7//! This module is responsible for talking to the FxA server over HTTP,
8//! serializing request bodies and deserializing response payloads into
9//! live objects that can be inspected by other parts of the code.
10
11use super::{config::Config, util};
12use crate::{Error, Result};
13use error_support::breadcrumb;
14use parking_lot::Mutex;
15use rc_crypto::{
16    digest,
17    hawk::{Credentials, Key, PayloadHasher, RequestBuilder, SHA256},
18    hkdf, hmac,
19};
20use serde_derive::{Deserialize, Serialize};
21use serde_json::json;
22use std::{
23    collections::HashMap,
24    sync::atomic::{AtomicBool, Ordering},
25    time::{Duration, Instant},
26};
27use sync15::DeviceType;
28use url::Url;
29use viaduct::{header_names, status_codes, Method, Request, Response};
30
31const HAWK_HKDF_SALT: [u8; 32] = [0b0; 32];
32const HAWK_KEY_LENGTH: usize = 32;
33const RETRY_AFTER_DEFAULT_SECONDS: u64 = 10;
34// Devices older than this many days will not appear in the devices list
35const DEVICES_FILTER_DAYS: u64 = 21;
36
37/// Trait defining the low-level API for talking to the FxA server.
38///
39/// These are all the methods and datatypes used for interacting with
40/// the FxA server. It's defined as a trait mostly to make it mockable
41/// for testing purposes.
42///
43/// The default live implementation of this trait can be found in
44/// the [`Client`] struct, and you should consult that struct for
45/// documentation on specific methods.
46///
47/// A brief note on names: you'll see that the method names on this trait
48/// try to follow a pattern of `<verb>_<noun>[_<additional info>]()`.
49/// Please try to keep to this pattern if you add methods to this trait.
50///
51/// Consistent names are helpful at the best of times, but they're
52/// particularly important here because so many of the nouns in OAuth
53/// contain embedded verbs! For example: at a glance, does a method
54/// named `refresh_token` refresh something called a "token", or does
55/// it return something called a "refresh token"? Using unambiguous
56/// verbs to start each method helps avoid confusion here.
57///
58// Due to limitations in mockall, we have to add explicit lifetimes that the Rust compiler would have been happy to infer.
59#[allow(clippy::needless_lifetimes)]
60#[cfg_attr(test, mockall::automock)]
61pub(crate) trait FxAClient {
62    fn create_refresh_token_using_authorization_code<'a>(
63        &self,
64        config: &Config,
65        session_token: Option<&'a str>,
66        code: &str,
67        code_verifier: &str,
68    ) -> Result<OAuthTokenResponse>;
69    fn create_refresh_token_using_session_token<'a>(
70        &self,
71        config: &Config,
72        session_token: &str,
73        scopes: &[&'a str],
74    ) -> Result<OAuthTokenResponse>;
75    fn check_refresh_token_status(
76        &self,
77        config: &Config,
78        refresh_token: &str,
79    ) -> Result<IntrospectResponse>;
80    fn create_access_token_using_refresh_token<'a>(
81        &self,
82        config: &Config,
83        refresh_token: &str,
84        ttl: Option<u64>,
85        scopes: &[&'a str],
86    ) -> Result<OAuthTokenResponse>;
87    fn create_access_token_using_session_token<'a>(
88        &self,
89        config: &Config,
90        session_token: &str,
91        scopes: &[&'a str],
92    ) -> Result<OAuthTokenResponse>;
93    fn create_authorization_code_using_session_token(
94        &self,
95        config: &Config,
96        session_token: &str,
97        auth_params: AuthorizationRequestParameters,
98    ) -> Result<OAuthAuthResponse>;
99    #[allow(dead_code)]
100    fn duplicate_session_token(
101        &self,
102        config: &Config,
103        session_token: &str,
104    ) -> Result<DuplicateTokenResponse>;
105    fn destroy_access_token(&self, config: &Config, token: &str) -> Result<()>;
106    fn destroy_refresh_token(&self, config: &Config, token: &str) -> Result<()>;
107    fn get_profile(
108        &self,
109        config: &Config,
110        profile_access_token: &str,
111        etag: Option<String>,
112    ) -> Result<Option<ResponseAndETag<ProfileResponse>>>;
113    fn get_pending_commands(
114        &self,
115        config: &Config,
116        refresh_token: &str,
117        index: u64,
118        limit: Option<u64>,
119    ) -> Result<PendingCommandsResponse>;
120    fn invoke_command(
121        &self,
122        config: &Config,
123        refresh_token: &str,
124        command: &str,
125        target: &str,
126        payload: &serde_json::Value,
127        ttl: Option<u64>,
128    ) -> Result<()>;
129    fn update_device_record<'a>(
130        &self,
131        config: &Config,
132        refresh_token: &str,
133        update: DeviceUpdateRequest<'a>,
134    ) -> Result<UpdateDeviceResponse>;
135    fn destroy_device_record(&self, config: &Config, refresh_token: &str, id: &str) -> Result<()>;
136    fn get_devices(&self, config: &Config, refresh_token: &str) -> Result<Vec<GetDeviceResponse>>;
137    fn get_attached_clients(
138        &self,
139        config: &Config,
140        session_token: &str,
141    ) -> Result<Vec<GetAttachedClientResponse>>;
142    fn get_scoped_key_data(
143        &self,
144        config: &Config,
145        session_token: &str,
146        client_id: &str,
147        scope: &str,
148    ) -> Result<HashMap<String, ScopedKeyDataResponse>>;
149    #[allow(dead_code)]
150    fn get_fxa_client_configuration(&self, config: &Config) -> Result<ClientConfigurationResponse>;
151    #[allow(dead_code)]
152    fn get_openid_configuration(&self, config: &Config) -> Result<OpenIdConfigurationResponse>;
153    fn simulate_network_error(&self) {}
154}
155
156enum HttpClientState {
157    Ok,
158    Backoff {
159        backoff_end_duration: Duration,
160        time_since_backoff: Instant,
161    },
162}
163
164pub struct Client {
165    state: Mutex<HashMap<String, HttpClientState>>,
166    simulate_network_error: AtomicBool,
167}
168impl FxAClient for Client {
169    fn get_fxa_client_configuration(&self, config: &Config) -> Result<ClientConfigurationResponse> {
170        // Why go through two-levels of indirection? It looks kinda dumb.
171        // Well, `config:Config` also needs to fetch the config, but does not have access
172        // to an instance of `http_client`, so it calls the helper function directly.
173        fxa_client_configuration(config.client_config_url()?)
174    }
175    fn get_openid_configuration(&self, config: &Config) -> Result<OpenIdConfigurationResponse> {
176        openid_configuration(config.openid_config_url()?)
177    }
178
179    fn get_profile(
180        &self,
181        config: &Config,
182        access_token: &str,
183        etag: Option<String>,
184    ) -> Result<Option<ResponseAndETag<ProfileResponse>>> {
185        let url = config.userinfo_endpoint()?;
186        let mut request =
187            Request::get(url).header(header_names::AUTHORIZATION, bearer_token(access_token))?;
188        if let Some(etag) = etag {
189            request = request.header(header_names::IF_NONE_MATCH, format!("\"{}\"", etag))?;
190        }
191        let resp = self.make_request(request)?;
192        if resp.status == status_codes::NOT_MODIFIED {
193            return Ok(None);
194        }
195        let etag = resp
196            .headers
197            .get(header_names::ETAG)
198            .map(ToString::to_string);
199        Ok(Some(ResponseAndETag {
200            etag,
201            response: resp.json()?,
202        }))
203    }
204
205    fn create_refresh_token_using_authorization_code(
206        &self,
207        config: &Config,
208        session_token: Option<&str>,
209        code: &str,
210        code_verifier: &str,
211    ) -> Result<OAuthTokenResponse> {
212        let req_body = OAauthTokenRequest::UsingCode {
213            code: code.to_string(),
214            client_id: config.client_id.to_string(),
215            code_verifier: code_verifier.to_string(),
216            ttl: None,
217        };
218        self.make_oauth_token_request(
219            config,
220            session_token,
221            serde_json::to_value(req_body).unwrap(),
222        )
223    }
224
225    fn create_refresh_token_using_session_token(
226        &self,
227        config: &Config,
228        session_token: &str,
229        scopes: &[&str],
230    ) -> Result<OAuthTokenResponse> {
231        let url = config.token_endpoint()?;
232        let key = derive_auth_key_from_session_token(session_token)?;
233        let body = json!({
234            "client_id": config.client_id,
235            "scope": scopes.join(" "),
236            "grant_type": "fxa-credentials",
237            "access_type": "offline",
238        });
239        let request = HawkRequestBuilder::new(Method::Post, url, &key)
240            .body(body)
241            .build()?;
242        Ok(self.make_request(request)?.json()?)
243    }
244
245    // For the regular generation of an `access_token` from long-lived credentials.
246
247    fn create_access_token_using_refresh_token(
248        &self,
249        config: &Config,
250        refresh_token: &str,
251        ttl: Option<u64>,
252        scopes: &[&str],
253    ) -> Result<OAuthTokenResponse> {
254        let req = OAauthTokenRequest::UsingRefreshToken {
255            client_id: config.client_id.clone(),
256            refresh_token: refresh_token.to_string(),
257            scope: Some(scopes.join(" ")),
258            ttl,
259        };
260        self.make_oauth_token_request(config, None, serde_json::to_value(req).unwrap())
261    }
262
263    fn create_access_token_using_session_token(
264        &self,
265        config: &Config,
266        session_token: &str,
267        scopes: &[&str],
268    ) -> Result<OAuthTokenResponse> {
269        let parameters = json!({
270            "client_id": config.client_id,
271            "grant_type": "fxa-credentials",
272            "scope": scopes.join(" ")
273        });
274        let key = derive_auth_key_from_session_token(session_token)?;
275        let url = config.token_endpoint()?;
276        let request = HawkRequestBuilder::new(Method::Post, url, &key)
277            .body(parameters)
278            .build()?;
279        self.make_request(request)?.json().map_err(Into::into)
280    }
281
282    fn create_authorization_code_using_session_token(
283        &self,
284        config: &Config,
285        session_token: &str,
286        auth_params: AuthorizationRequestParameters,
287    ) -> Result<OAuthAuthResponse> {
288        let parameters = serde_json::to_value(auth_params)?;
289        let key = derive_auth_key_from_session_token(session_token)?;
290        let url = config.auth_url_path("v1/oauth/authorization")?;
291        let request = HawkRequestBuilder::new(Method::Post, url, &key)
292            .body(parameters)
293            .build()?;
294
295        Ok(self.make_request(request)?.json()?)
296    }
297
298    fn check_refresh_token_status(
299        &self,
300        config: &Config,
301        refresh_token: &str,
302    ) -> Result<IntrospectResponse> {
303        let body = json!({
304            "token_type_hint": "refresh_token",
305            "token": refresh_token,
306        });
307        let url = config.introspection_endpoint()?;
308        Ok(self.make_request(Request::post(url).json(&body))?.json()?)
309    }
310
311    fn duplicate_session_token(
312        &self,
313        config: &Config,
314        session_token: &str,
315    ) -> Result<DuplicateTokenResponse> {
316        let url = config.auth_url_path("v1/session/duplicate")?;
317        let key = derive_auth_key_from_session_token(session_token)?;
318        let duplicate_body = json!({
319            "reason": "migration"
320        });
321        let request = HawkRequestBuilder::new(Method::Post, url, &key)
322            .body(duplicate_body)
323            .build()?;
324
325        Ok(self.make_request(request)?.json()?)
326    }
327
328    fn destroy_access_token(&self, config: &Config, access_token: &str) -> Result<()> {
329        let body = json!({
330            "token": access_token,
331        });
332        self.destroy_token_helper(config, &body)
333    }
334
335    fn destroy_refresh_token(&self, config: &Config, refresh_token: &str) -> Result<()> {
336        let body = json!({
337            "refresh_token": refresh_token,
338        });
339        self.destroy_token_helper(config, &body)
340    }
341
342    fn get_pending_commands(
343        &self,
344        config: &Config,
345        refresh_token: &str,
346        index: u64,
347        limit: Option<u64>,
348    ) -> Result<PendingCommandsResponse> {
349        let url = config.auth_url_path("v1/account/device/commands")?;
350        let mut request = Request::get(url)
351            .header(header_names::AUTHORIZATION, bearer_token(refresh_token))?
352            .query(&[("index", &index.to_string())]);
353        if let Some(limit) = limit {
354            request = request.query(&[("limit", &limit.to_string())])
355        }
356        Ok(self.make_request(request)?.json()?)
357    }
358
359    fn invoke_command(
360        &self,
361        config: &Config,
362        refresh_token: &str,
363        command: &str,
364        target: &str,
365        payload: &serde_json::Value,
366        ttl: Option<u64>,
367    ) -> Result<()> {
368        let body = serde_json::to_string(&InvokeCommandRequest {
369            command,
370            target,
371            payload,
372            ttl,
373        })?;
374        let url = config.auth_url_path("v1/account/devices/invoke_command")?;
375        let request = Request::post(url)
376            .header(header_names::AUTHORIZATION, bearer_token(refresh_token))?
377            .header(header_names::CONTENT_TYPE, "application/json")?
378            .body(body);
379        self.make_request(request)?;
380        Ok(())
381    }
382
383    fn get_devices(&self, config: &Config, refresh_token: &str) -> Result<Vec<GetDeviceResponse>> {
384        let url = config.auth_url_path("v1/account/devices")?;
385        let timestamp = util::past_timestamp(DEVICES_FILTER_DAYS).to_string();
386        breadcrumb!(
387            "get_devices timestamp: {timestamp}, refresh len: {}",
388            refresh_token.len()
389        );
390        let request = Request::get(url)
391            .header(header_names::AUTHORIZATION, bearer_token(refresh_token))?
392            .query(&[("filterIdleDevicesTimestamp", &timestamp)]);
393        Ok(self.make_request(request)?.json()?)
394    }
395
396    fn update_device_record(
397        &self,
398        config: &Config,
399        refresh_token: &str,
400        update: DeviceUpdateRequest<'_>,
401    ) -> Result<UpdateDeviceResponse> {
402        let url = config.auth_url_path("v1/account/device")?;
403        let request = Request::post(url)
404            .header(header_names::AUTHORIZATION, bearer_token(refresh_token))?
405            .header(header_names::CONTENT_TYPE, "application/json")?
406            .body(serde_json::to_string(&update)?);
407        Ok(self.make_request(request)?.json()?)
408    }
409
410    fn destroy_device_record(&self, config: &Config, refresh_token: &str, id: &str) -> Result<()> {
411        let body = json!({
412            "id": id,
413        });
414        let url = config.auth_url_path("v1/account/device/destroy")?;
415        let request = Request::post(url)
416            .header(header_names::AUTHORIZATION, bearer_token(refresh_token))?
417            .header(header_names::CONTENT_TYPE, "application/json")?
418            .body(body.to_string());
419
420        self.make_request(request)?;
421        Ok(())
422    }
423
424    fn get_attached_clients(
425        &self,
426        config: &Config,
427        session_token: &str,
428    ) -> Result<Vec<GetAttachedClientResponse>> {
429        let url = config.auth_url_path("v1/account/attached_clients")?;
430        let key = derive_auth_key_from_session_token(session_token)?;
431        let request = HawkRequestBuilder::new(Method::Get, url, &key).build()?;
432        Ok(self.make_request(request)?.json()?)
433    }
434
435    fn get_scoped_key_data(
436        &self,
437        config: &Config,
438        session_token: &str,
439        client_id: &str,
440        scope: &str,
441    ) -> Result<HashMap<String, ScopedKeyDataResponse>> {
442        let body = json!({
443            "client_id": client_id,
444            "scope": scope,
445        });
446        let url = config.auth_url_path("v1/account/scoped-key-data")?;
447        let key = derive_auth_key_from_session_token(session_token)?;
448        let request = HawkRequestBuilder::new(Method::Post, url, &key)
449            .body(body)
450            .build()?;
451        self.make_request(request)?.json().map_err(|e| e.into())
452    }
453
454    fn simulate_network_error(&self) {
455        self.simulate_network_error.store(true, Ordering::Relaxed);
456    }
457}
458
459macro_rules! fetch {
460    ($url:expr) => {
461        viaduct::Request::get($url)
462            .send()?
463            .require_success()?
464            .json()?
465    };
466}
467
468#[inline]
469pub(crate) fn fxa_client_configuration(url: Url) -> Result<ClientConfigurationResponse> {
470    Ok(fetch!(url))
471}
472#[inline]
473pub(crate) fn openid_configuration(url: Url) -> Result<OpenIdConfigurationResponse> {
474    Ok(fetch!(url))
475}
476
477impl Client {
478    pub fn new() -> Self {
479        Self {
480            state: Mutex::new(HashMap::new()),
481            simulate_network_error: AtomicBool::new(false),
482        }
483    }
484
485    fn destroy_token_helper(&self, config: &Config, body: &serde_json::Value) -> Result<()> {
486        let url = config.oauth_url_path("v1/destroy")?;
487        self.make_request(Request::post(url).json(body))?;
488        Ok(())
489    }
490
491    fn make_oauth_token_request(
492        &self,
493        config: &Config,
494        session_token: Option<&str>,
495        body: serde_json::Value,
496    ) -> Result<OAuthTokenResponse> {
497        let url = config.token_endpoint()?;
498        if let Some(session_token) = session_token {
499            let key = derive_auth_key_from_session_token(session_token)?;
500            let request = HawkRequestBuilder::new(Method::Post, url, &key)
501                .body(body)
502                .build()?;
503
504            Ok(self.make_request(request)?.json()?)
505        } else {
506            Ok(self.make_request(Request::post(url).json(&body))?.json()?)
507        }
508    }
509
510    fn handle_too_many_requests(&self, resp: Response) -> Result<Response> {
511        let path = resp.url.path().to_string();
512        if let Some(retry_after) = resp.headers.get_as::<u64, _>(header_names::RETRY_AFTER) {
513            let retry_after = retry_after.unwrap_or(RETRY_AFTER_DEFAULT_SECONDS);
514            let time_out_state = HttpClientState::Backoff {
515                backoff_end_duration: Duration::from_secs(retry_after),
516                time_since_backoff: Instant::now(),
517            };
518            self.state.lock().insert(path, time_out_state);
519            return Err(Error::BackoffError(retry_after));
520        }
521        Self::default_handle_response_error(resp)
522    }
523
524    fn default_handle_response_error(resp: Response) -> Result<Response> {
525        let json: std::result::Result<serde_json::Value, _> = resp.json();
526        match json {
527            Ok(json) => Err(Error::RemoteError {
528                code: json["code"].as_u64().unwrap_or(0),
529                errno: json["errno"].as_u64().unwrap_or(0),
530                error: json["error"].as_str().unwrap_or("").to_string(),
531                message: json["message"].as_str().unwrap_or("").to_string(),
532                info: json["info"].as_str().unwrap_or("").to_string(),
533            }),
534            Err(_) => Err(resp.require_success().unwrap_err().into()),
535        }
536    }
537
538    fn make_request(&self, request: Request) -> Result<Response> {
539        if self.simulate_network_error.swap(false, Ordering::Relaxed) {
540            return Err(Error::RequestError(viaduct::Error::NetworkError(
541                "Simulated error".to_owned(),
542            )));
543        }
544
545        let url = request.url.path().to_string();
546        if let HttpClientState::Backoff {
547            backoff_end_duration,
548            time_since_backoff,
549        } = self.state.lock().get(&url).unwrap_or(&HttpClientState::Ok)
550        {
551            let elapsed_time = time_since_backoff.elapsed();
552            if elapsed_time < *backoff_end_duration {
553                let remaining = *backoff_end_duration - elapsed_time;
554                return Err(Error::BackoffError(remaining.as_secs()));
555            }
556        }
557        self.state.lock().insert(url, HttpClientState::Ok);
558        let resp = request.send()?;
559        if resp.is_success() || resp.status == status_codes::NOT_MODIFIED {
560            Ok(resp)
561        } else {
562            match resp.status {
563                status_codes::TOO_MANY_REQUESTS => self.handle_too_many_requests(resp),
564                _ => Self::default_handle_response_error(resp),
565            }
566        }
567    }
568}
569
570fn bearer_token(token: &str) -> String {
571    format!("Bearer {}", token)
572}
573
574fn kw(name: &str) -> Vec<u8> {
575    format!("identity.mozilla.com/picl/v1/{}", name)
576        .as_bytes()
577        .to_vec()
578}
579
580pub fn derive_auth_key_from_session_token(session_token: &str) -> Result<Vec<u8>> {
581    let session_token_bytes = hex::decode(session_token)?;
582    let context_info = kw("sessionToken");
583    let salt = hmac::SigningKey::new(&digest::SHA256, &HAWK_HKDF_SALT);
584    let mut out = vec![0u8; HAWK_KEY_LENGTH * 2];
585    hkdf::extract_and_expand(&salt, &session_token_bytes, &context_info, &mut out)?;
586    Ok(out)
587}
588
589#[derive(Serialize, Deserialize)]
590pub struct AuthorizationRequestParameters {
591    pub client_id: String,
592    pub scope: String,
593    pub state: String,
594    pub access_type: String,
595    pub code_challenge: Option<String>,
596    pub code_challenge_method: Option<String>,
597    pub keys_jwe: Option<String>,
598}
599
600struct HawkRequestBuilder<'a> {
601    url: Url,
602    method: Method,
603    body: Option<String>,
604    hkdf_sha256_key: &'a [u8],
605}
606
607impl<'a> HawkRequestBuilder<'a> {
608    pub fn new(method: Method, url: Url, hkdf_sha256_key: &'a [u8]) -> Self {
609        rc_crypto::ensure_initialized();
610        HawkRequestBuilder {
611            url,
612            method,
613            body: None,
614            hkdf_sha256_key,
615        }
616    }
617
618    // This class assumes that the content being sent it always of the type
619    // application/json.
620    pub fn body(mut self, body: serde_json::Value) -> Self {
621        self.body = Some(body.to_string());
622        self
623    }
624
625    fn make_hawk_header(&self) -> Result<String> {
626        // Make sure we de-allocate the hash after hawk_request_builder.
627        let hash;
628        let method = format!("{}", self.method);
629        let mut hawk_request_builder = RequestBuilder::from_url(method.as_str(), &self.url)?;
630        if let Some(ref body) = self.body {
631            hash = PayloadHasher::hash("application/json", SHA256, body)?;
632            hawk_request_builder = hawk_request_builder.hash(&hash[..]);
633        }
634        let hawk_request = hawk_request_builder.request();
635        let token_id = hex::encode(&self.hkdf_sha256_key[0..HAWK_KEY_LENGTH]);
636        let hmac_key = &self.hkdf_sha256_key[HAWK_KEY_LENGTH..(2 * HAWK_KEY_LENGTH)];
637        let hawk_credentials = Credentials {
638            id: token_id,
639            key: Key::new(hmac_key, SHA256)?,
640        };
641        let header = hawk_request.make_header(&hawk_credentials)?;
642        Ok(format!("Hawk {}", header))
643    }
644
645    pub fn build(self) -> Result<Request> {
646        let hawk_header = self.make_hawk_header()?;
647        let mut request =
648            Request::new(self.method, self.url).header(header_names::AUTHORIZATION, hawk_header)?;
649        if let Some(body) = self.body {
650            request = request
651                .header(header_names::CONTENT_TYPE, "application/json")?
652                .body(body);
653        }
654        Ok(request)
655    }
656}
657
658#[derive(Deserialize)]
659pub(crate) struct ClientConfigurationResponse {
660    pub(crate) auth_server_base_url: String,
661    pub(crate) oauth_server_base_url: String,
662    pub(crate) profile_server_base_url: String,
663    pub(crate) sync_tokenserver_base_url: String,
664}
665
666#[derive(Deserialize)]
667pub(crate) struct OpenIdConfigurationResponse {
668    pub(crate) authorization_endpoint: String,
669    pub(crate) introspection_endpoint: String,
670    pub(crate) issuer: String,
671    pub(crate) jwks_uri: String,
672    #[allow(dead_code)]
673    pub(crate) token_endpoint: String,
674    pub(crate) userinfo_endpoint: String,
675}
676
677#[derive(Clone)]
678pub struct ResponseAndETag<T> {
679    pub response: T,
680    pub etag: Option<String>,
681}
682
683#[derive(Deserialize)]
684pub struct PendingCommandsResponse {
685    pub index: u64,
686    #[allow(dead_code)]
687    pub last: Option<bool>,
688    pub messages: Vec<PendingCommand>,
689}
690
691#[derive(Deserialize)]
692pub struct PendingCommand {
693    pub index: u64,
694    pub data: CommandData,
695}
696
697#[derive(Debug, Deserialize)]
698pub struct CommandData {
699    pub command: String,
700    pub payload: serde_json::Value, // Need https://github.com/serde-rs/serde/issues/912 to make payload an enum instead.
701    pub sender: Option<String>,
702}
703
704#[derive(Clone, Debug, Deserialize, Serialize)]
705pub struct PushSubscription {
706    #[serde(rename = "pushCallback")]
707    pub endpoint: String,
708    #[serde(rename = "pushPublicKey")]
709    pub public_key: String,
710    #[serde(rename = "pushAuthKey")]
711    pub auth_key: String,
712}
713
714impl From<crate::DevicePushSubscription> for PushSubscription {
715    fn from(sub: crate::DevicePushSubscription) -> Self {
716        PushSubscription {
717            endpoint: sub.endpoint,
718            public_key: sub.public_key,
719            auth_key: sub.auth_key,
720        }
721    }
722}
723
724impl From<PushSubscription> for crate::DevicePushSubscription {
725    fn from(sub: PushSubscription) -> Self {
726        crate::DevicePushSubscription {
727            endpoint: sub.endpoint,
728            public_key: sub.public_key,
729            auth_key: sub.auth_key,
730        }
731    }
732}
733
734/// We use the double Option pattern in this struct.
735/// The outer option represents the existence of the field
736/// and the inner option its value or null.
737/// TL;DR:
738/// `None`: the field will not be present in the JSON body.
739/// `Some(None)`: the field will have a `null` value.
740/// `Some(Some(T))`: the field will have the serialized value of T.
741#[derive(Serialize)]
742#[allow(clippy::option_option)]
743pub struct DeviceUpdateRequest<'a> {
744    #[serde(skip_serializing_if = "Option::is_none")]
745    #[serde(rename = "name")]
746    display_name: Option<Option<&'a str>>,
747    #[serde(skip_serializing_if = "Option::is_none")]
748    #[serde(rename = "type")]
749    device_type: Option<&'a DeviceType>,
750    #[serde(flatten)]
751    push_subscription: Option<&'a PushSubscription>,
752    #[serde(skip_serializing_if = "Option::is_none")]
753    #[serde(rename = "availableCommands")]
754    available_commands: Option<Option<&'a HashMap<String, String>>>,
755}
756
757#[allow(clippy::option_option)]
758pub struct DeviceUpdateRequestBuilder<'a> {
759    device_type: Option<&'a DeviceType>,
760    display_name: Option<Option<&'a str>>,
761    push_subscription: Option<&'a PushSubscription>,
762    available_commands: Option<Option<&'a HashMap<String, String>>>,
763}
764
765impl<'a> DeviceUpdateRequestBuilder<'a> {
766    pub fn new() -> Self {
767        Self {
768            device_type: None,
769            display_name: None,
770            push_subscription: None,
771            available_commands: None,
772        }
773    }
774
775    pub fn push_subscription(mut self, push_subscription: &'a PushSubscription) -> Self {
776        self.push_subscription = Some(push_subscription);
777        self
778    }
779
780    pub fn available_commands(mut self, available_commands: &'a HashMap<String, String>) -> Self {
781        self.available_commands = Some(Some(available_commands));
782        self
783    }
784
785    pub fn display_name(mut self, display_name: &'a str) -> Self {
786        self.display_name = Some(Some(display_name));
787        self
788    }
789
790    pub fn clear_display_name(mut self) -> Self {
791        self.display_name = Some(None);
792        self
793    }
794
795    pub fn device_type(mut self, device_type: &'a DeviceType) -> Self {
796        self.device_type = Some(device_type);
797        self
798    }
799
800    pub fn build(self) -> DeviceUpdateRequest<'a> {
801        DeviceUpdateRequest {
802            display_name: self.display_name,
803            device_type: self.device_type,
804            push_subscription: self.push_subscription,
805            available_commands: self.available_commands,
806        }
807    }
808}
809
810#[derive(Clone, Debug, Serialize, Deserialize)]
811pub struct DeviceLocation {
812    pub city: Option<String>,
813    pub country: Option<String>,
814    pub state: Option<String>,
815    #[serde(rename = "stateCode")]
816    pub state_code: Option<String>,
817}
818
819#[derive(Clone, Debug, Serialize, Deserialize)]
820pub struct GetDeviceResponse {
821    #[serde(flatten)]
822    pub common: DeviceResponseCommon,
823    #[serde(rename = "isCurrentDevice")]
824    pub is_current_device: bool,
825    pub location: DeviceLocation,
826    #[serde(rename = "lastAccessTime")]
827    pub last_access_time: Option<u64>,
828}
829
830impl std::ops::Deref for GetDeviceResponse {
831    type Target = DeviceResponseCommon;
832    fn deref(&self) -> &DeviceResponseCommon {
833        &self.common
834    }
835}
836
837pub type UpdateDeviceResponse = DeviceResponseCommon;
838
839#[derive(Clone, Debug, Serialize, Deserialize)]
840pub struct DeviceResponseCommon {
841    pub id: String,
842    #[serde(rename = "name")]
843    pub display_name: String,
844    #[serde(rename = "type")]
845    pub device_type: DeviceType,
846    #[serde(flatten)]
847    pub push_subscription: Option<PushSubscription>,
848    #[serde(rename = "availableCommands")]
849    pub available_commands: HashMap<String, String>,
850    #[serde(rename = "pushEndpointExpired")]
851    pub push_endpoint_expired: bool,
852}
853
854#[derive(Clone, Debug, Serialize, Deserialize)]
855#[serde(rename_all = "camelCase")]
856pub struct GetAttachedClientResponse {
857    pub client_id: Option<String>,
858    pub session_token_id: Option<String>,
859    pub refresh_token_id: Option<String>,
860    pub device_id: Option<String>,
861    pub device_type: DeviceType,
862    pub is_current_session: bool,
863    pub name: Option<String>,
864    pub created_time: Option<u64>,
865    pub last_access_time: Option<u64>,
866    pub scope: Option<Vec<String>>,
867    pub user_agent: String,
868    pub os: Option<String>,
869}
870
871// We model the OAuthTokenRequest according to the up to date
872// definition on
873// https://github.com/mozilla/fxa/blob/8ae0e6876a50c7f386a9ec5b6df9ebb54ccdf1b5/packages/fxa-auth-server/lib/oauth/routes/token.js#L70-L152
874
875#[derive(Serialize)]
876#[serde(tag = "grant_type")]
877enum OAauthTokenRequest {
878    #[serde(rename = "refresh_token")]
879    UsingRefreshToken {
880        client_id: String,
881        refresh_token: String,
882        #[serde(skip_serializing_if = "Option::is_none")]
883        scope: Option<String>,
884        #[serde(skip_serializing_if = "Option::is_none")]
885        ttl: Option<u64>,
886    },
887    #[serde(rename = "authorization_code")]
888    UsingCode {
889        client_id: String,
890        code: String,
891        code_verifier: String,
892        #[serde(skip_serializing_if = "Option::is_none")]
893        ttl: Option<u64>,
894    },
895}
896
897#[derive(Deserialize)]
898pub struct OAuthTokenResponse {
899    pub keys_jwe: Option<String>,
900    pub refresh_token: Option<String>,
901    pub session_token: Option<String>,
902    pub expires_in: u64,
903    pub scope: String,
904    pub access_token: String,
905}
906
907#[derive(Deserialize, Debug)]
908pub struct OAuthAuthResponse {
909    #[allow(dead_code)]
910    pub redirect: String,
911    pub code: String,
912    #[allow(dead_code)]
913    pub state: String,
914}
915
916#[derive(Deserialize)]
917pub struct IntrospectResponse {
918    pub active: bool,
919    // Technically the response has a lot of other fields,
920    // but in practice we only use `active`.
921}
922
923#[derive(Clone, Serialize, Deserialize)]
924#[serde(rename_all = "camelCase")]
925pub struct ProfileResponse {
926    pub uid: String,
927    pub email: String,
928    pub display_name: Option<String>,
929    pub avatar: String,
930    pub avatar_default: bool,
931}
932
933impl From<ProfileResponse> for crate::Profile {
934    fn from(p: ProfileResponse) -> Self {
935        crate::Profile {
936            uid: p.uid,
937            email: p.email,
938            display_name: p.display_name,
939            avatar: p.avatar,
940            is_default_avatar: p.avatar_default,
941        }
942    }
943}
944
945#[derive(Deserialize)]
946pub struct ScopedKeyDataResponse {
947    #[allow(dead_code)]
948    pub identifier: String,
949    #[allow(dead_code)]
950    #[serde(rename = "keyRotationSecret")]
951    pub key_rotation_secret: String,
952    #[allow(dead_code)]
953    #[serde(rename = "keyRotationTimestamp")]
954    pub key_rotation_timestamp: u64,
955}
956
957#[derive(Deserialize, Clone, Debug, PartialEq, Eq)]
958pub struct DuplicateTokenResponse {
959    pub uid: String,
960    #[serde(rename = "sessionToken")]
961    pub session_token: String,
962    pub verified: bool,
963    #[serde(rename = "authAt")]
964    pub auth_at: u64,
965}
966
967#[derive(Serialize)]
968struct InvokeCommandRequest<'a> {
969    pub command: &'a str,
970    pub target: &'a str,
971    pub payload: &'a serde_json::Value,
972    #[serde(skip_serializing_if = "Option::is_none")]
973    pub ttl: Option<u64>,
974}
975
976#[cfg(test)]
977mod tests {
978    use super::*;
979    use mockito::mock;
980    #[test]
981    #[allow(non_snake_case)]
982    fn check_OAauthTokenRequest_serialization() {
983        // Ensure OAauthTokenRequest serializes to what the server expects.
984        let using_code = OAauthTokenRequest::UsingCode {
985            code: "foo".to_owned(),
986            client_id: "bar".to_owned(),
987            code_verifier: "bobo".to_owned(),
988            ttl: None,
989        };
990        assert_eq!("{\"grant_type\":\"authorization_code\",\"client_id\":\"bar\",\"code\":\"foo\",\"code_verifier\":\"bobo\"}", serde_json::to_string(&using_code).unwrap());
991        let using_code = OAauthTokenRequest::UsingRefreshToken {
992            client_id: "bar".to_owned(),
993            refresh_token: "foo".to_owned(),
994            scope: Some("bobo".to_owned()),
995            ttl: Some(123),
996        };
997        assert_eq!("{\"grant_type\":\"refresh_token\",\"client_id\":\"bar\",\"refresh_token\":\"foo\",\"scope\":\"bobo\",\"ttl\":123}", serde_json::to_string(&using_code).unwrap());
998    }
999
1000    #[test]
1001    #[allow(non_snake_case)]
1002    fn check_InvokeCommandRequest_serialization() -> Result<()> {
1003        let payload = json!({
1004            "a": "b",
1005        });
1006        let with_ttl = InvokeCommandRequest {
1007            command: "with_ttl",
1008            target: "a",
1009            payload: &payload,
1010            ttl: Some(30),
1011        };
1012        assert_eq!(
1013            serde_json::to_value(with_ttl)?,
1014            json!({
1015                "command": "with_ttl",
1016                "target": "a",
1017                "payload": {
1018                    "a": "b",
1019                },
1020                "ttl": 30,
1021            })
1022        );
1023
1024        let without_ttl = InvokeCommandRequest {
1025            command: "without_ttl",
1026            target: "b",
1027            payload: &payload,
1028            ttl: None,
1029        };
1030        assert_eq!(
1031            serde_json::to_value(without_ttl)?,
1032            json!({
1033                "command": "without_ttl",
1034                "target": "b",
1035                "payload": {
1036                    "a": "b",
1037                },
1038            })
1039        );
1040
1041        Ok(())
1042    }
1043
1044    #[test]
1045    fn test_backoff() {
1046        viaduct_reqwest::use_reqwest_backend();
1047        let m = mock("POST", "/v1/account/devices/invoke_command")
1048            .with_status(429)
1049            .with_header("Content-Type", "application/json")
1050            .with_header("retry-after", "1000000")
1051            .with_body(
1052                r#"{
1053                "code": 429,
1054                "errno": 120,
1055                "error": "Too many requests",
1056                "message": "Too many requests",
1057                "retryAfter": 1000000,
1058                "info": "Some information"
1059            }"#,
1060            )
1061            .create();
1062        let client = Client::new();
1063        let path = format!(
1064            "{}/{}",
1065            mockito::server_url(),
1066            "v1/account/devices/invoke_command"
1067        );
1068        let url = Url::parse(&path).unwrap();
1069        let path = url.path().to_string();
1070        let request = Request::post(url);
1071        assert!(client.make_request(request.clone()).is_err());
1072        let state = client.state.lock();
1073        if let HttpClientState::Backoff {
1074            backoff_end_duration,
1075            time_since_backoff: _,
1076        } = state.get(&path).unwrap()
1077        {
1078            assert_eq!(*backoff_end_duration, Duration::from_secs(1_000_000));
1079            // Hacky way to drop the mutex gaurd, so that the next call to
1080            // client.make_request doesn't hang or panic
1081            std::mem::drop(state);
1082            assert!(client.make_request(request).is_err());
1083            // We should be backed off, the second "make_request" should not
1084            // send a request to the server
1085            m.expect(1).assert();
1086        } else {
1087            panic!("HttpClientState should be a timeout!");
1088        }
1089    }
1090
1091    #[test]
1092    fn test_backoff_then_ok() {
1093        viaduct_reqwest::use_reqwest_backend();
1094        let m = mock("POST", "/v1/account/devices/invoke_command")
1095            .with_status(429)
1096            .with_header("Content-Type", "application/json")
1097            .with_header("retry-after", "1")
1098            .with_body(
1099                r#"{
1100                "code": 429,
1101                "errno": 120,
1102                "error": "Too many requests",
1103                "message": "Too many requests",
1104                "retryAfter": 1,
1105                "info": "Some information"
1106            }"#,
1107            )
1108            .create();
1109        let client = Client::new();
1110        let path = format!(
1111            "{}/{}",
1112            mockito::server_url(),
1113            "v1/account/devices/invoke_command"
1114        );
1115        let url = Url::parse(&path).unwrap();
1116        let path = url.path().to_string();
1117        let request = Request::post(url);
1118        assert!(client.make_request(request.clone()).is_err());
1119        let state = client.state.lock();
1120        if let HttpClientState::Backoff {
1121            backoff_end_duration,
1122            time_since_backoff: _,
1123        } = state.get(&path).unwrap()
1124        {
1125            assert_eq!(*backoff_end_duration, Duration::from_secs(1));
1126            // We sleep for 1 second, so pass the backoff timeout
1127            std::thread::sleep(*backoff_end_duration);
1128
1129            // Hacky way to drop the mutex gaurd, so that the next call to
1130            // client.make_request doesn't hang or panic
1131            std::mem::drop(state);
1132            assert!(client.make_request(request).is_err());
1133            // We backed off, but the time has passed, the second request should have
1134            // went to the server
1135            m.expect(2).assert();
1136        } else {
1137            panic!("HttpClientState should be a timeout!");
1138        }
1139    }
1140
1141    #[test]
1142    fn test_backoff_per_path() {
1143        viaduct_reqwest::use_reqwest_backend();
1144        let m1 = mock("POST", "/v1/account/devices/invoke_command")
1145            .with_status(429)
1146            .with_header("Content-Type", "application/json")
1147            .with_header("retry-after", "1000000")
1148            .with_body(
1149                r#"{
1150                "code": 429,
1151                "errno": 120,
1152                "error": "Too many requests",
1153                "message": "Too many requests",
1154                "retryAfter": 1000000,
1155                "info": "Some information"
1156            }"#,
1157            )
1158            .create();
1159        let m2 = mock("GET", "/v1/account/device/commands")
1160            .with_status(200)
1161            .with_header("Content-Type", "application/json")
1162            .with_body(
1163                r#"
1164        {
1165         "index": 3,
1166         "last": true,
1167         "messages": []
1168        }"#,
1169            )
1170            .create();
1171        let client = Client::new();
1172        let path = format!(
1173            "{}/{}",
1174            mockito::server_url(),
1175            "v1/account/devices/invoke_command"
1176        );
1177        let url = Url::parse(&path).unwrap();
1178        let path = url.path().to_string();
1179        let request = Request::post(url);
1180        assert!(client.make_request(request).is_err());
1181        let state = client.state.lock();
1182        if let HttpClientState::Backoff {
1183            backoff_end_duration,
1184            time_since_backoff: _,
1185        } = state.get(&path).unwrap()
1186        {
1187            assert_eq!(*backoff_end_duration, Duration::from_secs(1_000_000));
1188
1189            let path2 = format!("{}/{}", mockito::server_url(), "v1/account/device/commands");
1190            // Hacky way to drop the mutex guard, so that the next call to
1191            // client.make_request doesn't hang or panic
1192            std::mem::drop(state);
1193            let second_request = Request::get(Url::parse(&path2).unwrap());
1194            assert!(client.make_request(second_request).is_ok());
1195            // The first endpoint is backed off, but the second one is not
1196            // Both endpoint should be hit
1197            m1.expect(1).assert();
1198            m2.expect(1).assert();
1199        } else {
1200            panic!("HttpClientState should be a timeout!");
1201        }
1202    }
1203}