use crate::auth::keys::{KeyFetchError, PublicKeyManager};
use jsonwebtoken::{decode, decode_header, Algorithm, Validation};
use serde::{Deserialize, Serialize};
use std::time::{SystemTime, UNIX_EPOCH};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum TokenVerificationError {
#[error("Key fetch error: {0}")]
KeyFetchError(#[from] KeyFetchError),
#[error("JWT validation error: {0}")]
JwtError(#[from] jsonwebtoken::errors::Error),
#[error("Invalid token: {0}")]
InvalidToken(String),
#[error("Token expired")]
Expired,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct FirebaseTokenClaims {
pub iss: String,
pub aud: String,
pub sub: String,
pub iat: u64,
pub exp: u64,
pub auth_time: u64,
pub user_id: String,
pub provider_id: Option<String>,
pub name: Option<String>,
pub picture: Option<String>,
pub email: Option<String>,
pub email_verified: Option<bool>,
#[serde(flatten)]
pub claims: serde_json::Map<String, serde_json::Value>,
}
pub struct IdTokenVerifier {
project_id: String,
key_manager: PublicKeyManager,
}
impl IdTokenVerifier {
pub fn new(project_id: String) -> Self {
Self {
project_id,
key_manager: PublicKeyManager::new(),
}
}
pub async fn verify_id_token(
&self,
token: &str,
) -> Result<FirebaseTokenClaims, TokenVerificationError> {
self.verify_token_with_issuer(
token,
&format!("https://securetoken.google.com/{}", self.project_id),
)
.await
}
pub async fn verify_session_cookie(
&self,
token: &str,
) -> Result<FirebaseTokenClaims, TokenVerificationError> {
self.verify_token_with_issuer(
token,
&format!("https://session.firebase.google.com/{}", self.project_id),
)
.await
}
async fn verify_token_with_issuer(
&self,
token: &str,
issuer: &str,
) -> Result<FirebaseTokenClaims, TokenVerificationError> {
let header = decode_header(token)?;
let kid = header.kid.ok_or_else(|| {
TokenVerificationError::InvalidToken("Missing kid in header".to_string())
})?;
let key = self.key_manager.get_key(&kid).await?;
let mut validation = Validation::new(Algorithm::RS256);
validation.set_audience(&[&self.project_id]);
validation.set_issuer(&[issuer]);
let token_data = decode::<FirebaseTokenClaims>(token, &key, &validation)?;
let claims = token_data.claims;
if claims.sub.is_empty() {
return Err(TokenVerificationError::InvalidToken(
"Subject (sub) claim must not be empty".to_string(),
));
}
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as usize;
if claims.auth_time > (now + 300) as u64 {
return Err(TokenVerificationError::InvalidToken(
"Auth time is in the future".to_string(),
));
}
Ok(claims)
}
}