use axum::Router;
use axum::body::Body;
use axum::extract::{Request, State};
use axum::http::{HeaderMap, StatusCode, header};
use axum::middleware::{self, Next};
use axum::response::{IntoResponse, Response};
use base64::Engine;
use md5::{Digest as _, Md5};
#[derive(Clone)]
pub(crate) enum MockAuthScheme {
Basic { username: String, password: String },
Digest {
username: String,
password: String,
realm: String,
nonce: String,
},
Bearer { token: String },
}
impl MockAuthScheme {
fn challenge(&self) -> String {
match self {
MockAuthScheme::Basic { .. } => "Basic realm=\"mock\"".to_string(),
MockAuthScheme::Digest { realm, nonce, .. } => {
format!("Digest realm=\"{realm}\", nonce=\"{nonce}\", qop=\"auth\", algorithm=MD5")
}
MockAuthScheme::Bearer { .. } => "Bearer realm=\"mock\"".to_string(),
}
}
fn check(&self, headers: &HeaderMap, method: &str) -> bool {
let Some(auth) = headers
.get(header::AUTHORIZATION)
.and_then(|v| v.to_str().ok())
else {
return false;
};
match self {
MockAuthScheme::Basic { username, password } => {
let expected = format!(
"Basic {}",
base64::engine::general_purpose::STANDARD
.encode(format!("{username}:{password}"))
);
auth == expected
}
MockAuthScheme::Bearer { token } => auth == format!("Bearer {token}"),
MockAuthScheme::Digest {
username,
password,
realm,
nonce,
} => verify_digest(auth, username, password, realm, nonce, method),
}
}
}
fn verify_digest(
auth_header: &str,
expected_username: &str,
password: &str,
expected_realm: &str,
expected_nonce: &str,
method: &str,
) -> bool {
let Some(rest) = auth_header.strip_prefix("Digest ") else {
return false;
};
let mut fields = std::collections::HashMap::new();
for part in rest.split(',') {
let part = part.trim();
let Some((key, value)) = part.split_once('=') else {
continue;
};
fields.insert(key.trim(), value.trim().trim_matches('"'));
}
let get = |k: &str| fields.get(k).copied().unwrap_or_default();
if get("username") != expected_username
|| get("realm") != expected_realm
|| get("nonce") != expected_nonce
{
return false;
}
let uri = get("uri");
let nc = get("nc");
let cnonce = get("cnonce");
let qop = get("qop");
let client_response = get("response");
if uri.is_empty() || nc.is_empty() || cnonce.is_empty() || client_response.is_empty() {
return false;
}
let ha1 = md5_hex(format!("{expected_username}:{expected_realm}:{password}"));
let ha2 = md5_hex(format!("{method}:{uri}"));
let expected_response = md5_hex(format!("{ha1}:{expected_nonce}:{nc}:{cnonce}:{qop}:{ha2}"));
expected_response == client_response
}
fn md5_hex(input: String) -> String {
let mut hasher = Md5::new();
hasher.update(input.as_bytes());
let digest = hasher.finalize();
digest.iter().map(|b| format!("{b:02x}")).collect()
}
pub(crate) fn require_auth(router: Router, scheme: MockAuthScheme) -> Router {
router.layer(middleware::from_fn_with_state(scheme, auth_gate))
}
async fn auth_gate(State(scheme): State<MockAuthScheme>, req: Request, next: Next) -> Response {
let method = req.method().as_str().to_string();
if scheme.check(req.headers(), &method) {
return next.run(req).await;
}
let challenge = scheme.challenge();
(
StatusCode::UNAUTHORIZED,
[(header::WWW_AUTHENTICATE, challenge)],
Body::empty(),
)
.into_response()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn verify_digest_accepts_hand_computed_response() {
let username = "admin";
let password = "12345";
let realm = "mock realm";
let nonce = "abc123nonce";
let method = "GET";
let uri = "http://127.0.0.1:9/stream.ts";
let nc = "00000001";
let cnonce = "clientnonce";
let qop = "auth";
let ha1 = md5_hex(format!("{username}:{realm}:{password}"));
let ha2 = md5_hex(format!("{method}:{uri}"));
let response = md5_hex(format!("{ha1}:{nonce}:{nc}:{cnonce}:{qop}:{ha2}"));
let header = format!(
"Digest username=\"{username}\", realm=\"{realm}\", nonce=\"{nonce}\", \
uri=\"{uri}\", qop={qop}, nc={nc}, cnonce=\"{cnonce}\", response=\"{response}\""
);
assert!(verify_digest(
&header, username, password, realm, nonce, method
));
}
#[test]
fn verify_digest_rejects_wrong_password() {
let username = "admin";
let realm = "mock realm";
let nonce = "abc123nonce";
let method = "GET";
let uri = "http://127.0.0.1:9/stream.ts";
let nc = "00000001";
let cnonce = "clientnonce";
let qop = "auth";
let ha1 = md5_hex(format!("{username}:{realm}:wrongpass"));
let ha2 = md5_hex(format!("{method}:{uri}"));
let response = md5_hex(format!("{ha1}:{nonce}:{nc}:{cnonce}:{qop}:{ha2}"));
let header = format!(
"Digest username=\"{username}\", realm=\"{realm}\", nonce=\"{nonce}\", \
uri=\"{uri}\", qop={qop}, nc={nc}, cnonce=\"{cnonce}\", response=\"{response}\""
);
assert!(!verify_digest(
&header, username, "12345", realm, nonce, method
));
}
}