firebase_token/
jwk.rs

1use std::time::Duration;
2
3use reqwest::header::HeaderMap;
4use serde::Deserialize;
5use thiserror::Error;
6
7const JWK_URL: &str =
8    "https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com";
9
10#[derive(Debug, Deserialize)]
11pub(crate) struct JwkKeys {
12    pub(crate) keys: Vec<JwkKey>,
13    pub(crate) max_age: Option<Duration>,
14}
15
16#[derive(Debug, Deserialize)]
17pub(crate) struct JwkKey {
18    pub(crate) e: String,
19    pub(crate) alg: String,
20    pub(crate) kid: String,
21    pub(crate) n: String,
22}
23
24#[derive(Debug, Deserialize)]
25struct KeyResponse {
26    keys: Vec<JwkKey>,
27}
28
29#[derive(Error, Debug)]
30pub enum JwkKeysError {
31    #[error("Fetch `{0}`")]
32    Fetch(reqwest::Error),
33    #[error("NoCacheControlKey")]
34    NoCacheControlKey,
35    #[error("InvalidCacheControlValue `{0}`")]
36    InvalidCacheControlValue(reqwest::header::ToStrError),
37    #[error("InvalidMaxAge `{0}`")]
38    InvalidMaxAge(String),
39    #[error("Parse `{0}`")]
40    Parse(reqwest::Error),
41}
42
43impl JwkKeys {
44    pub(crate) async fn fetch_keys() -> Result<Self, JwkKeysError> {
45        let response = reqwest::get(JWK_URL).await.map_err(JwkKeysError::Fetch)?;
46
47        let max_age = match parse_max_age(response.headers()) {
48            Ok(v) => Some(v),
49            Err(e) => {
50                tracing::error!("could not parse max_age: {:?}", e);
51                None
52            }
53        };
54
55        let public_keys = response
56            .json::<KeyResponse>()
57            .await
58            .map_err(JwkKeysError::Parse)?;
59
60        Ok(JwkKeys {
61            keys: public_keys.keys,
62            max_age,
63        })
64    }
65}
66
67fn parse_max_age(headers: &HeaderMap) -> Result<Duration, JwkKeysError> {
68    let cache_control = match headers.get("Cache-Control") {
69        Some(header_value) => header_value.to_str(),
70        None => return Err(JwkKeysError::NoCacheControlKey),
71    };
72
73    let cache_control_value = match cache_control {
74        Ok(v) => v,
75        Err(err) => return Err(JwkKeysError::InvalidCacheControlValue(err)),
76    };
77
78    for v in cache_control_value.split(',') {
79        let mut pair = v.split('=').map(|s| s.trim());
80        let key = match pair.next() {
81            Some(v) => v,
82            None => continue,
83        };
84
85        if key.to_lowercase() != "max-age" {
86            continue;
87        }
88
89        let value = match pair.next().and_then(|v| v.parse().ok()) {
90            Some(v) => v,
91            None => continue,
92        };
93        return Ok(Duration::from_secs(value));
94    }
95
96    Err(JwkKeysError::InvalidMaxAge(cache_control_value.to_owned()))
97}