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}