use std::time::{SystemTime, UNIX_EPOCH}; use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation, decode, encode}; use serde::{Deserialize, Serialize}; use uuid::Uuid; #[derive(Debug, thiserror::Error)] pub enum JwtError { #[error("jwt secret not configured")] NotConfigured, #[error("jwt invalid: {0}")] Invalid(String), } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AppJwtClaims { pub sub: String, pub token_hash: String, pub jti: String, pub appid: String, pub iat: i64, pub exp: i64, } impl AppJwtClaims { pub fn uid(&self) -> Option { Uuid::parse_str(&self.sub).ok() } pub fn jti_uuid(&self) -> Option { Uuid::parse_str(&self.jti).ok() } } fn now_secs() -> i64 { SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_secs() as i64 } pub fn issue_app_jwt( secret: &str, appid: &str, uid: Uuid, token_hash: &str, ttl_secs: u64, ) -> Result { if secret.is_empty() { return Err(JwtError::NotConfigured); } let iat = now_secs(); let claims = AppJwtClaims { sub: uid.to_string(), token_hash: token_hash.to_string(), jti: Uuid::now_v7().to_string(), appid: appid.to_string(), iat, exp: iat + ttl_secs as i64, }; encode( &Header::new(Algorithm::HS256), &claims, &EncodingKey::from_secret(secret.as_bytes()), ) .map_err(|e| JwtError::Invalid(e.to_string())) } pub fn verify_app_jwt(secret: &str, appid: &str, token: &str) -> Result { if secret.is_empty() { return Err(JwtError::NotConfigured); } let data = decode::( token, &DecodingKey::from_secret(secret.as_bytes()), &Validation::new(Algorithm::HS256), ) .map_err(|e| JwtError::Invalid(e.to_string()))?; if data.claims.appid != appid { return Err(JwtError::Invalid("appid mismatch".to_string())); } Ok(data.claims) }