#[cfg(feature = "jwt")]
pub mod jwt;
#[cfg(all(feature = "keto", feature = "axum"))]
pub mod keto;
#[cfg(feature = "keto")]
pub mod keto_proto;
#[cfg(feature = "axum")]
pub mod introspection;
#[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};
#[cfg(feature = "axum")]
pub use introspection::{IntrospectionClient, IntrospectionLayer, IntrospectionResponse};
#[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 email: Option<String>,
pub name: Option<String>,
pub roles: Vec<String>,
pub scope: Option<String>,
pub client_id: Option<String>,
pub exp: Option<i64>,
pub claims: Option<JwtClaims>,
pub is_authenticated: bool,
}
impl AuthContext {
pub fn unauthenticated() -> Self {
Self {
subject: None,
email: None,
name: None,
roles: Vec::new(),
scope: None,
client_id: None,
exp: None,
claims: None,
is_authenticated: false,
}
}
pub fn authenticated(subject: impl Into<String>, claims: Option<JwtClaims>) -> Self {
Self {
subject: Some(subject.into()),
email: None,
name: None,
roles: Vec::new(),
scope: None,
client_id: None,
exp: None,
claims,
is_authenticated: true,
}
}
pub fn is_authenticated(&self) -> bool {
self.is_authenticated
}
pub fn subject(&self) -> Option<&String> {
self.subject.as_ref()
}
pub fn email(&self) -> Option<&String> {
self.email.as_ref()
}
pub fn name(&self) -> Option<&String> {
self.name.as_ref()
}
pub fn roles(&self) -> &[String] {
&self.roles
}
pub fn scope(&self) -> Option<&String> {
self.scope.as_ref()
}
pub fn client_id(&self) -> Option<&String> {
self.client_id.as_ref()
}
pub fn with_email(mut self, email: impl Into<String>) -> Self {
self.email = Some(email.into());
self
}
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn with_roles(mut self, roles: Vec<String>) -> Self {
self.roles = roles;
self
}
pub fn with_scope(mut self, scope: impl Into<String>) -> Self {
self.scope = Some(scope.into());
self
}
pub fn with_client_id(mut self, client_id: impl Into<String>) -> Self {
self.client_id = Some(client_id.into());
self
}
pub fn with_exp(mut self, exp: i64) -> Self {
self.exp = Some(exp);
self
}
}
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);
}
}