firebase_admin_sdk/auth/
verifier.rs

1use jsonwebtoken::{decode, decode_header, Algorithm, DecodingKey, Validation};
2use serde::{Deserialize, Serialize};
3use crate::auth::keys::{PublicKeyManager, KeyFetchError};
4use thiserror::Error;
5use std::time::{SystemTime, UNIX_EPOCH};
6
7#[derive(Error, Debug)]
8pub enum TokenVerificationError {
9    #[error("Key fetch error: {0}")]
10    KeyFetchError(#[from] KeyFetchError),
11    #[error("JWT validation error: {0}")]
12    JwtError(#[from] jsonwebtoken::errors::Error),
13    #[error("Invalid token: {0}")]
14    InvalidToken(String),
15    #[error("Token expired")]
16    Expired,
17}
18
19#[derive(Debug, Serialize, Deserialize)]
20pub struct FirebaseTokenClaims {
21    pub aud: String,
22    pub iss: String,
23    pub sub: String,
24    pub exp: usize,
25    pub iat: usize,
26    pub auth_time: usize,
27    pub user_id: String,
28    #[serde(flatten)]
29    pub claims: serde_json::Map<String, serde_json::Value>,
30}
31
32pub struct IdTokenVerifier {
33    project_id: String,
34    key_manager: PublicKeyManager,
35}
36
37impl IdTokenVerifier {
38    pub fn new(project_id: String) -> Self {
39        Self {
40            project_id,
41            key_manager: PublicKeyManager::new(),
42        }
43    }
44
45    /// Verifies a Firebase ID token.
46    pub async fn verify_id_token(&self, token: &str) -> Result<FirebaseTokenClaims, TokenVerificationError> {
47        self.verify_token_with_issuer(token, &format!("https://securetoken.google.com/{}", self.project_id)).await
48    }
49
50    /// Verifies a Firebase Session Cookie.
51    pub async fn verify_session_cookie(&self, token: &str) -> Result<FirebaseTokenClaims, TokenVerificationError> {
52        self.verify_token_with_issuer(token, &format!("https://session.firebase.google.com/{}", self.project_id)).await
53    }
54
55    async fn verify_token_with_issuer(&self, token: &str, issuer: &str) -> Result<FirebaseTokenClaims, TokenVerificationError> {
56        // 1. Decode header to get kid
57        let header = decode_header(token)?;
58        let kid = header.kid.ok_or_else(|| TokenVerificationError::InvalidToken("Missing kid in header".to_string()))?;
59
60        // 2. Get public key
61        let public_key_pem = self.key_manager.get_key(&kid).await?;
62        let key = DecodingKey::from_rsa_pem(public_key_pem.as_bytes())?;
63
64        // 3. Configure validation
65        let mut validation = Validation::new(Algorithm::RS256);
66        validation.set_audience(&[&self.project_id]);
67        validation.set_issuer(&[issuer]);
68
69        // 4. Verify
70        let token_data = decode::<FirebaseTokenClaims>(token, &key, &validation)?;
71        let claims = token_data.claims;
72
73        // 5. Additional validations (sub not empty, auth_time < now)
74        if claims.sub.is_empty() {
75            return Err(TokenVerificationError::InvalidToken("Subject (sub) claim must not be empty".to_string()));
76        }
77
78        let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as usize;
79        // Allowing some clock skew? jsonwebtoken handles exp/iat with leeway.
80        // auth_time validation usually not strictly enforced by jsonwebtoken default.
81        if claims.auth_time > now + 300 { // 5 minutes future skew tolerance
82             return Err(TokenVerificationError::InvalidToken("Auth time is in the future".to_string()));
83        }
84
85        Ok(claims)
86    }
87}