Skip to main content

featherbit/admin/
auth.rs

1//! HTTP Basic Auth middleware for the admin API.
2//!
3//! Guards every admin endpoint except the health probes, comparing the
4//! `Authorization: Basic` header against credentials from `AdminConfig`.
5
6use std::sync::Arc;
7
8use axum::body::Body;
9use axum::extract::State;
10use axum::http::{Request, StatusCode};
11use axum::middleware::Next;
12use axum::response::{IntoResponse, Response};
13use base64::engine::general_purpose::STANDARD;
14use base64::Engine;
15
16/// Expected Basic Auth credentials, sourced from `AdminConfig`
17/// (which in turn supports `${ENV_VAR}` interpolation).
18#[derive(Clone)]
19pub struct AuthState {
20    /// Username the client must present.
21    pub username: String,
22    /// Password the client must present.
23    pub password: String,
24}
25
26/// Axum middleware enforcing HTTP Basic Auth on admin endpoints.
27///
28/// `/healthz` and `/readyz` bypass authentication so orchestrators can probe
29/// them without credentials. Every other request must carry an
30/// `Authorization: Basic <base64(user:pass)>` header matching [`AuthState`];
31/// otherwise the middleware responds `401 Unauthorized` with a
32/// `WWW-Authenticate: Basic realm="featherbit admin"` challenge and the
33/// inner handler is never invoked.
34pub async fn basic_auth_middleware(
35    State(auth): State<Arc<AuthState>>,
36    req: Request<Body>,
37    next: Next,
38) -> Response {
39    // Skip auth for health/ready endpoints
40    let path = req.uri().path();
41    if path == "/healthz" || path == "/readyz" {
42        return next.run(req).await;
43    }
44
45    let auth_header = req
46        .headers()
47        .get("authorization")
48        .and_then(|v| v.to_str().ok());
49
50    let authorized = match auth_header {
51        Some(header) if header.starts_with("Basic ") => {
52            let decoded = STANDARD.decode(&header[6..]).unwrap_or_default();
53            let credentials = String::from_utf8(decoded).unwrap_or_default();
54            let expected = format!("{}:{}", auth.username, auth.password);
55            credentials == expected
56        }
57        _ => false,
58    };
59
60    if authorized {
61        next.run(req).await
62    } else {
63        (
64            StatusCode::UNAUTHORIZED,
65            [("www-authenticate", "Basic realm=\"featherbit admin\"")],
66            "Unauthorized",
67        )
68            .into_response()
69    }
70}