1pub mod structs;
2
3use jsonwebtoken::{decode, decode_header, Algorithm, DecodingKey, Validation};
4use reqwest;
5use std::{collections::HashSet, sync::Arc};
6use structs::*;
7use time::{Duration, OffsetDateTime};
8use tokio::sync::RwLock;
9
10impl FirebaseTokenPayload {
11 fn verify(&self, project_id: &str, current_time: OffsetDateTime) -> FirebaseAuthResult<()> {
12 if self.exp <= current_time.unix_timestamp() {
14 return Err(FirebaseAuthError::TokenExpired);
15 }
16
17 if self.iat >= current_time.unix_timestamp() {
19 return Err(FirebaseAuthError::InvalidTokenFormat);
20 }
21
22 if self.auth_time >= current_time.unix_timestamp() {
24 return Err(FirebaseAuthError::InvalidAuthTime);
25 }
26
27 if self.aud != project_id {
29 return Err(FirebaseAuthError::InvalidAudience);
30 }
31
32 let expected_issuer = format!("https://securetoken.google.com/{}", project_id);
34 if self.iss != expected_issuer {
35 return Err(FirebaseAuthError::InvalidIssuer);
36 }
37
38 if self.sub.is_empty() {
40 return Err(FirebaseAuthError::InvalidSubject);
41 }
42
43 Ok(())
44 }
45
46 fn to_auth_user(&self) -> FirebaseAuthUser {
47 FirebaseAuthUser {
48 uid: self.sub.clone(),
49 issued_at: OffsetDateTime::from_unix_timestamp(self.iat)
50 .unwrap_or_else(|_| OffsetDateTime::now_utc()),
51 expires_at: OffsetDateTime::from_unix_timestamp(self.exp)
52 .unwrap_or_else(|_| OffsetDateTime::now_utc()),
53 auth_time: OffsetDateTime::from_unix_timestamp(self.auth_time)
54 .unwrap_or_else(|_| OffsetDateTime::now_utc()),
55 }
56 }
57}
58
59impl FirebaseAuth {
60 pub async fn new(project_id: String) -> Self {
61 let auth = FirebaseAuth {
62 config: FirebaseAuthConfig {
63 project_id,
64 public_keys_url: String::from(
65 "https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com",
66 ),
67 },
68 cached_public_keys: Arc::new(RwLock::new(None)),
69 };
70
71 auth.update_public_keys()
73 .await
74 .expect("Initial key fetch failed");
75
76 auth.start_key_refresh_task();
78
79 auth
80 }
81
82 fn start_key_refresh_task(&self) {
83 let cached_keys = self.cached_public_keys.clone();
84 let config = self.config.clone();
85
86 tokio::spawn(async move {
87 loop {
88 let next_update = {
90 let keys = cached_keys.read().await;
91 keys.as_ref()
92 .map(|state| state.expiry)
93 .unwrap_or_else(|| OffsetDateTime::now_utc())
94 };
95
96 let now = OffsetDateTime::now_utc();
98 let sleep_duration = if next_update > now {
99 let total_duration = (next_update - now).whole_seconds();
101 Duration::seconds((total_duration as f64 * 0.9) as i64)
102 } else {
103 Duration::seconds(0)
104 };
105
106 tokio::time::sleep(tokio::time::Duration::from_secs(
108 sleep_duration.whole_seconds() as u64,
109 ))
110 .await;
111
112 let client = reqwest::Client::new();
114
115 match Self::fetch_public_keys(&config, &client).await {
117 Ok((keys, expiry)) => {
118 let mut cached = cached_keys.write().await;
119 *cached = Some(SharedState { keys, expiry });
120 println!(
121 "Successfully updated public keys. Next update in {} seconds",
122 (expiry - OffsetDateTime::now_utc()).whole_seconds()
123 );
124 }
125 Err(e) => {
126 eprintln!("Failed to update public keys: {:?}", e);
127 tokio::time::sleep(tokio::time::Duration::from_secs(60)).await;
129 }
130 }
131 }
132 });
133 }
134
135 async fn update_public_keys(&self) -> FirebaseAuthResult<()> {
136 println!("Updating public keys...");
137 let client = reqwest::Client::new();
138 let (keys, expiry) = Self::fetch_public_keys(&self.config, &client).await?;
139 let mut cached = self.cached_public_keys.write().await;
140 *cached = Some(SharedState { keys, expiry });
141 println!("Public keys updated successfully with expiry: {}", expiry);
142 Ok(())
143 }
144
145 async fn fetch_public_keys(
146 config: &FirebaseAuthConfig,
147 client: &reqwest::Client,
148 ) -> FirebaseAuthResult<(PublicKeysResponse, OffsetDateTime)> {
149 println!("Fetching public keys from URL: {}", config.public_keys_url);
150 let response = client
151 .get(&config.public_keys_url)
152 .send()
153 .await
154 .map_err(|e| FirebaseAuthError::HttpError(e.to_string()))?;
155
156 println!("Received response with status: {}", response.status());
157 let cache_control = response
159 .headers()
160 .get("Cache-Control")
161 .and_then(|h| h.to_str().ok())
162 .unwrap_or("max-age=3600");
163 println!("Cache-Control header value: {}", cache_control);
164
165 let max_age = cache_control
167 .split(',')
168 .find(|&s| s.trim().starts_with("max-age="))
169 .and_then(|s| s.trim().strip_prefix("max-age="))
170 .and_then(|s| s.parse::<i64>().ok())
171 .unwrap_or(3600);
172
173 let keys: PublicKeysResponse = response
174 .json()
175 .await
176 .map_err(|e| FirebaseAuthError::HttpError(e.to_string()))?;
177
178 let expiry = OffsetDateTime::now_utc() + Duration::seconds(max_age);
180
181 Ok((keys, expiry))
182 }
183
184 pub async fn verify_token(&self, token: &str) -> FirebaseAuthResult<FirebaseAuthUser> {
185 let header =
187 decode_header(token).map_err(|e| FirebaseAuthError::JwtError(e.to_string()))?;
188
189 if header.alg != Algorithm::RS256 {
191 return Err(FirebaseAuthError::InvalidTokenFormat);
192 }
193
194 let kid = header.kid.ok_or(FirebaseAuthError::InvalidTokenFormat)?;
196
197 let cached_keys = self.cached_public_keys.read().await;
199 let state = cached_keys
200 .as_ref()
201 .ok_or(FirebaseAuthError::InvalidTokenFormat)?;
202
203 let public_key = state
205 .keys
206 .keys
207 .get(&kid)
208 .ok_or(FirebaseAuthError::InvalidSignature)?;
209
210 let mut validation = Validation::new(Algorithm::RS256);
212
213 let mut iss_set = HashSet::new();
215 iss_set.insert(format!(
216 "https://securetoken.google.com/{}",
217 self.config.project_id
218 ));
219 validation.iss = Some(iss_set);
220
221 let mut aud_set = HashSet::new();
222 aud_set.insert(self.config.project_id.clone());
223 validation.aud = Some(aud_set);
224
225 validation.validate_exp = true;
226 validation.validate_nbf = false;
227 validation.set_required_spec_claims(&["sub"]);
228
229 let token_data = decode::<FirebaseTokenPayload>(
231 token,
232 &DecodingKey::from_rsa_pem(public_key.as_bytes())
233 .map_err(|e| FirebaseAuthError::JwtError(e.to_string()))?,
234 &validation,
235 )
236 .map_err(|e| FirebaseAuthError::JwtError(e.to_string()))?;
237
238 token_data
240 .claims
241 .verify(&self.config.project_id, OffsetDateTime::now_utc())?;
242
243 Ok(token_data.claims.to_auth_user())
244 }
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250
251 #[tokio::test]
252 async fn test_public_key_fetch() {
253 println!("Starting public key fetch test");
254
255 let auth = FirebaseAuth::new("oyetime-test".to_string()).await;
256 let client = reqwest::Client::new();
257
258 println!("Making request to fetch public keys...");
259 match FirebaseAuth::fetch_public_keys(&auth.config, &client).await {
260 Ok((keys, expiry)) => {
261 println!("✅ Successfully fetched public keys:");
262 println!("Keys: {:#?}", keys);
263 println!("Expiry: {}", expiry);
264 assert!(!keys.keys.is_empty(), "Keys should not be empty");
265 }
266 Err(e) => {
267 println!("❌ Failed to fetch public keys:");
268 println!("Error: {:?}", e);
269 panic!("Public key fetch failed");
270 }
271 }
272 }
273
274 #[tokio::test]
275 async fn test_key_refresh() {
276 println!("Starting key refresh test");
277
278 let auth = FirebaseAuth::new("test-project".to_string()).await;
279 println!(
280 "Initial cached keys: {:#?}",
281 auth.cached_public_keys.read().await
282 );
283
284 auth.update_public_keys().await.expect("Key refresh failed");
285
286 let cached = auth.cached_public_keys.read().await;
287 println!("Updated cached keys: {:#?}", cached);
288 assert!(
289 cached.is_some(),
290 "Cached keys should be present after refresh"
291 );
292 }
293}