#![allow(deprecated)]
use crate::rest_authentication::RestAuthentication;
use crate::{AuthenticationBackend, AuthenticationError, SimpleUser, User};
use chrono::{Duration, Utc};
use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation, decode, encode};
use reinhardt_http::Request;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use uuid::Uuid;
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum JwtError {
#[error("Token expired")]
TokenExpired,
#[error("Invalid signature: {0}")]
InvalidSignature(String),
#[error("Invalid token: {0}")]
InvalidToken(String),
#[error("Encoding error: {0}")]
EncodingError(String),
}
impl From<jsonwebtoken::errors::Error> for JwtError {
fn from(err: jsonwebtoken::errors::Error) -> Self {
match err.kind() {
jsonwebtoken::errors::ErrorKind::ExpiredSignature => JwtError::TokenExpired,
jsonwebtoken::errors::ErrorKind::InvalidSignature
| jsonwebtoken::errors::ErrorKind::InvalidRsaKey(_)
| jsonwebtoken::errors::ErrorKind::InvalidEcdsaKey => {
JwtError::InvalidSignature(err.to_string())
}
_ => JwtError::InvalidToken(err.to_string()),
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Claims {
pub sub: String,
pub exp: i64,
pub iat: i64,
pub username: String,
#[serde(default)]
pub is_staff: bool,
#[serde(default)]
pub is_superuser: bool,
}
impl Claims {
pub fn new(
user_id: String,
username: String,
expires_in: Duration,
is_staff: bool,
is_superuser: bool,
) -> Self {
let now = Utc::now();
Self {
sub: user_id,
username,
iat: now.timestamp(),
exp: (now + expires_in).timestamp(),
is_staff,
is_superuser,
}
}
pub fn is_expired(&self) -> bool {
Utc::now().timestamp() > self.exp
}
}
#[derive(Clone)]
pub struct JwtAuth {
encoding_key: EncodingKey,
decoding_key: DecodingKey,
validation: Validation,
validation_allow_expired: Validation,
}
impl JwtAuth {
pub fn new(secret: &[u8]) -> Self {
let mut validation_allow_expired = Validation::default();
validation_allow_expired.validate_exp = false;
Self {
encoding_key: EncodingKey::from_secret(secret),
decoding_key: DecodingKey::from_secret(secret),
validation: Validation::default(),
validation_allow_expired,
}
}
pub fn encode(&self, claims: &Claims) -> Result<String, JwtError> {
encode(&Header::default(), claims, &self.encoding_key)
.map_err(|e| JwtError::EncodingError(e.to_string()))
}
pub fn decode(&self, token: &str) -> Result<Claims, JwtError> {
decode::<Claims>(token, &self.decoding_key, &self.validation)
.map(|data| data.claims)
.map_err(JwtError::from)
}
pub fn generate_token(
&self,
user_id: String,
username: String,
is_staff: bool,
is_superuser: bool,
) -> Result<String, JwtError> {
let claims = Claims::new(
user_id,
username,
Duration::hours(24),
is_staff,
is_superuser,
);
self.encode(&claims)
}
pub fn verify_token(&self, token: &str) -> Result<Claims, JwtError> {
let claims = self.decode(token)?;
if claims.is_expired() {
return Err(JwtError::TokenExpired);
}
Ok(claims)
}
pub fn verify_token_allow_expired(&self, token: &str) -> Result<Claims, JwtError> {
decode::<Claims>(token, &self.decoding_key, &self.validation_allow_expired)
.map(|data| data.claims)
.map_err(JwtError::from)
}
}
#[async_trait::async_trait]
impl RestAuthentication for JwtAuth {
async fn authenticate(
&self,
request: &Request,
) -> Result<Option<Box<dyn User>>, AuthenticationError> {
let auth_header = request
.headers
.get("Authorization")
.and_then(|h| h.to_str().ok());
if let Some(header) = auth_header {
if let Some(token) = header.strip_prefix("Bearer ") {
match self.verify_token(token) {
Ok(claims) => {
let id = Uuid::parse_str(&claims.sub)
.map_err(|_| AuthenticationError::InvalidToken)?;
return Ok(Some(Box::new(SimpleUser {
id,
username: claims.username.clone(),
email: String::new(),
is_active: true,
is_admin: false,
is_staff: false,
is_superuser: false,
})));
}
Err(err) => {
return Err(AuthenticationError::from(err));
}
}
}
}
Ok(None)
}
}
#[async_trait::async_trait]
impl AuthenticationBackend for JwtAuth {
async fn authenticate(
&self,
request: &Request,
) -> Result<Option<Box<dyn User>>, AuthenticationError> {
<Self as RestAuthentication>::authenticate(self, request).await
}
async fn get_user(&self, _user_id: &str) -> Result<Option<Box<dyn User>>, AuthenticationError> {
Ok(None)
}
}
#[cfg(test)]
mod tests {
use super::*;
use bytes::Bytes;
use hyper::{HeaderMap, Method};
use reinhardt_http::Request;
use rstest::rstest;
fn create_request_with_bearer(token: &str) -> Request {
let mut headers = HeaderMap::new();
headers.insert(
"Authorization",
format!("Bearer {}", token).parse().unwrap(),
);
Request::builder()
.method(Method::GET)
.uri("/api/resource")
.headers(headers)
.body(Bytes::new())
.build()
.unwrap()
}
#[rstest]
#[tokio::test]
async fn test_authenticate_with_valid_uuid_sub_claim() {
let jwt_auth = JwtAuth::new(b"test-secret-key-256bit!");
let user_id = "550e8400-e29b-41d4-a716-446655440000";
let username = "alice";
let token = jwt_auth
.generate_token(user_id.to_string(), username.to_string(), false, false)
.unwrap();
let request = create_request_with_bearer(&token);
let result = RestAuthentication::authenticate(&jwt_auth, &request).await;
let user = result.unwrap().unwrap();
assert_eq!(user.id(), user_id);
assert_eq!(user.username(), username);
assert!(user.is_authenticated());
assert!(user.is_active());
}
#[rstest]
#[tokio::test]
async fn test_authenticate_with_non_uuid_sub_claim_returns_invalid_token() {
let jwt_auth = JwtAuth::new(b"test-secret-key-256bit!");
let claims = Claims::new(
"not-a-valid-uuid".to_string(),
"bob".to_string(),
Duration::hours(1),
false,
false,
);
let token = jwt_auth.encode(&claims).unwrap();
let request = create_request_with_bearer(&token);
let result = RestAuthentication::authenticate(&jwt_auth, &request).await;
assert!(
matches!(&result, Err(AuthenticationError::InvalidToken)),
"expected InvalidToken error for non-UUID sub claim"
);
}
#[rstest]
#[tokio::test]
async fn test_authenticate_with_empty_sub_claim_returns_invalid_token() {
let jwt_auth = JwtAuth::new(b"test-secret-key-256bit!");
let claims = Claims::new(
String::new(),
"charlie".to_string(),
Duration::hours(1),
false,
false,
);
let token = jwt_auth.encode(&claims).unwrap();
let request = create_request_with_bearer(&token);
let result = RestAuthentication::authenticate(&jwt_auth, &request).await;
assert!(
matches!(&result, Err(AuthenticationError::InvalidToken)),
"expected InvalidToken error for empty sub claim"
);
}
#[rstest]
#[tokio::test]
async fn test_authenticate_with_tampered_token_returns_invalid_token() {
let jwt_auth = JwtAuth::new(b"test-secret-key-256bit!");
let token = jwt_auth
.generate_token(
"550e8400-e29b-41d4-a716-446655440000".to_string(),
"dave".to_string(),
false,
false,
)
.unwrap();
let tampered_token = format!("{}tampered", token);
let request = create_request_with_bearer(&tampered_token);
let result = RestAuthentication::authenticate(&jwt_auth, &request).await;
assert!(matches!(&result, Err(AuthenticationError::InvalidToken)));
}
#[rstest]
#[tokio::test]
async fn test_authenticate_without_authorization_header_returns_none() {
let jwt_auth = JwtAuth::new(b"test-secret-key-256bit!");
let request = Request::builder()
.method(Method::GET)
.uri("/api/resource")
.body(Bytes::new())
.build()
.unwrap();
let result = RestAuthentication::authenticate(&jwt_auth, &request).await;
assert!(result.unwrap().is_none());
}
#[rstest]
#[tokio::test]
async fn test_authenticate_with_non_bearer_prefix_returns_none() {
let jwt_auth = JwtAuth::new(b"test-secret-key-256bit!");
let mut headers = HeaderMap::new();
headers.insert("Authorization", "Token some-token-value".parse().unwrap());
let request = Request::builder()
.method(Method::GET)
.uri("/api/resource")
.headers(headers)
.body(Bytes::new())
.build()
.unwrap();
let result = RestAuthentication::authenticate(&jwt_auth, &request).await;
assert!(result.unwrap().is_none());
}
#[rstest]
#[tokio::test]
async fn test_authenticate_with_wrong_secret_returns_invalid_token() {
let jwt_auth_encode = JwtAuth::new(b"encoding-secret-key!!!");
let jwt_auth_decode = JwtAuth::new(b"different-secret-key!!");
let token = jwt_auth_encode
.generate_token(
"550e8400-e29b-41d4-a716-446655440000".to_string(),
"eve".to_string(),
false,
false,
)
.unwrap();
let request = create_request_with_bearer(&token);
let result = RestAuthentication::authenticate(&jwt_auth_decode, &request).await;
assert!(matches!(&result, Err(AuthenticationError::InvalidToken)));
}
#[rstest]
#[tokio::test]
async fn test_authenticate_does_not_fabricate_privilege_flags() {
let jwt_auth = JwtAuth::new(b"test-secret-key-256bit!");
let user_id = "550e8400-e29b-41d4-a716-446655440000";
let username = "alice";
let token = jwt_auth
.generate_token(user_id.to_string(), username.to_string(), false, false)
.unwrap();
let request = create_request_with_bearer(&token);
let result = RestAuthentication::authenticate(&jwt_auth, &request).await;
let user = result.unwrap().unwrap();
assert_eq!(user.id(), user_id);
assert_eq!(user.username(), username);
assert!(user.is_active());
assert!(!user.is_admin(), "admin flag should default to false");
assert!(!user.is_staff(), "staff flag should default to false");
assert!(
!user.is_superuser(),
"superuser flag should default to false"
);
}
#[rstest]
#[tokio::test]
async fn test_jwt_authenticated_user_has_no_email_in_claims() {
let jwt_auth = JwtAuth::new(b"test-secret-key-256bit!");
let token = jwt_auth
.generate_token(
"550e8400-e29b-41d4-a716-446655440000".to_string(),
"alice".to_string(),
false,
false,
)
.unwrap();
let request = create_request_with_bearer(&token);
let result = RestAuthentication::authenticate(&jwt_auth, &request).await;
let user = result.unwrap().unwrap();
let claims = Claims::new(
"550e8400-e29b-41d4-a716-446655440000".to_string(),
"alice".to_string(),
Duration::hours(1),
false,
false,
);
let serialized = serde_json::to_value(&claims).unwrap();
assert!(
serialized.get("email").is_none(),
"JWT Claims must not contain an email field"
);
assert_eq!(user.username(), "alice");
assert_eq!(user.id(), "550e8400-e29b-41d4-a716-446655440000");
}
#[rstest]
fn test_verify_expired_token_returns_token_expired_error() {
let jwt_auth = JwtAuth::new(b"test-secret-key-256bit!");
let claims = Claims {
sub: "user123".to_string(),
exp: Utc::now().timestamp() - 3600, iat: Utc::now().timestamp() - 7200,
username: "alice".to_string(),
is_staff: false,
is_superuser: false,
};
let token = jwt_auth.encode(&claims).unwrap();
let result = jwt_auth.verify_token(&token);
assert_eq!(result.unwrap_err(), JwtError::TokenExpired);
}
#[rstest]
fn test_verify_tampered_token_returns_invalid_token() {
let jwt_auth = JwtAuth::new(b"test-secret-key-256bit!");
let token = jwt_auth
.generate_token("user123".to_string(), "alice".to_string(), false, false)
.unwrap();
let tampered = format!("{}tampered", token);
let result = jwt_auth.verify_token(&tampered);
let err = result.unwrap_err();
assert!(
matches!(err, JwtError::InvalidToken(_)),
"expected InvalidToken, got: {:?}",
err
);
}
#[rstest]
fn test_verify_malformed_token_returns_invalid_token() {
let jwt_auth = JwtAuth::new(b"test-secret-key-256bit!");
let result = jwt_auth.verify_token("not-a-jwt");
let err = result.unwrap_err();
assert!(
matches!(err, JwtError::InvalidToken(_)),
"expected InvalidToken, got: {:?}",
err
);
}
#[rstest]
fn test_verify_allow_expired_returns_claims_for_expired_token() {
let jwt_auth = JwtAuth::new(b"test-secret-key-256bit!");
let claims = Claims {
sub: "user123".to_string(),
exp: Utc::now().timestamp() - 3600, iat: Utc::now().timestamp() - 7200,
username: "alice".to_string(),
is_staff: false,
is_superuser: false,
};
let token = jwt_auth.encode(&claims).unwrap();
let result = jwt_auth.verify_token_allow_expired(&token);
let decoded = result.unwrap();
assert_eq!(decoded.sub, "user123");
assert_eq!(decoded.username, "alice");
assert!(decoded.is_expired());
}
#[rstest]
fn test_verify_allow_expired_rejects_tampered_token() {
let jwt_auth = JwtAuth::new(b"test-secret-key-256bit!");
let token = jwt_auth
.generate_token("user123".to_string(), "alice".to_string(), false, false)
.unwrap();
let tampered = format!("{}tampered", token);
let result = jwt_auth.verify_token_allow_expired(&tampered);
assert!(result.is_err());
}
#[rstest]
fn test_verify_allow_expired_rejects_wrong_secret() {
let jwt_auth_encode = JwtAuth::new(b"encoding-secret-key!!!");
let jwt_auth_decode = JwtAuth::new(b"different-secret-key!!");
let token = jwt_auth_encode
.generate_token("user123".to_string(), "alice".to_string(), false, false)
.unwrap();
let result = jwt_auth_decode.verify_token_allow_expired(&token);
assert!(result.is_err());
}
#[rstest]
fn test_verify_allow_expired_works_for_valid_token() {
let jwt_auth = JwtAuth::new(b"test-secret-key-256bit!");
let token = jwt_auth
.generate_token("user123".to_string(), "alice".to_string(), false, false)
.unwrap();
let result = jwt_auth.verify_token_allow_expired(&token);
let claims = result.unwrap();
assert_eq!(claims.sub, "user123");
assert!(!claims.is_expired());
}
#[rstest]
#[tokio::test]
async fn test_authenticate_expired_token_returns_token_expired() {
let jwt_auth = JwtAuth::new(b"test-secret-key-256bit!");
let claims = Claims {
sub: "550e8400-e29b-41d4-a716-446655440000".to_string(),
exp: Utc::now().timestamp() - 3600,
iat: Utc::now().timestamp() - 7200,
username: "alice".to_string(),
is_staff: false,
is_superuser: false,
};
let token = jwt_auth.encode(&claims).unwrap();
let request = create_request_with_bearer(&token);
let result = RestAuthentication::authenticate(&jwt_auth, &request).await;
assert!(
matches!(&result, Err(AuthenticationError::TokenExpired)),
"expected TokenExpired"
);
}
#[rstest]
fn test_jwt_error_to_auth_error_mapping() {
assert_eq!(
AuthenticationError::from(JwtError::TokenExpired),
AuthenticationError::TokenExpired
);
assert_eq!(
AuthenticationError::from(JwtError::InvalidSignature("bad sig".to_string())),
AuthenticationError::InvalidToken
);
assert_eq!(
AuthenticationError::from(JwtError::InvalidToken("bad token".to_string())),
AuthenticationError::InvalidToken
);
assert!(matches!(
AuthenticationError::from(JwtError::EncodingError("enc err".to_string())),
AuthenticationError::Unknown(_)
));
}
#[rstest]
fn test_serde_default_backward_compatibility_for_staff_fields() {
let jwt_auth = JwtAuth::new(b"test-secret-key-256bit!");
let legacy_payload = serde_json::json!({
"sub": "user123",
"exp": chrono::Utc::now().timestamp() + 3600,
"iat": chrono::Utc::now().timestamp(),
"username": "alice"
});
let header = jsonwebtoken::Header::default();
let token = jsonwebtoken::encode(
&header,
&legacy_payload,
&jsonwebtoken::EncodingKey::from_secret(b"test-secret-key-256bit!"),
)
.unwrap();
let claims = jwt_auth.decode(&token).unwrap();
assert_eq!(claims.sub, "user123");
assert_eq!(claims.username, "alice");
assert!(
!claims.is_staff,
"is_staff should default to false for legacy tokens"
);
assert!(
!claims.is_superuser,
"is_superuser should default to false for legacy tokens"
);
}
}