mod jwt;
mod claims;
pub use jwt::validate_token;
pub use claims::Claims;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AuthResult {
pub role: String,
pub claims: HashMap<String, serde_json::Value>,
}
impl AuthResult {
pub fn anonymous(anon_role: &str) -> Self {
Self {
role: anon_role.to_string(),
claims: HashMap::new(),
}
}
pub fn get_claim(&self, key: &str) -> Option<&serde_json::Value> {
self.claims.get(key)
}
pub fn claims_json(&self) -> String {
serde_json::to_string(&self.claims).unwrap_or_else(|_| "{}".to_string())
}
}
#[derive(Clone, Debug)]
pub struct JwtConfig {
pub secret: Option<String>,
pub secret_is_base64: bool,
pub audience: Option<String>,
pub role_claim_key: String,
pub anon_role: Option<String>,
}
impl Default for JwtConfig {
fn default() -> Self {
Self {
secret: None,
secret_is_base64: false,
audience: None,
role_claim_key: "role".to_string(),
anon_role: None,
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum JwtError {
#[error("Missing authorization header")]
MissingHeader,
#[error("Invalid authorization header format")]
InvalidHeaderFormat,
#[error("Token expired")]
Expired,
#[error("Token not yet valid")]
NotYetValid,
#[error("Invalid signature")]
InvalidSignature,
#[error("Invalid token: {0}")]
InvalidToken(String),
#[error("Missing role claim")]
MissingRole,
#[error("Invalid audience")]
InvalidAudience,
}
pub fn authenticate(
auth_header: Option<&str>,
config: &JwtConfig,
) -> Result<AuthResult, JwtError> {
let token = match auth_header {
Some(header) => extract_bearer_token(header)?,
None => {
return match &config.anon_role {
Some(role) => Ok(AuthResult::anonymous(role)),
None => Err(JwtError::MissingHeader),
};
}
};
validate_token(token, config)
}
fn extract_bearer_token(header: &str) -> Result<&str, JwtError> {
let header = header.trim();
if let Some(token) = header.strip_prefix("Bearer ") {
Ok(token.trim())
} else if let Some(token) = header.strip_prefix("bearer ") {
Ok(token.trim())
} else {
Err(JwtError::InvalidHeaderFormat)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_bearer_token() {
assert_eq!(
extract_bearer_token("Bearer abc123").unwrap(),
"abc123"
);
assert_eq!(
extract_bearer_token("bearer abc123").unwrap(),
"abc123"
);
assert!(extract_bearer_token("Basic abc123").is_err());
}
#[test]
fn test_auth_result_anonymous() {
let result = AuthResult::anonymous("anon");
assert_eq!(result.role, "anon");
assert!(result.claims.is_empty());
}
#[test]
fn test_authenticate_no_header_with_anon() {
let config = JwtConfig {
anon_role: Some("web_anon".to_string()),
..Default::default()
};
let result = authenticate(None, &config).unwrap();
assert_eq!(result.role, "web_anon");
}
#[test]
fn test_authenticate_no_header_no_anon() {
let config = JwtConfig::default();
assert!(authenticate(None, &config).is_err());
}
}