use super::models::{AuthError, AuthResult, JwtClaims, TokenType};
use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation, decode, encode};
use std::time::{SystemTime, UNIX_EPOCH};
const ACCESS_TOKEN_EXPIRATION_SECS: u64 = 3600;
const REFRESH_TOKEN_EXPIRATION_SECS: u64 = 3600 * 24 * 7;
pub struct JwtManager {
encoding_key: EncodingKey,
decoding_key: DecodingKey,
access_expiration_secs: u64,
refresh_expiration_secs: u64,
}
impl JwtManager {
pub fn new(secret: &[u8]) -> Self {
Self {
encoding_key: EncodingKey::from_secret(secret),
decoding_key: DecodingKey::from_secret(secret),
access_expiration_secs: ACCESS_TOKEN_EXPIRATION_SECS,
refresh_expiration_secs: REFRESH_TOKEN_EXPIRATION_SECS,
}
}
pub fn with_expiration(secret: &[u8], access_expiration_secs: u64, refresh_expiration_secs: u64) -> Self {
Self {
encoding_key: EncodingKey::from_secret(secret),
decoding_key: DecodingKey::from_secret(secret),
access_expiration_secs,
refresh_expiration_secs,
}
}
pub fn generate_token(
&self,
user_id: &str,
username: &str,
role: &str,
token_type: TokenType,
) -> AuthResult<String> {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|_| AuthError::TokenGeneration("System time error".to_string()))?
.as_secs() as usize;
let expiration = match token_type {
TokenType::Access => now + self.access_expiration_secs as usize,
TokenType::Refresh => now + self.refresh_expiration_secs as usize,
};
let claims = JwtClaims {
sub: user_id.to_string(),
username: username.to_string(),
role: role.to_string(),
exp: expiration,
iat: now,
token_type,
};
encode(&Header::default(), &claims, &self.encoding_key).map_err(|e| AuthError::TokenGeneration(e.to_string()))
}
pub fn verify_token(&self, token: &str) -> AuthResult<JwtClaims> {
let mut validation = Validation::new(Algorithm::HS256);
validation.leeway = 0;
decode::<JwtClaims>(token, &self.decoding_key, &validation)
.map(|data| data.claims)
.map_err(|e| match e.kind() {
jsonwebtoken::errors::ErrorKind::ExpiredSignature => AuthError::TokenExpired,
_ => AuthError::InvalidToken,
})
}
pub fn verify_access_token(&self, token: &str) -> AuthResult<JwtClaims> {
let claims = self.verify_token(token)?;
if claims.token_type != TokenType::Access {
return Err(AuthError::InvalidToken);
}
Ok(claims)
}
pub fn verify_refresh_token(&self, token: &str) -> AuthResult<JwtClaims> {
let claims = self.verify_token(token)?;
if claims.token_type != TokenType::Refresh {
return Err(AuthError::InvalidToken);
}
Ok(claims)
}
pub fn refresh_access_token(&self, refresh_token: &str) -> AuthResult<String> {
let claims = self.verify_refresh_token(refresh_token)?;
self.generate_token(&claims.sub, &claims.username, &claims.role, TokenType::Access)
}
}
#[cfg(test)]
mod tests {
use super::*;
const TEST_SECRET: &[u8] = b"test-secret-key-for-testing";
#[test]
fn test_generate_and_verify_token() {
let manager = JwtManager::new(TEST_SECRET);
let token = manager
.generate_token("user123", "testuser", "admin", TokenType::Access)
.unwrap();
let claims = manager.verify_token(&token).unwrap();
assert_eq!(claims.sub, "user123");
assert_eq!(claims.username, "testuser");
assert_eq!(claims.role, "admin");
}
#[test]
fn test_token_types() {
let manager = JwtManager::new(TEST_SECRET);
let access_token = manager
.generate_token("user123", "testuser", "admin", TokenType::Access)
.unwrap();
let refresh_token = manager
.generate_token("user123", "testuser", "admin", TokenType::Refresh)
.unwrap();
let access_claims = manager.verify_token(&access_token).unwrap();
let refresh_claims = manager.verify_token(&refresh_token).unwrap();
assert_eq!(access_claims.token_type, TokenType::Access);
assert_eq!(refresh_claims.token_type, TokenType::Refresh);
}
#[test]
fn test_invalid_token() {
let manager = JwtManager::new(TEST_SECRET);
let result = manager.verify_token("invalid.token.here");
assert!(matches!(result, Err(AuthError::InvalidToken)));
}
#[test]
fn test_refresh_token() {
let manager = JwtManager::new(TEST_SECRET);
let refresh_token = manager
.generate_token("user123", "testuser", "admin", TokenType::Refresh)
.unwrap();
let new_access_token = manager.refresh_access_token(&refresh_token).unwrap();
let claims = manager.verify_token(&new_access_token).unwrap();
assert_eq!(claims.sub, "user123");
assert_eq!(claims.token_type, TokenType::Access);
}
#[test]
fn test_custom_expiration() {
let manager = JwtManager::with_expiration(TEST_SECRET, 60, 3600);
let token = manager
.generate_token("user123", "testuser", "admin", TokenType::Access)
.unwrap();
let claims = manager.verify_token(&token).unwrap();
assert!(claims.exp > claims.iat); }
#[test]
fn test_verify_access_token_accepts_access() {
let manager = JwtManager::new(TEST_SECRET);
let access_token = manager
.generate_token("user1", "alice", "admin", TokenType::Access)
.unwrap();
let claims = manager.verify_access_token(&access_token).unwrap();
assert_eq!(claims.token_type, TokenType::Access);
}
#[test]
fn test_verify_access_token_rejects_refresh() {
let manager = JwtManager::new(TEST_SECRET);
let refresh_token = manager
.generate_token("user1", "alice", "admin", TokenType::Refresh)
.unwrap();
let result = manager.verify_access_token(&refresh_token);
assert!(
matches!(result, Err(AuthError::InvalidToken)),
"refresh token should be rejected by verify_access_token"
);
}
#[test]
fn test_verify_refresh_token_accepts_refresh() {
let manager = JwtManager::new(TEST_SECRET);
let refresh_token = manager
.generate_token("user1", "alice", "admin", TokenType::Refresh)
.unwrap();
let claims = manager.verify_refresh_token(&refresh_token).unwrap();
assert_eq!(claims.token_type, TokenType::Refresh);
}
#[test]
fn test_verify_refresh_token_rejects_access() {
let manager = JwtManager::new(TEST_SECRET);
let access_token = manager
.generate_token("user1", "alice", "admin", TokenType::Access)
.unwrap();
let result = manager.verify_refresh_token(&access_token);
assert!(
matches!(result, Err(AuthError::InvalidToken)),
"access token should be rejected by verify_refresh_token"
);
}
}