use chrono::{Duration, Utc};
use jsonwebtoken::{
decode, encode, Algorithm, DecodingKey, EncodingKey, Header, TokenData, Validation,
};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::config::AuthConfig;
use crate::error::ApiError;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Claims {
pub sub: String,
pub email: String,
#[serde(default)]
pub roles: Vec<String>,
pub token_type: String,
pub iat: i64,
pub exp: i64,
pub nbf: i64,
pub iss: String,
pub aud: String,
pub jti: String,
}
impl Claims {
pub fn new_access(
user_id: impl Into<String>,
email: impl Into<String>,
roles: Vec<String>,
config: &AuthConfig,
) -> Self {
let now = Utc::now();
let exp = now + Duration::seconds(config.access_token_expiry_secs as i64);
Self {
sub: user_id.into(),
email: email.into(),
roles,
token_type: "access".to_string(),
iat: now.timestamp(),
exp: exp.timestamp(),
nbf: now.timestamp(),
iss: config.issuer.clone(),
aud: config.audience.clone(),
jti: Uuid::new_v4().to_string(),
}
}
pub fn new_refresh(
user_id: impl Into<String>,
email: impl Into<String>,
config: &AuthConfig,
) -> Self {
let now = Utc::now();
let exp = now + Duration::seconds(config.refresh_token_expiry_secs as i64);
Self {
sub: user_id.into(),
email: email.into(),
roles: vec![],
token_type: "refresh".to_string(),
iat: now.timestamp(),
exp: exp.timestamp(),
nbf: now.timestamp(),
iss: config.issuer.clone(),
aud: config.audience.clone(),
jti: Uuid::new_v4().to_string(),
}
}
pub fn is_access_token(&self) -> bool {
self.token_type == "access"
}
pub fn is_refresh_token(&self) -> bool {
self.token_type == "refresh"
}
pub fn has_role(&self, role: &str) -> bool {
self.roles.iter().any(|r| r == role)
}
pub fn has_any_role(&self, roles: &[&str]) -> bool {
roles.iter().any(|role| self.has_role(role))
}
pub fn has_all_roles(&self, roles: &[&str]) -> bool {
roles.iter().all(|role| self.has_role(role))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenPair {
pub access_token: String,
pub refresh_token: String,
pub token_type: String,
pub expires_in: u64,
}
pub fn create_token_pair(
user_id: impl Into<String>,
email: impl Into<String>,
roles: Vec<String>,
config: &AuthConfig,
) -> Result<TokenPair, ApiError> {
let user_id = user_id.into();
let email = email.into();
let access_claims = Claims::new_access(&user_id, &email, roles, config);
let access_token = encode(
&Header::new(Algorithm::HS256),
&access_claims,
&EncodingKey::from_secret(config.jwt_secret.as_bytes()),
)
.map_err(|e| ApiError::InternalServerError(format!("Failed to create access token: {}", e)))?;
let refresh_claims = Claims::new_refresh(&user_id, &email, config);
let refresh_token = encode(
&Header::new(Algorithm::HS256),
&refresh_claims,
&EncodingKey::from_secret(config.jwt_secret.as_bytes()),
)
.map_err(|e| ApiError::InternalServerError(format!("Failed to create refresh token: {}", e)))?;
Ok(TokenPair {
access_token,
refresh_token,
token_type: "Bearer".to_string(),
expires_in: config.access_token_expiry_secs,
})
}
pub fn verify_token(token: &str, config: &AuthConfig) -> Result<Claims, ApiError> {
let token = token.trim().trim_matches('"');
tracing::debug!("Verifying JWT: {}", token);
let mut validation = Validation::new(Algorithm::HS256);
validation.set_issuer(&[&config.issuer]);
validation.set_audience(&[&config.audience]);
validation.validate_exp = true;
validation.validate_nbf = true;
let token_data: TokenData<Claims> = decode(
token,
&DecodingKey::from_secret(config.jwt_secret.as_bytes()),
&validation,
)
.map_err(|e| {
tracing::debug!("Token verification failed: {}", e);
match e.kind() {
jsonwebtoken::errors::ErrorKind::ExpiredSignature => ApiError::Unauthorized,
jsonwebtoken::errors::ErrorKind::InvalidToken => ApiError::Unauthorized,
_ => ApiError::Unauthorized,
}
})?;
Ok(token_data.claims)
}
pub fn verify_access_token(token: &str, config: &AuthConfig) -> Result<Claims, ApiError> {
let claims = verify_token(token, config)?;
if !claims.is_access_token() {
return Err(ApiError::Unauthorized);
}
Ok(claims)
}
pub fn verify_refresh_token(token: &str, config: &AuthConfig) -> Result<Claims, ApiError> {
let claims = verify_token(token, config)?;
if !claims.is_refresh_token() {
return Err(ApiError::Unauthorized);
}
Ok(claims)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_and_verify_token() {
let config = AuthConfig::default();
let token_pair = create_token_pair(
"user-123",
"test@example.com",
vec!["user".to_string()],
&config,
)
.unwrap();
let claims = verify_access_token(&token_pair.access_token, &config).unwrap();
assert_eq!(claims.sub, "user-123");
assert_eq!(claims.email, "test@example.com");
assert!(claims.has_role("user"));
}
#[test]
fn test_refresh_token() {
let config = AuthConfig::default();
let token_pair =
create_token_pair("user-123", "test@example.com", vec![], &config).unwrap();
let claims = verify_refresh_token(&token_pair.refresh_token, &config).unwrap();
assert_eq!(claims.sub, "user-123");
assert!(claims.is_refresh_token());
}
}