Skip to main content

rok_auth_basic/
layer.rs

1use std::{
2    future::Future,
3    pin::Pin,
4    sync::Arc,
5    task::{Context, Poll},
6};
7
8use axum::{
9    body::Body,
10    http::{header, Request, Response, StatusCode},
11};
12use base64::{engine::general_purpose::STANDARD, Engine};
13use tower::{Layer, Service};
14
15use rok_auth::Claims;
16
17// ── VerifyFn type alias ───────────────────────────────────────────────────────
18
19/// A boxed async function that maps `(username, password)` to `Option<Claims>`.
20///
21/// Return `Some(claims)` to allow the request through; `None` to reject with
22/// `401 Unauthorized`.
23pub type VerifyFn = Arc<
24    dyn Fn(String, String) -> Pin<Box<dyn Future<Output = Option<Claims>> + Send>> + Send + Sync,
25>;
26
27// ── BasicAuthLayer ────────────────────────────────────────────────────────────
28
29/// Tower [`Layer`] that enforces HTTP Basic authentication on every request.
30///
31/// On success, the verified [`Claims`] are injected into request extensions so
32/// the `Ctx` extractor in `rok-auth` can see the authenticated user without
33/// re-verifying the credentials.
34///
35/// On failure the layer short-circuits and returns `401 Unauthorized` with a
36/// `WWW-Authenticate: Basic realm="…"` challenge header.
37///
38/// # Example
39///
40/// ```rust,ignore
41/// use rok_auth::Claims;
42/// use rok_auth_basic::BasicAuthLayer;
43///
44/// let layer = BasicAuthLayer::new("My API", |user: String, pass: String| async move {
45///     if user == "admin" && pass == std::env::var("API_PASS").unwrap_or_default() {
46///         Some(Claims::new(user, vec!["admin"]))
47///     } else {
48///         None
49///     }
50/// });
51///
52/// let app = Router::new()
53///     .route("/metrics", get(metrics_handler))
54///     .layer(layer);
55/// ```
56#[derive(Clone)]
57pub struct BasicAuthLayer {
58    verifier: VerifyFn,
59    realm: String,
60}
61
62impl BasicAuthLayer {
63    /// Build a layer with a custom `realm` and async `verifier` closure.
64    ///
65    /// `verifier` receives `(username, password)` and should return
66    /// `Some(Claims)` on success or `None` to reject the request.
67    pub fn new<F, Fut>(realm: impl Into<String>, verifier: F) -> Self
68    where
69        F: Fn(String, String) -> Fut + Send + Sync + 'static,
70        Fut: Future<Output = Option<Claims>> + Send + 'static,
71    {
72        Self {
73            realm: realm.into(),
74            verifier: Arc::new(move |u, p| Box::pin(verifier(u, p))),
75        }
76    }
77}
78
79impl<Svc> Layer<Svc> for BasicAuthLayer {
80    type Service = BasicAuthService<Svc>;
81
82    fn layer(&self, inner: Svc) -> Self::Service {
83        BasicAuthService {
84            inner,
85            verifier: Arc::clone(&self.verifier),
86            realm: self.realm.clone(),
87        }
88    }
89}
90
91// ── BasicAuthService ──────────────────────────────────────────────────────────
92
93#[derive(Clone)]
94pub struct BasicAuthService<Svc> {
95    inner: Svc,
96    verifier: VerifyFn,
97    realm: String,
98}
99
100type BoxFut<T, E> = Pin<Box<dyn Future<Output = Result<T, E>> + Send>>;
101
102impl<Svc, ReqBody> Service<Request<ReqBody>> for BasicAuthService<Svc>
103where
104    Svc: Service<Request<ReqBody>, Response = Response<Body>> + Clone + Send + 'static,
105    Svc::Error: Send,
106    Svc::Future: Send,
107    ReqBody: Send + 'static,
108{
109    type Response = Response<Body>;
110    type Error = Svc::Error;
111    type Future = BoxFut<Self::Response, Self::Error>;
112
113    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
114        self.inner.poll_ready(cx)
115    }
116
117    fn call(&mut self, mut req: Request<ReqBody>) -> Self::Future {
118        let verifier = Arc::clone(&self.verifier);
119        let realm = self.realm.clone();
120        let clone = self.inner.clone();
121        let mut inner = std::mem::replace(&mut self.inner, clone);
122
123        Box::pin(async move {
124            // ── 1. Parse Authorization: Basic <base64> ─────────────────────────
125            let Some((username, password)) = parse_basic_header(req.headers()) else {
126                return Ok(deny(&realm));
127            };
128
129            // ── 2. Verify credentials ──────────────────────────────────────────
130            let Some(claims) = verifier(username, password).await else {
131                return Ok(deny(&realm));
132            };
133
134            // ── 3. Inject Claims into extensions for Ctx extractor ─────────────
135            req.extensions_mut().insert(claims);
136
137            // ── 4. Proceed to inner service ────────────────────────────────────
138            inner.call(req).await
139        })
140    }
141}
142
143// ── Helpers ───────────────────────────────────────────────────────────────────
144
145/// Parse `Authorization: Basic <base64(user:pass)>` → `(username, password)`.
146pub fn parse_basic_header(headers: &axum::http::HeaderMap) -> Option<(String, String)> {
147    let value = headers.get(header::AUTHORIZATION)?.to_str().ok()?;
148    let encoded = value.strip_prefix("Basic ")?;
149    let decoded = STANDARD.decode(encoded.trim()).ok()?;
150    let s = String::from_utf8(decoded).ok()?;
151    let mut parts = s.splitn(2, ':');
152    let username = parts.next()?.to_string();
153    let password = parts.next()?.to_string();
154    Some((username, password))
155}
156
157fn deny(realm: &str) -> Response<Body> {
158    Response::builder()
159        .status(StatusCode::UNAUTHORIZED)
160        .header(header::WWW_AUTHENTICATE, format!("Basic realm=\"{realm}\""))
161        .header(header::CONTENT_TYPE, "application/json")
162        .body(Body::from(
163            r#"{"error":"Unauthorized","message":"Basic authentication required"}"#,
164        ))
165        .unwrap()
166}