firebase_admin/auth/session_cookie/
certs.rs1use crate::auth::error::TokenVerificationError;
15use crate::core::http::parse_cache_control_max_age;
16use crate::core::HttpClient;
17use std::collections::HashMap;
18use std::sync::RwLock;
19use std::time::{Duration, Instant};
20use tokio::sync::Mutex;
21use x509_parser::prelude::{FromDer, X509Certificate};
22
23const SESSION_COOKIE_CERTS_URL: &str =
24 "https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys";
25
26const MIN_CACHE_TTL: Duration = Duration::from_secs(60 * 60);
30
31struct Cached {
32 keys: HashMap<String, (String, String)>,
35 fetched_at: Instant,
36 ttl: Duration,
37}
38
39pub struct SessionCookieCertCache {
46 url: String,
47 http: HttpClient,
48 cache: RwLock<Option<Cached>>,
49 refresh_lock: Mutex<()>,
50}
51
52impl SessionCookieCertCache {
53 pub fn new(http: HttpClient) -> Self {
55 Self {
56 url: SESSION_COOKIE_CERTS_URL.to_string(),
57 http,
58 cache: RwLock::new(None),
59 refresh_lock: Mutex::new(()),
60 }
61 }
62
63 pub async fn public_key(&self, kid: &str) -> Result<(String, String), TokenVerificationError> {
66 if let Some(key) = self.cached_key(kid) {
67 return Ok(key);
68 }
69
70 let _guard = self.refresh_lock.lock().await;
71
72 if let Some(key) = self.cached_key(kid) {
73 return Ok(key);
74 }
75
76 self.refresh().await?;
77
78 self.cached_key(kid)
79 .ok_or(TokenVerificationError::InvalidSignature)
80 }
81
82 fn cached_key(&self, kid: &str) -> Option<(String, String)> {
83 let guard = self.cache.read().ok()?;
84 let cached = guard.as_ref()?;
85 if cached.fetched_at.elapsed() > cached.ttl {
86 return None;
87 }
88 cached.keys.get(kid).cloned()
89 }
90
91 async fn refresh(&self) -> Result<(), TokenVerificationError> {
92 let response = self
93 .http
94 .inner()
95 .get(&self.url)
96 .send()
97 .await
98 .map_err(|e| TokenVerificationError::Jwks(e.to_string()))?;
99
100 let ttl = response
101 .headers()
102 .get(reqwest::header::CACHE_CONTROL)
103 .and_then(|v| v.to_str().ok())
104 .and_then(parse_cache_control_max_age)
105 .unwrap_or(MIN_CACHE_TTL);
106
107 let certs: HashMap<String, String> = response
108 .json()
109 .await
110 .map_err(|e| TokenVerificationError::Jwks(e.to_string()))?;
111
112 let mut keys = HashMap::with_capacity(certs.len());
113 for (kid, pem) in certs {
114 let (n, e) = rsa_components_from_certificate_pem(&pem)?;
115 keys.insert(kid, (n, e));
116 }
117
118 let mut guard = self
119 .cache
120 .write()
121 .map_err(|_| TokenVerificationError::Jwks("cert cache lock poisoned".to_string()))?;
122 *guard = Some(Cached {
123 keys,
124 fetched_at: Instant::now(),
125 ttl,
126 });
127
128 Ok(())
129 }
130}
131
132fn rsa_components_from_certificate_pem(
145 pem: &str,
146) -> Result<(String, String), TokenVerificationError> {
147 use base64::Engine;
148
149 let (_, pem) = x509_parser::pem::parse_x509_pem(pem.as_bytes())
150 .map_err(|e| TokenVerificationError::Jwks(format!("invalid certificate PEM: {e}")))?;
151 let (_, cert) = X509Certificate::from_der(&pem.contents)
152 .map_err(|e| TokenVerificationError::Jwks(format!("invalid certificate DER: {e}")))?;
153
154 let spki = &cert.tbs_certificate.subject_pki;
155 let public_key = spki
156 .parsed()
157 .map_err(|e| TokenVerificationError::Jwks(format!("unsupported public key: {e}")))?;
158
159 let rsa_key = match public_key {
160 x509_parser::public_key::PublicKey::RSA(rsa) => rsa,
161 other => {
162 return Err(TokenVerificationError::Jwks(format!(
163 "expected an RSA public key, found {other:?}"
164 )))
165 }
166 };
167
168 let n = base64::engine::general_purpose::URL_SAFE_NO_PAD
169 .encode(strip_leading_zero(rsa_key.modulus));
170 let e = base64::engine::general_purpose::URL_SAFE_NO_PAD
171 .encode(strip_leading_zero(rsa_key.exponent));
172 Ok((n, e))
173}
174
175fn strip_leading_zero(bytes: &[u8]) -> &[u8] {
182 match bytes {
183 [0x00, rest @ ..] if !rest.is_empty() => rest,
184 _ => bytes,
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191 use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
192 use serde_json::json;
193 use wiremock::matchers::{method, path};
194 use wiremock::{Mock, MockServer, ResponseTemplate};
195
196 const TEST_PRIVATE_KEY_PEM: &str = include_str!("../../../tests/fixtures/test_private_key.pem");
197 const TEST_CERT_PEM: &str = include_str!("../../../tests/fixtures/test_cert.pem");
201
202 #[test]
203 fn extracts_the_correct_rsa_public_key_from_a_certificate() {
204 let (n, e) = rsa_components_from_certificate_pem(TEST_CERT_PEM).unwrap();
205
206 let claims = json!({
212 "sub": "someone",
213 "iss": "issuer",
214 "aud": "audience",
215 "exp": 9_999_999_999i64,
216 });
217 let signing_key = EncodingKey::from_rsa_pem(TEST_PRIVATE_KEY_PEM.as_bytes()).unwrap();
218 let token = encode(&Header::new(Algorithm::RS256), &claims, &signing_key).unwrap();
219
220 let decoding_key = DecodingKey::from_rsa_components(&n, &e).unwrap();
221 let mut validation = Validation::new(Algorithm::RS256);
222 validation.set_audience(&["audience"]);
223 validation.set_issuer(&["issuer"]);
224 decode::<serde_json::Value>(&token, &decoding_key, &validation)
225 .expect("token should verify against the key extracted from the certificate");
226 }
227
228 #[test]
229 fn rejects_a_malformed_certificate() {
230 let err = rsa_components_from_certificate_pem("not a certificate").unwrap_err();
231 assert!(matches!(err, TokenVerificationError::Jwks(_)));
232 }
233
234 #[tokio::test]
235 async fn fetches_and_caches_certs_from_the_endpoint() {
236 let server = MockServer::start().await;
237 Mock::given(method("GET"))
238 .and(path("/certs"))
239 .respond_with(
240 ResponseTemplate::new(200).set_body_json(json!({ "test-kid": TEST_CERT_PEM })),
241 )
242 .expect(1)
243 .mount(&server)
244 .await;
245
246 let cache = SessionCookieCertCache {
247 url: format!("{}/certs", server.uri()),
248 http: HttpClient::default(),
249 cache: RwLock::new(None),
250 refresh_lock: Mutex::new(()),
251 };
252
253 let (n, e) = cache.public_key("test-kid").await.unwrap();
254 let expected = rsa_components_from_certificate_pem(TEST_CERT_PEM).unwrap();
255 assert_eq!((n, e), expected);
256
257 cache.public_key("test-kid").await.unwrap();
259 }
260}