use std::sync::Arc;
use axum::extract::Request;
use axum::http::{StatusCode, header};
use axum::middleware::Next;
use axum::response::{IntoResponse, Response};
use koan_core::auth::{self, Role};
use super::AuthUser;
#[derive(Clone)]
pub struct AuthState {
pub public_pem: Arc<Vec<u8>>,
pub auth_enabled: bool,
pub introspection_key: Option<Arc<String>>,
}
pub async fn auth_middleware(
axum::extract::State(state): axum::extract::State<AuthState>,
mut request: Request,
next: Next,
) -> Response {
if !state.auth_enabled {
request.extensions_mut().insert(AuthUser::anonymous_admin());
return next.run(request).await;
}
if let Some(ref expected_key) = state.introspection_key
&& let Some(provided) = request
.headers()
.get("X-Introspection-Key")
.and_then(|v| v.to_str().ok())
&& provided == expected_key.as_str()
{
request.extensions_mut().insert(AuthUser::anonymous_admin());
return next.run(request).await;
}
let token = request
.headers()
.get(axum::http::header::COOKIE)
.and_then(|v| v.to_str().ok())
.and_then(|cookies| {
cookies
.split(';')
.find_map(|c| c.trim().strip_prefix("koan_access=").map(String::from))
})
.or_else(|| {
request
.headers()
.get(header::AUTHORIZATION)
.and_then(|v| v.to_str().ok())
.and_then(|v| v.strip_prefix("Bearer "))
.map(String::from)
})
.or_else(|| {
request.uri().query().and_then(|q| {
q.split('&')
.find_map(|pair| pair.strip_prefix("token=").map(String::from))
})
});
let Some(token) = token else {
return (
StatusCode::UNAUTHORIZED,
[("WWW-Authenticate", "Bearer")],
"missing or invalid Authorization header",
)
.into_response();
};
match auth::validate_access_token(&state.public_pem, &token) {
Ok(claims) => {
let role = claims.role.parse().unwrap_or(Role::Readonly);
let user = AuthUser {
user_id: claims.sub,
username: claims.username,
role,
};
request.extensions_mut().insert(user);
next.run(request).await
}
Err(_) => (
StatusCode::UNAUTHORIZED,
[("WWW-Authenticate", "Bearer")],
"invalid or expired token",
)
.into_response(),
}
}