Skip to main content

alopex_server/
auth.rs

1use axum::http::HeaderMap;
2use serde::{Deserialize, Serialize};
3use tonic::metadata::MetadataMap;
4
5/// Authentication mode for the server.
6#[derive(Clone, Debug, Default, Deserialize, Serialize)]
7#[serde(tag = "type", rename_all = "snake_case")]
8pub enum AuthMode {
9    /// No authentication.
10    #[default]
11    None,
12    /// Dev API key authentication.
13    Dev { api_key: String },
14}
15
16/// Authentication error for HTTP/gRPC.
17#[derive(Debug, thiserror::Error)]
18pub enum AuthError {
19    #[error("missing credentials")]
20    Missing,
21    #[error("invalid credentials")]
22    Invalid,
23}
24
25/// Middleware helper for authentication.
26#[derive(Clone)]
27pub struct AuthMiddleware {
28    mode: AuthMode,
29}
30
31impl AuthMiddleware {
32    /// Create a new auth middleware.
33    pub fn new(mode: AuthMode) -> Self {
34        Self { mode }
35    }
36
37    /// Validate HTTP headers and return actor identity (if any).
38    pub fn validate_http(&self, headers: &HeaderMap) -> Result<Option<String>, AuthError> {
39        match &self.mode {
40            AuthMode::None => Ok(None),
41            AuthMode::Dev { api_key } => {
42                let provided = extract_api_key(headers);
43                if provided.as_deref() == Some(api_key.as_str()) {
44                    Ok(Some("dev".to_string()))
45                } else if provided.is_none() {
46                    Err(AuthError::Missing)
47                } else {
48                    Err(AuthError::Invalid)
49                }
50            }
51        }
52    }
53
54    /// Validate gRPC metadata and return actor identity (if any).
55    pub fn validate_grpc(&self, metadata: &MetadataMap) -> Result<Option<String>, AuthError> {
56        match &self.mode {
57            AuthMode::None => Ok(None),
58            AuthMode::Dev { api_key } => {
59                let provided = extract_api_key_from_metadata(metadata);
60                if provided.as_deref() == Some(api_key.as_str()) {
61                    Ok(Some("dev".to_string()))
62                } else if provided.is_none() {
63                    Err(AuthError::Missing)
64                } else {
65                    Err(AuthError::Invalid)
66                }
67            }
68        }
69    }
70
71    pub fn mode(&self) -> &AuthMode {
72        &self.mode
73    }
74}
75
76fn extract_api_key(headers: &HeaderMap) -> Option<String> {
77    if let Some(value) = headers.get("x-api-key").and_then(|v| v.to_str().ok()) {
78        return Some(value.to_string());
79    }
80    headers
81        .get(axum::http::header::AUTHORIZATION)
82        .and_then(|v| v.to_str().ok())
83        .and_then(|raw| raw.strip_prefix("Bearer "))
84        .map(|v| v.to_string())
85}
86
87fn extract_api_key_from_metadata(metadata: &MetadataMap) -> Option<String> {
88    if let Some(value) = metadata.get("x-api-key").and_then(|v| v.to_str().ok()) {
89        return Some(value.to_string());
90    }
91    metadata
92        .get("authorization")
93        .and_then(|v| v.to_str().ok())
94        .and_then(|raw| raw.strip_prefix("Bearer "))
95        .map(|v| v.to_string())
96}