Skip to main content

firebase_admin_sdk/auth/
verifier.rs

1use crate::auth::keys::{KeyFetchError, PublicKeyManager};
2use jsonwebtoken::{decode, decode_header, Algorithm, Validation};
3use serde::{Deserialize, Serialize};
4use std::time::{SystemTime, UNIX_EPOCH};
5use thiserror::Error;
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 iss: String,
22    pub aud: String,
23    pub sub: String,
24    pub iat: u64,
25    pub exp: u64,
26    pub auth_time: u64,
27    pub user_id: String,
28    pub provider_id: Option<String>,
29    pub name: Option<String>,
30    pub picture: Option<String>,
31    pub email: Option<String>,
32    pub email_verified: Option<bool>,
33
34    #[serde(flatten)]
35    pub claims: serde_json::Map<String, serde_json::Value>,
36}
37
38pub struct IdTokenVerifier {
39    project_id: String,
40    key_manager: PublicKeyManager,
41}
42
43impl IdTokenVerifier {
44    pub fn new(project_id: String) -> Self {
45        Self {
46            project_id,
47            key_manager: PublicKeyManager::new(),
48        }
49    }
50
51    /// Verifies a Firebase ID token.
52    pub async fn verify_id_token(
53        &self,
54        token: &str,
55    ) -> Result<FirebaseTokenClaims, TokenVerificationError> {
56        self.verify_token_with_issuer(
57            token,
58            &format!("https://securetoken.google.com/{}", self.project_id),
59        )
60        .await
61    }
62
63    /// Verifies a Firebase Session Cookie.
64    pub async fn verify_session_cookie(
65        &self,
66        token: &str,
67    ) -> Result<FirebaseTokenClaims, TokenVerificationError> {
68        self.verify_token_with_issuer(
69            token,
70            &format!("https://session.firebase.google.com/{}", self.project_id),
71        )
72        .await
73    }
74
75    async fn verify_token_with_issuer(
76        &self,
77        token: &str,
78        issuer: &str,
79    ) -> Result<FirebaseTokenClaims, TokenVerificationError> {
80        // 1. Decode header to get kid
81        let header = decode_header(token)?;
82        let kid = header.kid.ok_or_else(|| {
83            TokenVerificationError::InvalidToken("Missing kid in header".to_string())
84        })?;
85
86        // 2. Get public key
87        let key = self.key_manager.get_key(&kid).await?;
88
89        // 3. Configure validation
90        let mut validation = Validation::new(Algorithm::RS256);
91        validation.set_audience(&[&self.project_id]);
92        validation.set_issuer(&[issuer]);
93
94        // 4. Verify
95        let token_data = decode::<FirebaseTokenClaims>(token, &key, &validation)?;
96        let claims = token_data.claims;
97
98        // 5. Additional validations (sub not empty, auth_time < now)
99        if claims.sub.is_empty() {
100            return Err(TokenVerificationError::InvalidToken(
101                "Subject (sub) claim must not be empty".to_string(),
102            ));
103        }
104
105        let now = SystemTime::now()
106            .duration_since(UNIX_EPOCH)
107            .unwrap()
108            .as_secs() as usize;
109        // Allowing some clock skew? jsonwebtoken handles exp/iat with leeway.
110        // auth_time validation usually not strictly enforced by jsonwebtoken default.
111        if claims.auth_time > (now + 300) as u64 {
112            // 5 minutes future skew tolerance
113            return Err(TokenVerificationError::InvalidToken(
114                "Auth time is in the future".to_string(),
115            ));
116        }
117
118        Ok(claims)
119    }
120}