use serde::{Deserialize, Serialize};
use std::time::Duration;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthConfig {
pub provider: AuthProviderType,
pub token_expiration: Duration,
pub allow_anonymous: bool,
pub required_claims: Vec<String>,
}
impl Default for AuthConfig {
fn default() -> Self {
Self {
provider: AuthProviderType::Jwt {
secret: String::new(),
issuer: None,
audience: None,
},
token_expiration: Duration::from_secs(3600),
allow_anonymous: true,
required_claims: Vec::new(),
}
}
}
impl AuthConfig {
pub fn jwt(secret: impl Into<String>) -> Self {
Self {
provider: AuthProviderType::Jwt {
secret: secret.into(),
issuer: None,
audience: None,
},
..Default::default()
}
}
pub fn with_expiration(mut self, duration: Duration) -> Self {
self.token_expiration = duration;
self
}
pub fn require_auth(mut self) -> Self {
self.allow_anonymous = false;
self
}
pub fn require_claim(mut self, claim: impl Into<String>) -> Self {
self.required_claims.push(claim.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum AuthProviderType {
Jwt {
secret: String,
issuer: Option<String>,
audience: Option<String>,
},
OAuth {
discovery_url: String,
client_id: String,
},
ApiKey {
header: String,
},
None,
}
pub trait AuthProvider: Send + Sync {
fn validate(&self, token: &str) -> Result<TokenClaims, super::SecurityError>;
fn generate_token(&self, claims: &TokenClaims) -> Result<String, super::SecurityError>;
}
#[derive(Debug, Clone)]
pub struct AuthToken {
pub raw: String,
pub claims: TokenClaims,
}
impl AuthToken {
pub fn new(raw: impl Into<String>, claims: TokenClaims) -> Self {
Self {
raw: raw.into(),
claims,
}
}
pub fn user_id(&self) -> Option<&str> {
self.claims.sub.as_deref()
}
pub fn tenant_id(&self) -> Option<&str> {
self.claims.tenant_id.as_deref()
}
pub fn is_expired(&self) -> bool {
if let Some(exp) = self.claims.exp {
chrono::Utc::now().timestamp() as u64 > exp
} else {
false
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenClaims {
pub sub: Option<String>,
pub iss: Option<String>,
pub aud: Option<String>,
pub exp: Option<u64>,
pub iat: Option<u64>,
pub nbf: Option<u64>,
pub jti: Option<String>,
pub tenant_id: Option<String>,
pub roles: Vec<String>,
pub permissions: Vec<String>,
#[serde(flatten)]
pub extra: std::collections::HashMap<String, serde_json::Value>,
}
impl Default for TokenClaims {
fn default() -> Self {
Self {
sub: None,
iss: None,
aud: None,
exp: None,
iat: Some(chrono::Utc::now().timestamp() as u64),
nbf: None,
jti: None,
tenant_id: None,
roles: Vec::new(),
permissions: Vec::new(),
extra: std::collections::HashMap::new(),
}
}
}
impl TokenClaims {
pub fn for_user(user_id: impl Into<String>) -> Self {
Self {
sub: Some(user_id.into()),
..Default::default()
}
}
pub fn expires_in(mut self, duration: Duration) -> Self {
let now = chrono::Utc::now().timestamp() as u64;
self.exp = Some(now + duration.as_secs());
self
}
pub fn for_tenant(mut self, tenant_id: impl Into<String>) -> Self {
self.tenant_id = Some(tenant_id.into());
self
}
pub fn with_role(mut self, role: impl Into<String>) -> Self {
self.roles.push(role.into());
self
}
pub fn with_permission(mut self, permission: impl Into<String>) -> Self {
self.permissions.push(permission.into());
self
}
pub fn with_claim(mut self, key: impl Into<String>, value: impl Serialize) -> Self {
if let Ok(json_value) = serde_json::to_value(value) {
self.extra.insert(key.into(), json_value);
}
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_auth_config() {
let config = AuthConfig::jwt("my-secret")
.with_expiration(Duration::from_secs(7200))
.require_auth();
assert!(!config.allow_anonymous);
assert_eq!(config.token_expiration, Duration::from_secs(7200));
}
#[test]
fn test_token_claims() {
let claims = TokenClaims::for_user("user-123")
.for_tenant("tenant-456")
.with_role("admin")
.expires_in(Duration::from_secs(3600));
assert_eq!(claims.sub.as_deref(), Some("user-123"));
assert_eq!(claims.tenant_id.as_deref(), Some("tenant-456"));
assert!(claims.roles.contains(&"admin".to_string()));
assert!(claims.exp.is_some());
}
#[test]
fn test_auth_token() {
let claims = TokenClaims::for_user("user-123");
let token = AuthToken::new("raw-token-string", claims);
assert_eq!(token.user_id(), Some("user-123"));
assert!(!token.is_expired());
}
}