116 lines
3.2 KiB
Rust
116 lines
3.2 KiB
Rust
/// Authentication provider trait.
|
|||
|
|
///
|
||
|
|
/// Enables pluggable authentication backends (Authentik, custom RBAC, Keycloak, etc).
|
||
|
|
/// Implementations must validate tokens and extract claims.
|
||
|
|
///
|
||
|
|
/// # Minimal Design
|
||
|
|
/// Single method: validate_token() returns raw claims JSON.
|
||
|
|
/// Memory service extracts what it needs (groups, resources, etc).
|
||
|
|
/// This works with ANY JSON structure.
|
||
|
|
|
||
|
|
use async_trait::async_trait;
|
||
|
|
use serde_json::Value;
|
||
|
|
|
||
|
|
/// Standard token claims format.
|
||
|
|
#[derive(Clone, Debug)]
|
||
|
|
pub struct Claims {
|
||
|
|
/// Subject (user/service ID)
|
||
|
|
pub sub: String,
|
||
|
|
|
||
|
|
/// Groups/roles user belongs to
|
||
|
|
pub groups: Vec<String>,
|
||
|
|
|
||
|
|
/// Custom attributes (memory_resources, etc)
|
||
|
|
pub attributes: serde_json::Map<String, Value>,
|
||
|
|
|
||
|
|
/// Expiration timestamp (Unix seconds)
|
||
|
|
pub exp: i64,
|
||
|
|
|
||
|
|
/// Issued at timestamp (Unix seconds)
|
||
|
|
pub iat: i64,
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Authentication provider trait.
|
||
|
|
///
|
||
|
|
/// Implement this trait for any OIDC/OAuth2 provider or custom auth system.
|
||
|
|
#[async_trait]
|
||
|
|
pub trait AuthProvider: Send + Sync {
|
||
|
|
/// Validate token and extract claims.
|
||
|
|
///
|
||
|
|
/// Implementation should:
|
||
|
|
/// 1. Verify JWT signature (using JWKS or shared key)
|
||
|
|
/// 2. Check expiration
|
||
|
|
/// 3. Validate issuer and audience
|
||
|
|
/// 4. Extract claims into standard Claims format
|
||
|
|
///
|
||
|
|
/// # Errors
|
||
|
|
/// Returns error if token is invalid, expired, or verification fails.
|
||
|
|
async fn validate_token(&self, token: &str) -> Result<Claims, AuthError>;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Authentication errors.
|
||
|
|
#[derive(Clone, Debug)]
|
||
|
|
pub enum AuthError {
|
||
|
|
/// Token is missing or malformed
|
||
|
|
MissingToken,
|
||
|
|
|
||
|
|
/// JWT signature verification failed
|
||
|
|
InvalidSignature,
|
||
|
|
|
||
|
|
/// Token has expired
|
||
|
|
TokenExpired,
|
||
|
|
|
||
|
|
/// Issuer claim doesn't match configured issuer
|
||
|
|
InvalidIssuer,
|
||
|
|
|
||
|
|
/// Audience claim doesn't match configured audience
|
||
|
|
InvalidAudience,
|
||
|
|
|
||
|
|
/// Can't reach OIDC provider
|
||
|
|
ProviderUnavailable(String),
|
||
|
|
|
||
|
|
/// Other error
|
||
|
|
Other(String),
|
||
|
|
}
|
||
|
|
|
||
|
|
impl std::fmt::Display for AuthError {
|
||
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||
|
|
match self {
|
||
|
|
AuthError::MissingToken => write!(f, "Missing token"),
|
||
|
|
AuthError::InvalidSignature => write!(f, "Invalid signature"),
|
||
|
|
AuthError::TokenExpired => write!(f, "Token expired"),
|
||
|
|
AuthError::InvalidIssuer => write!(f, "Invalid issuer"),
|
||
|
|
AuthError::InvalidAudience => write!(f, "Invalid audience"),
|
||
|
|
AuthError::ProviderUnavailable(e) => write!(f, "Provider unavailable: {}", e),
|
||
|
|
AuthError::Other(e) => write!(f, "{}", e),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl std::error::Error for AuthError {}
|
||
|
|
|
||
|
|
#[cfg(test)]
|
||
|
|
mod tests {
|
||
|
|
use super::*;
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_auth_error_display() {
|
||
|
|
let err = AuthError::TokenExpired;
|
||
|
|
assert_eq!(err.to_string(), "Token expired");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_claims_structure() {
|
||
|
|
let claims = Claims {
|
||
|
|
sub: "rock".to_string(),
|
||
|
|
groups: vec!["memory-users".to_string()],
|
||
|
|
attributes: serde_json::Map::new(),
|
||
|
|
exp: 1735689600,
|
||
|
|
iat: 1735689300,
|
||
|
|
};
|
||
|
|
|
||
|
|
assert_eq!(claims.sub, "rock");
|
||
|
|
assert_eq!(claims.groups.len(), 1);
|
||
|
|
}
|
||
|
|
}
|