mockforge_http/auth/
admin_auth.rs1use axum::body::Body;
6use axum::http::header::HeaderValue;
7use axum::http::{Request, StatusCode};
8use axum::response::Response;
9use base64::{engine::general_purpose, Engine as _};
10use subtle::ConstantTimeEq;
11use tracing::{debug, warn};
12
13fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
18 a.ct_eq(b).into()
19}
20
21fn verify_admin_password(supplied: &str, configured: &str) -> bool {
29 if configured.starts_with("$2") {
30 match bcrypt::verify(supplied, configured) {
31 Ok(ok) => ok,
32 Err(e) => {
33 warn!("Malformed bcrypt hash in admin password config: {}", e);
34 false
35 }
36 }
37 } else {
38 constant_time_eq(supplied.as_bytes(), configured.as_bytes())
39 }
40}
41
42pub fn check_admin_auth(
44 req: &Request<Body>,
45 admin_auth_required: bool,
46 admin_username: &Option<String>,
47 admin_password: &Option<String>,
48) -> Result<(), Response> {
49 if !admin_auth_required {
51 debug!("Admin auth not required, allowing access");
52 return Ok(());
53 }
54
55 let auth_header = req.headers().get("authorization").and_then(|h| h.to_str().ok());
57
58 if let Some(auth_value) = auth_header {
59 if let Some(basic_creds) = auth_value.strip_prefix("Basic ") {
61 match general_purpose::STANDARD.decode(basic_creds) {
63 Ok(decoded) => {
64 if let Ok(creds_str) = String::from_utf8(decoded) {
65 if let Some((username, password)) = creds_str.split_once(':') {
67 if let (Some(expected_user), Some(expected_pass)) =
69 (admin_username, admin_password)
70 {
71 let user_ok =
75 constant_time_eq(username.as_bytes(), expected_user.as_bytes());
76 if user_ok && verify_admin_password(password, expected_pass) {
77 debug!("Admin authentication successful");
78 return Ok(());
79 }
80 }
81 }
82 }
83 }
84 Err(e) => {
85 warn!("Failed to decode admin credentials: {}", e);
86 }
87 }
88 }
89 }
90
91 warn!("Admin authentication failed or missing");
93 let mut res = Response::new(Body::from(
94 serde_json::json!({
95 "error": "Authentication required",
96 "message": "Admin UI requires authentication"
97 })
98 .to_string(),
99 ));
100 *res.status_mut() = StatusCode::UNAUTHORIZED;
101 res.headers_mut()
102 .insert("www-authenticate", HeaderValue::from_static("Basic realm=\"MockForge Admin\""));
103
104 Err(res)
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110 use axum::http::Request;
111
112 #[test]
113 fn test_admin_auth_not_required() {
114 let req = Request::builder().body(Body::empty()).unwrap();
115 assert!(check_admin_auth(&req, false, &None, &None).is_ok());
116 }
117
118 #[test]
119 fn test_admin_auth_missing() {
120 let req = Request::builder().body(Body::empty()).unwrap();
121 let username = Some("admin".to_string());
122 let password = Some("secret".to_string());
123 assert!(check_admin_auth(&req, true, &username, &password).is_err());
124 }
125
126 #[test]
127 fn test_admin_auth_valid() {
128 let username = Some("admin".to_string());
129 let password = Some("secret".to_string());
130
131 let credentials = general_purpose::STANDARD.encode("admin:secret");
133 let auth_value = format!("Basic {}", credentials);
134
135 let req = Request::builder()
136 .header("authorization", auth_value)
137 .body(Body::empty())
138 .unwrap();
139
140 assert!(check_admin_auth(&req, true, &username, &password).is_ok());
141 }
142
143 #[test]
144 fn test_admin_auth_invalid_password() {
145 let username = Some("admin".to_string());
146 let password = Some("secret".to_string());
147
148 let credentials = general_purpose::STANDARD.encode("admin:wrong");
150 let auth_value = format!("Basic {}", credentials);
151
152 let req = Request::builder()
153 .header("authorization", auth_value)
154 .body(Body::empty())
155 .unwrap();
156
157 assert!(check_admin_auth(&req, true, &username, &password).is_err());
158 }
159
160 #[test]
161 fn test_admin_auth_bcrypt_hashed_password() {
162 let username = Some("admin".to_string());
163 let hashed = bcrypt::hash("secret", bcrypt::DEFAULT_COST).unwrap();
165 let password = Some(hashed);
166
167 let credentials = general_purpose::STANDARD.encode("admin:secret");
168 let req = Request::builder()
169 .header("authorization", format!("Basic {}", credentials))
170 .body(Body::empty())
171 .unwrap();
172 assert!(check_admin_auth(&req, true, &username, &password).is_ok());
173
174 let wrong = general_purpose::STANDARD.encode("admin:wrong");
176 let req = Request::builder()
177 .header("authorization", format!("Basic {}", wrong))
178 .body(Body::empty())
179 .unwrap();
180 assert!(check_admin_auth(&req, true, &username, &password).is_err());
181 }
182}