use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation, decode, encode};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Claims {
pub sub: String,
pub exp: usize,
pub iat: usize,
pub role: Option<String>,
}
const ALGORITHM: Algorithm = Algorithm::HS256;
const MIN_SECRET_LEN: usize = 32;
pub fn create_token(
user_id: &str,
role: Option<&str>,
secret: &str,
ttl_secs: u64,
) -> crate::error::Result<String> {
if secret.len() < MIN_SECRET_LEN {
return Err(crate::error::Error::Internal(
"JWT secret must be at least 32 bytes".into(),
));
}
let now = now_unix_secs()?;
let claims = Claims {
sub: user_id.to_string(),
exp: now + ttl_secs as usize,
iat: now,
role: role.map(String::from),
};
let header = Header::new(ALGORITHM);
encode(
&header,
&claims,
&EncodingKey::from_secret(secret.as_bytes()),
)
.map_err(|err| crate::error::Error::Internal(format!("JWT encode error: {err}")))
}
pub fn validate_token(token: &str, secret: &str) -> crate::error::Result<Claims> {
let mut validation = Validation::new(ALGORITHM);
validation.validate_exp = true;
validation.leeway = 0;
validation.algorithms = vec![ALGORITHM];
decode::<Claims>(
token,
&DecodingKey::from_secret(secret.as_bytes()),
&validation,
)
.map(|data| data.claims)
.map_err(|_err| crate::error::Error::Unauthorized)
}
fn now_unix_secs() -> crate::error::Result<usize> {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as usize)
.map_err(|err| crate::error::Error::Internal(err.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
const SECRET: &str = "test-secret-that-is-at-least-32-bytes-long!";
#[test]
fn create_and_validate_roundtrip() {
let token =
create_token("user-42", Some("admin"), SECRET, 3600).expect("token creation failed");
let claims = validate_token(&token, SECRET).expect("validation failed");
assert_eq!(claims.sub, "user-42");
assert_eq!(claims.role.as_deref(), Some("admin"));
assert!(claims.exp > claims.iat);
}
#[test]
fn expired_token_is_rejected() {
let token = create_token("user-42", None, SECRET, 0).expect("token creation failed");
std::thread::sleep(std::time::Duration::from_secs(1));
let result = validate_token(&token, SECRET);
assert!(result.is_err());
}
#[test]
fn wrong_secret_is_rejected() {
let other_secret = "another-secret-at-least-32-bytes-long!!";
let token = create_token("user-42", None, SECRET, 3600).expect("token creation failed");
let result = validate_token(&token, other_secret);
assert!(result.is_err());
}
#[test]
fn short_secret_is_rejected() {
let result = create_token("user-42", None, "too-short", 3600);
assert!(result.is_err());
}
#[test]
fn tampered_payload_is_rejected() {
let token = create_token("user-42", None, SECRET, 3600).expect("token creation failed");
let parts: Vec<&str> = token.split('.').collect();
assert_eq!(parts.len(), 3, "JWT should have 3 parts");
let mut payload_bytes = parts[1].as_bytes().to_vec();
payload_bytes[0] ^= 0xFF;
let tampered_payload = String::from_utf8_lossy(&payload_bytes);
let tampered_token = format!("{}.{}.{}", parts[0], tampered_payload, parts[2]);
let result = validate_token(&tampered_token, SECRET);
assert!(result.is_err());
}
}