use axum::http::{HeaderMap, header};
use sha2::{Digest, Sha256};
use subtle::ConstantTimeEq;
use crate::config::Config;
use crate::problem::Problem;
use crate::problems::SLUGS;
#[must_use]
pub fn bearer_token(headers: &HeaderMap) -> Option<&str> {
let value = headers.get(header::AUTHORIZATION)?.to_str().ok()?;
let rest = value
.strip_prefix("Bearer ")
.or_else(|| value.strip_prefix("bearer "))?;
if rest.is_empty() { None } else { Some(rest) }
}
fn sha256(bytes: &[u8]) -> [u8; 32] {
let digest = Sha256::digest(bytes);
let mut out = [0u8; 32];
out.copy_from_slice(&digest);
out
}
#[must_use]
pub fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
bool::from(sha256(a).ct_eq(&sha256(b)))
}
pub fn require_admin(config: &dyn Config, headers: &HeaderMap) -> Result<(), Problem> {
let Some(configured) = config.get("ADMIN_TOKEN").filter(|token| !token.is_empty()) else {
return Err(Problem::new(&SLUGS.admin_unauthorized));
};
let Some(presented) = bearer_token(headers) else {
return Err(Problem::new(&SLUGS.admin_unauthorized));
};
if constant_time_eq(presented.as_bytes(), configured.as_bytes()) {
Ok(())
} else {
Err(Problem::new(&SLUGS.admin_forbidden))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::MapConfig;
fn headers(token: Option<&str>) -> HeaderMap {
let mut map = HeaderMap::new();
if let Some(token) = token {
map.insert(
header::AUTHORIZATION,
header::HeaderValue::from_str(&format!("Bearer {token}")).expect("header builds"),
);
}
map
}
fn config(token: Option<&str>) -> MapConfig {
MapConfig::from_pairs(token.map(|t| ("ADMIN_TOKEN", t)))
}
const DUMMY: &str = "test-admin-token-0123456789abcdef";
#[test]
fn unset_token_disables_admin() {
let err = require_admin(&config(None), &headers(Some(DUMMY))).expect_err("disabled");
assert_eq!(err.status, axum::http::StatusCode::UNAUTHORIZED);
assert_eq!(err.slug, "admin-unauthorized");
}
#[test]
fn missing_header_is_unauthorized() {
let err = require_admin(&config(Some(DUMMY)), &headers(None)).expect_err("no header");
assert_eq!(err.status, axum::http::StatusCode::UNAUTHORIZED);
}
#[test]
fn wrong_token_is_forbidden() {
let err =
require_admin(&config(Some(DUMMY)), &headers(Some("wrong"))).expect_err("wrong token");
assert_eq!(err.status, axum::http::StatusCode::FORBIDDEN);
assert_eq!(err.slug, "admin-forbidden");
}
#[test]
fn correct_token_passes() {
require_admin(&config(Some(DUMMY)), &headers(Some(DUMMY))).expect("authorized");
}
#[test]
fn empty_bearer_is_unauthorized() {
let mut map = HeaderMap::new();
map.insert(
header::AUTHORIZATION,
header::HeaderValue::from_static("Bearer "),
);
let err = require_admin(&config(Some(DUMMY)), &map).expect_err("empty bearer");
assert_eq!(err.status, axum::http::StatusCode::UNAUTHORIZED);
}
#[test]
fn constant_time_eq_matches_and_rejects() {
assert!(constant_time_eq(b"abcdef", b"abcdef"));
assert!(!constant_time_eq(b"abcdef", b"abcdef-longer"));
assert!(!constant_time_eq(b"abcdef", b"abcdeg"));
}
}