#[cfg(feature = "jwt")]
pub mod jwt;
#[cfg(all(feature = "keto", feature = "axum"))]
pub mod keto;
#[cfg(feature = "keto")]
pub mod keto_proto;
#[cfg(feature = "jwt")]
pub use jwt::{JwtLayer, JwtValidator, extract_jwt_claims};
#[cfg(all(feature = "keto", feature = "axum"))]
pub use keto::{KetoClient, KetoConfig, KetoLayer, Permission, RelationTuple, Subject};
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct JwtClaims {
pub sub: String,
pub iat: i64,
pub exp: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub iss: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub aud: Option<String>,
#[serde(flatten)]
pub extra: std::collections::HashMap<String, serde_json::Value>,
}
impl JwtClaims {
pub fn get(&self, key: &str) -> Option<&serde_json::Value> {
self.extra.get(key)
}
pub fn is_expired(&self) -> bool {
let now = chrono::Utc::now().timestamp();
self.exp < now
}
}
#[derive(Debug, Clone, Default)]
pub enum AuthStrategy {
#[cfg(feature = "jwt")]
Jwt(JwtValidator),
#[cfg(all(feature = "keto", feature = "axum"))]
Keto(KetoClient),
#[default]
None,
}
#[derive(Debug, Clone)]
pub struct AuthContext {
pub subject: Option<String>,
pub claims: Option<JwtClaims>,
pub is_authenticated: bool,
}
impl AuthContext {
pub fn unauthenticated() -> Self {
Self {
subject: None,
claims: None,
is_authenticated: false,
}
}
pub fn authenticated(subject: impl Into<String>, claims: Option<JwtClaims>) -> Self {
Self {
subject: Some(subject.into()),
claims,
is_authenticated: true,
}
}
pub fn is_authenticated(&self) -> bool {
self.is_authenticated
}
pub fn subject(&self) -> Option<&String> {
self.subject.as_ref()
}
}
impl Default for AuthContext {
fn default() -> Self {
Self::unauthenticated()
}
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct AuthLayer {
strategy: AuthStrategy,
}
impl AuthLayer {
pub fn new(strategy: AuthStrategy) -> Self {
Self { strategy }
}
#[cfg(feature = "jwt")]
pub fn jwt(validator: jwt::JwtValidator) -> Self {
Self::new(AuthStrategy::Jwt(validator))
}
#[cfg(all(feature = "keto", feature = "axum"))]
pub fn keto(client: keto::KetoClient) -> Self {
Self::new(AuthStrategy::Keto(client))
}
pub fn none() -> Self {
Self::new(AuthStrategy::None)
}
}
pub fn extract_subject(headers: &http::HeaderMap) -> Option<String> {
headers
.get("Authorization")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.strip_prefix("Bearer ").map(str::to_string))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_auth_context_unauthenticated() {
let ctx = AuthContext::unauthenticated();
assert!(!ctx.is_authenticated());
assert!(ctx.subject().is_none());
}
#[test]
fn test_auth_context_authenticated() {
let ctx = AuthContext::authenticated("user-1", None);
assert!(ctx.is_authenticated());
assert_eq!(ctx.subject(), Some(&"user-1".to_string()));
}
#[test]
fn test_auth_layer_new() {
let layer = AuthLayer::none();
match layer.strategy {
AuthStrategy::None => {}
#[allow(unreachable_patterns)]
_ => panic!("Expected None strategy"),
}
}
#[test]
fn test_extract_subject_bearer() {
let mut headers = http::HeaderMap::new();
headers.insert(
"Authorization",
http::HeaderValue::from_static("Bearer mytoken"),
);
assert_eq!(extract_subject(&headers), Some("mytoken".to_string()));
}
#[test]
fn test_extract_subject_non_bearer() {
let mut headers = http::HeaderMap::new();
headers.insert(
"Authorization",
http::HeaderValue::from_static("Basic credentials"),
);
assert_eq!(extract_subject(&headers), None);
}
#[test]
fn test_extract_subject_missing() {
let headers = http::HeaderMap::new();
assert_eq!(extract_subject(&headers), None);
}
}