use chrono::{Duration, Utc}; use domain::auth::AccessClaims; use domain::ports::AccessTokenIssuer; use domain::user::User; use domain::DomainError; use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation}; pub const ACCESS_TOKEN_TTL_MINUTES: i64 = 15; pub struct JwtIssuer { enc: EncodingKey, dec: DecodingKey, ttl: Duration, } impl JwtIssuer { pub fn new(secret: &str) -> Self { Self { enc: EncodingKey::from_secret(secret.as_bytes()), dec: DecodingKey::from_secret(secret.as_bytes()), ttl: Duration::minutes(ACCESS_TOKEN_TTL_MINUTES), } } } impl AccessTokenIssuer for JwtIssuer { fn issue(&self, user: &User) -> Result { let claims = AccessClaims { sub: user.id, role: user.role, exp: (Utc::now() + self.ttl).timestamp(), }; jsonwebtoken::encode(&Header::default(), &claims, &self.enc) .map_err(|e| DomainError::Storage(e.to_string())) } fn verify(&self, token: &str) -> Result { jsonwebtoken::decode::(token, &self.dec, &Validation::default()) .map(|d| d.claims) .map_err(|_| DomainError::InvalidToken) } } #[cfg(test)] mod tests { use super::*; use domain::user::Role; use uuid::Uuid; fn user() -> User { User { id: Uuid::new_v4(), email: "a@x.de".into(), display_name: "A".into(), password_hash: String::new(), role: Role::Admin, is_active: true, created_at: Utc::now(), } } #[test] fn issue_and_verify_roundtrip() { let issuer = JwtIssuer::new("0123456789012345678901234567890123456789"); let u = user(); let claims = issuer.verify(&issuer.issue(&u).unwrap()).unwrap(); assert_eq!(claims.sub, u.id); assert_eq!(claims.role, Role::Admin); } #[test] fn other_secret_and_expired_tokens_are_rejected() { let a = JwtIssuer::new("0123456789012345678901234567890123456789"); let b = JwtIssuer::new("abcdefghijabcdefghijabcdefghijabcdefghij"); assert_eq!( b.verify(&a.issue(&user()).unwrap()).unwrap_err(), DomainError::InvalidToken ); let expired = JwtIssuer { ttl: Duration::minutes(-10), ..JwtIssuer::new("0123456789012345678901234567890123456789") }; assert_eq!( a.verify(&expired.issue(&user()).unwrap()).unwrap_err(), DomainError::InvalidToken ); assert_eq!(a.verify("garbage").unwrap_err(), DomainError::InvalidToken); } }