Skip to main content

faucet_cli/serve/
auth.rs

1//! Bearer-token authentication for `/v1/*`. Constant-time comparison via
2//! `subtle`; the `Authorization` header is the only accepted credential.
3
4use crate::serve::error::ServeError;
5use crate::serve::state::ServerState;
6use axum::extract::{Request, State};
7use axum::middleware::Next;
8use axum::response::Response;
9use subtle::ConstantTimeEq;
10
11/// Timing-safe byte-slice equality. Differing lengths return `false` after a
12/// constant-time length check (subtle short-circuits unequal lengths — this
13/// leaks only the token *length*, never its content); equal-length inputs are
14/// compared byte-by-byte with no early exit on the first mismatch.
15pub(crate) fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
16    a.ct_eq(b).into()
17}
18
19/// Validate a raw `Authorization` header value against the expected token.
20pub fn authorize_header(header: Option<&str>, expected: &str) -> Result<(), ServeError> {
21    let value = header.ok_or(ServeError::Unauthorized)?;
22    let token = value
23        .strip_prefix("Bearer ")
24        .ok_or(ServeError::Unauthorized)?;
25    if constant_time_eq(token.as_bytes(), expected.as_bytes()) {
26        Ok(())
27    } else {
28        Err(ServeError::Unauthorized)
29    }
30}
31
32/// Axum middleware enforcing bearer auth on `/v1/*`. A no-op when the server was
33/// started with `--no-auth`. CORS preflight (`OPTIONS`) is allowed through so
34/// browsers (which omit `Authorization` on preflight) work behind a CORS policy.
35pub async fn require_auth(
36    State(state): State<ServerState>,
37    req: Request,
38    next: Next,
39) -> Result<Response, ServeError> {
40    if req.method() == axum::http::Method::OPTIONS {
41        return Ok(next.run(req).await);
42    }
43    if let Some(expected) = state.auth_token() {
44        let header = req
45            .headers()
46            .get(axum::http::header::AUTHORIZATION)
47            .and_then(|v| v.to_str().ok());
48        authorize_header(header, expected)?;
49    }
50    Ok(next.run(req).await)
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56
57    #[test]
58    fn constant_time_eq_matches_only_identical() {
59        assert!(constant_time_eq(b"abc123", b"abc123"));
60        assert!(!constant_time_eq(b"abc123", b"abc124"));
61        assert!(!constant_time_eq(b"abc", b"abc123")); // length differs
62    }
63
64    #[test]
65    fn authorize_accepts_correct_bearer() {
66        assert!(authorize_header(Some("Bearer s3cret"), "s3cret").is_ok());
67    }
68
69    #[test]
70    fn authorize_rejects_wrong_or_missing() {
71        assert!(authorize_header(Some("Bearer nope"), "s3cret").is_err());
72        assert!(authorize_header(None, "s3cret").is_err());
73        assert!(authorize_header(Some("s3cret"), "s3cret").is_err()); // no "Bearer " prefix
74        assert!(authorize_header(Some("Basic s3cret"), "s3cret").is_err());
75    }
76}