use chrono::{DateTime, Duration, Utc};
use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation, decode, encode};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Claims {
pub sub: String,
pub iss: String,
pub aud: String,
pub iat: u64,
pub exp: u64,
}
#[derive(Debug, Clone)]
pub struct JwtConfig {
pub secret: String,
pub issuer: String,
pub audience: String,
pub ttl_hours: u32,
}
pub fn create_token(user_id: &str, config: &JwtConfig, now: DateTime<Utc>) -> String {
let iat = now.timestamp() as u64;
let exp = (now + Duration::hours(i64::from(config.ttl_hours))).timestamp() as u64;
let claims = Claims {
sub: user_id.to_owned(),
iss: config.issuer.clone(),
aud: config.audience.clone(),
iat,
exp,
};
encode(
&Header::default(),
&claims,
&EncodingKey::from_secret(config.secret.as_bytes()),
)
.expect("JWT encoding is infallible with HMAC-SHA256")
}
pub fn verify_token(token: &str, config: &JwtConfig) -> Option<Claims> {
let mut validation = Validation::default();
validation.set_issuer(&[&config.issuer]);
validation.set_audience(&[&config.audience]);
decode::<Claims>(
token,
&DecodingKey::from_secret(config.secret.as_bytes()),
&validation,
)
.map(|data| data.claims)
.ok()
}
#[cfg(test)]
mod tests {
use super::*;
fn test_config() -> JwtConfig {
JwtConfig {
secret: "test-secret-key-for-jwt-validation".to_owned(),
issuer: "lenso".to_owned(),
audience: "lenso".to_owned(),
ttl_hours: 1,
}
}
#[test]
fn roundtrip_jwt_token() {
let config = test_config();
let now = Utc::now();
let token = create_token("usr_abc123", &config, now);
let claims = verify_token(&token, &config).expect("token should verify");
assert_eq!(claims.sub, "usr_abc123");
assert_eq!(claims.iss, "lenso");
assert_eq!(claims.aud, "lenso");
}
#[test]
fn rejects_wrong_secret() {
let config = test_config();
let now = Utc::now();
let token = create_token("usr_abc123", &config, now);
let mut wrong_config = config.clone();
wrong_config.secret = "wrong-secret".to_owned();
assert!(verify_token(&token, &wrong_config).is_none());
}
#[test]
fn rejects_expired_token() {
let config = test_config();
let past = Utc::now() - Duration::hours(2);
let token = create_token("usr_abc123", &config, past);
assert!(verify_token(&token, &config).is_none());
}
#[test]
fn rejects_wrong_audience() {
let config = test_config();
let now = Utc::now();
let token = create_token("usr_abc123", &config, now);
let mut wrong_config = config.clone();
wrong_config.audience = "wrong-audience".to_owned();
assert!(verify_token(&token, &wrong_config).is_none());
}
}