1use crate::error::Result;
39use crate::handler::BoxFuture;
40use crate::middleware::{Middleware, Next};
41use crate::request::Request;
42use crate::response::Response;
43use crate::security::constant_time_eq;
44use async_trait::async_trait;
45use std::sync::Arc;
46
47#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct Principal(pub String);
50
51impl Request {
52 pub fn principal(&self) -> Option<&str> {
54 self.extensions.get::<Principal>().map(|p| p.0.as_str())
55 }
56}
57
58fn unauthorized(challenge: &str, message: &str) -> Response {
59 Response::from_error(crate::error::Error::unauthorized(message))
60 .with_header("www-authenticate", challenge)
61 .with_header("cache-control", "no-store")
62}
63
64fn exempt(paths: &[String], req: &Request) -> bool {
65 paths.iter().any(|p| p == req.uri.path())
66}
67
68#[derive(Clone)]
75pub struct ApiKeyAuth {
76 header: String,
77 keys: Vec<(String, String)>,
78 bearer: bool,
79 except: Vec<String>,
80}
81
82impl Default for ApiKeyAuth {
83 fn default() -> Self {
84 Self::new()
85 }
86}
87
88impl ApiKeyAuth {
89 pub fn new() -> Self {
91 Self {
92 header: "x-api-key".into(),
93 keys: Vec::new(),
94 bearer: false,
95 except: Vec::new(),
96 }
97 }
98
99 pub fn header(mut self, name: &str) -> Self {
101 self.header = name.to_ascii_lowercase();
102 self
103 }
104
105 pub fn allow_bearer(mut self) -> Self {
107 self.bearer = true;
108 self
109 }
110
111 pub fn key(mut self, name: &str, secret: &str) -> Self {
113 self.keys.push((name.to_string(), secret.to_string()));
114 self
115 }
116
117 pub fn except_paths<I, S>(mut self, paths: I) -> Self
119 where
120 I: IntoIterator<Item = S>,
121 S: Into<String>,
122 {
123 self.except = paths.into_iter().map(Into::into).collect();
124 self
125 }
126
127 fn lookup(&self, presented: &str) -> Option<&str> {
128 let mut found = None;
130 for (name, secret) in &self.keys {
131 if constant_time_eq(secret.as_bytes(), presented.as_bytes()) && found.is_none() {
132 found = Some(name.as_str());
133 }
134 }
135 found
136 }
137}
138
139#[async_trait]
140impl Middleware for ApiKeyAuth {
141 async fn handle(&self, mut req: Request, next: Next) -> Result<Response> {
142 if exempt(&self.except, &req) {
143 return next.run(req).await;
144 }
145 let presented = req
146 .headers
147 .get(self.header.as_str())
148 .and_then(|v| v.to_str().ok())
149 .map(str::to_string)
150 .or_else(|| {
151 self.bearer
152 .then(|| {
153 req.headers
154 .get(http::header::AUTHORIZATION)
155 .and_then(|v| v.to_str().ok())
156 .and_then(|v| v.strip_prefix("Bearer "))
157 .map(str::to_string)
158 })
159 .flatten()
160 });
161 let challenge = if self.bearer { "Bearer" } else { "ApiKey" };
162 let Some(presented) = presented else {
163 return Ok(unauthorized(challenge, "API key required"));
164 };
165 match self.lookup(&presented) {
166 Some(name) => {
167 req.extensions.insert(Principal(name.to_string()));
168 next.run(req).await
169 }
170 None => Ok(unauthorized(challenge, "Invalid API key")),
171 }
172 }
173}
174
175type Verifier = dyn Fn(String, String) -> BoxFuture<'static, bool> + Send + Sync;
178
179#[derive(Clone)]
181pub struct BasicAuth {
182 realm: String,
183 users: Vec<(String, String)>,
184 verifier: Option<Arc<Verifier>>,
185 except: Vec<String>,
186}
187
188impl BasicAuth {
189 pub fn new(realm: &str) -> Self {
191 Self {
192 realm: realm.replace(['"', '\\', '\r', '\n'], ""),
193 users: Vec::new(),
194 verifier: None,
195 except: Vec::new(),
196 }
197 }
198
199 pub fn user(mut self, name: &str, password: &str) -> Self {
201 self.users.push((name.to_string(), password.to_string()));
202 self
203 }
204
205 pub fn verify_with<F, Fut>(mut self, f: F) -> Self
208 where
209 F: Fn(String, String) -> Fut + Send + Sync + 'static,
210 Fut: std::future::Future<Output = bool> + Send + 'static,
211 {
212 self.verifier = Some(Arc::new(move |user, pass| Box::pin(f(user, pass))));
213 self
214 }
215
216 pub fn except_paths<I, S>(mut self, paths: I) -> Self
218 where
219 I: IntoIterator<Item = S>,
220 S: Into<String>,
221 {
222 self.except = paths.into_iter().map(Into::into).collect();
223 self
224 }
225
226 fn challenge(&self) -> String {
227 format!("Basic realm=\"{}\", charset=\"UTF-8\"", self.realm)
228 }
229
230 fn check_builtin(&self, user: &str, password: &str) -> bool {
231 let mut ok = false;
233 for (name, secret) in &self.users {
234 let name_ok = constant_time_eq(name.as_bytes(), user.as_bytes());
235 let pass_ok = constant_time_eq(secret.as_bytes(), password.as_bytes());
236 ok |= name_ok & pass_ok;
237 }
238 ok
239 }
240}
241
242fn base64_decode(input: &str) -> Option<Vec<u8>> {
244 let mut out = Vec::with_capacity(input.len() * 3 / 4);
245 let (mut buf, mut bits) = (0u32, 0u32);
246 for c in input.trim_end_matches('=').bytes() {
247 let v = match c {
248 b'A'..=b'Z' => c - b'A',
249 b'a'..=b'z' => c - b'a' + 26,
250 b'0'..=b'9' => c - b'0' + 52,
251 b'+' | b'-' => 62,
252 b'/' | b'_' => 63,
253 _ => return None,
254 };
255 buf = (buf << 6) | u32::from(v);
256 bits += 6;
257 if bits >= 8 {
258 bits -= 8;
259 out.push((buf >> bits) as u8);
260 buf &= (1 << bits) - 1;
261 }
262 }
263 Some(out)
264}
265
266#[async_trait]
267impl Middleware for BasicAuth {
268 async fn handle(&self, mut req: Request, next: Next) -> Result<Response> {
269 if exempt(&self.except, &req) {
270 return next.run(req).await;
271 }
272 let credentials = req
273 .headers
274 .get(http::header::AUTHORIZATION)
275 .and_then(|v| v.to_str().ok())
276 .and_then(|v| {
277 let (scheme, rest) = v.split_once(' ')?;
278 scheme.eq_ignore_ascii_case("basic").then_some(rest.trim())
279 })
280 .and_then(base64_decode)
281 .and_then(|raw| String::from_utf8(raw).ok())
282 .and_then(|text| {
283 let (user, pass) = text.split_once(':')?;
284 Some((user.to_string(), pass.to_string()))
285 });
286 let Some((user, pass)) = credentials else {
287 return Ok(unauthorized(&self.challenge(), "Credentials required"));
288 };
289 let ok = match &self.verifier {
290 Some(verify) => verify(user.clone(), pass).await,
291 None => self.check_builtin(&user, &pass),
292 };
293 if !ok {
294 return Ok(unauthorized(&self.challenge(), "Invalid credentials"));
295 }
296 req.extensions.insert(Principal(user));
297 next.run(req).await
298 }
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304
305 #[test]
306 fn base64_decoding() {
307 assert_eq!(base64_decode("dXNlcjpwYXNz").unwrap(), b"user:pass");
308 assert_eq!(base64_decode("dXNlcjpwYXNzMQ==").unwrap(), b"user:pass1");
309 assert_eq!(base64_decode("dXNlcjpwYXNzMQ").unwrap(), b"user:pass1");
310 assert_eq!(base64_decode("_-8").unwrap(), vec![0xff, 0xef]);
312 assert_eq!(base64_decode("").unwrap(), Vec::<u8>::new());
313 assert!(base64_decode("not base64!").is_none());
314 }
315
316 #[test]
317 fn realm_cannot_break_the_header() {
318 let auth = BasicAuth::new("a\"b\r\nX-Evil: 1");
319 assert_eq!(
320 auth.challenge(),
321 "Basic realm=\"abX-Evil: 1\", charset=\"UTF-8\""
322 );
323 }
324
325 #[test]
326 fn api_key_lookup_returns_the_owner() {
327 let a = ApiKeyAuth::new().key("a", "secret-a").key("b", "secret-b");
328 assert_eq!(a.lookup("secret-b"), Some("b"));
329 assert_eq!(a.lookup("secret-"), None);
330 assert_eq!(a.lookup(""), None);
331 }
332}