use axum::http::StatusCode;
use std::sync::Arc;
#[derive(Clone, Default)]
pub struct AuthConfig {
pub(super) keys: Arc<Vec<String>>,
pub(super) jwt: Option<JwtConfig>,
}
impl AuthConfig {
pub fn new(keys: Vec<String>) -> Self {
Self {
keys: Arc::new(keys),
jwt: None,
}
}
pub fn with_jwt(mut self, jwt: JwtConfig) -> Self {
self.jwt = Some(jwt);
self
}
pub fn is_valid(&self, presented: &str) -> bool {
if self.keys.is_empty() {
return true;
}
let mut found = false;
let presented_bytes = presented.as_bytes();
for k in self.keys.iter() {
let k_bytes = k.as_bytes();
if k_bytes.len() != presented_bytes.len() {
continue;
}
let mut diff: u8 = 0;
for (a, b) in k_bytes.iter().zip(presented_bytes.iter()) {
diff |= a ^ b;
}
if diff == 0 {
found = true;
}
}
found
}
pub fn is_enabled(&self) -> bool {
!self.keys.is_empty() || self.jwt.is_some()
}
}
#[derive(Clone)]
pub struct JwtConfig {
decoding_key: jsonwebtoken::DecodingKey,
validation: jsonwebtoken::Validation,
}
impl JwtConfig {
pub fn hs256(secret: &str, audience: Option<String>) -> Option<Self> {
if secret.is_empty() {
return None;
}
let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS256);
validation.set_required_spec_claims(&["exp"]);
if let Some(aud) = audience {
validation.set_audience(&[aud]);
} else {
validation.validate_aud = false;
}
Some(Self {
decoding_key: jsonwebtoken::DecodingKey::from_secret(secret.as_bytes()),
validation,
})
}
pub fn is_valid(&self, token: &str) -> bool {
jsonwebtoken::decode::<serde_json::Value>(token, &self.decoding_key, &self.validation)
.is_ok()
}
}
pub(super) fn auth_config_from_env() -> AuthConfig {
let raw = std::env::var("RECURSIVE_HTTP_AUTH_KEYS").unwrap_or_default();
let keys: Vec<String> = raw
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
let mut config = AuthConfig::new(keys);
let jwt_secret = std::env::var("RECURSIVE_HTTP_AUTH_JWT_SECRET").unwrap_or_default();
let jwt_audience = std::env::var("RECURSIVE_HTTP_AUTH_JWT_AUDIENCE")
.ok()
.filter(|s| !s.is_empty());
if let Some(jwt) = JwtConfig::hs256(&jwt_secret, jwt_audience) {
config = config.with_jwt(jwt);
}
config
}
pub(super) async fn auth_middleware(
axum::extract::State(auth): axum::extract::State<AuthConfig>,
req: axum::extract::Request,
next: axum::middleware::Next,
) -> axum::response::Response {
if !auth.is_enabled() {
return next.run(req).await;
}
let path = req.uri().path();
if path == "/health" || path == "/metrics" {
return next.run(req).await;
}
if !auth.keys.is_empty() {
if let Some(presented) = req.headers().get("x-api-key").and_then(|v| v.to_str().ok()) {
if auth.is_valid(presented) {
return next.run(req).await;
}
}
}
if let Some(ref jwt) = auth.jwt {
if let Some(authz) = req
.headers()
.get("authorization")
.and_then(|v| v.to_str().ok())
{
if let Some(token) = authz.strip_prefix("Bearer ") {
if jwt.is_valid(token) {
return next.run(req).await;
}
}
}
}
let mut resp = axum::response::Response::new(axum::body::Body::from("unauthorized"));
*resp.status_mut() = StatusCode::UNAUTHORIZED;
resp
}