1pub mod hash;
7
8use base64::{Engine, engine::general_purpose::STANDARD};
9use std::sync::{Arc, LazyLock};
10use tokio::sync::Semaphore;
11
12static AUTH_SLOTS: LazyLock<Arc<Semaphore>> = LazyLock::new(|| Arc::new(Semaphore::new(4)));
14
15#[derive(Debug)]
16pub(crate) struct AuthCheckUnavailable;
17
18pub(crate) async fn check_basic_auth_bounded(
20 auth_header: Option<&str>,
21 credentials: &Credentials,
22) -> Result<bool, AuthCheckUnavailable> {
23 check_basic_auth_with_slots(auth_header, credentials, &AUTH_SLOTS).await
24}
25
26async fn check_basic_auth_with_slots(
27 auth_header: Option<&str>,
28 credentials: &Credentials,
29 slots: &Arc<Semaphore>,
30) -> Result<bool, AuthCheckUnavailable> {
31 if credentials.is_empty() {
32 return Ok(true);
33 }
34 let Some(header) = auth_header.filter(|h| h.starts_with("Basic ")) else {
35 return Ok(false);
36 };
37 let permit = slots
38 .clone()
39 .try_acquire_owned()
40 .map_err(|_| AuthCheckUnavailable)?;
41 let credentials = credentials.clone();
42 let header = header.to_owned();
43 run_auth_check(permit, move || credentials.verify(&header)).await
44}
45
46async fn run_auth_check(
47 permit: tokio::sync::OwnedSemaphorePermit,
48 check: impl FnOnce() -> bool + Send + 'static,
49) -> Result<bool, AuthCheckUnavailable> {
50 tokio::task::spawn_blocking(move || {
51 let _permit = permit;
54 check()
55 })
56 .await
57 .map_err(|_| AuthCheckUnavailable)
58}
59
60#[derive(Debug, Clone)]
62pub struct Credential {
63 username: String,
64 password: String,
65}
66
67impl Credential {
68 pub fn new(username: String, password: String) -> Self {
70 Self { username, password }
71 }
72
73 pub fn parse(value: &str) -> Option<Self> {
79 let (user, pass) = value.split_once(':')?;
80 if user.is_empty() {
81 return None;
82 }
83 Some(Self::new(user.to_string(), pass.to_string()))
84 }
85
86 pub fn username(&self) -> &str {
88 &self.username
89 }
90
91 pub fn password(&self) -> &str {
93 &self.password
94 }
95}
96
97#[derive(Debug, Clone)]
99pub struct Credentials {
100 entries: Vec<Credential>,
101}
102
103impl Credentials {
104 pub fn new() -> Self {
106 Self {
107 entries: Vec::new(),
108 }
109 }
110
111 pub fn from_entries(entries: Vec<Credential>) -> Self {
113 Self { entries }
114 }
115
116 pub fn is_empty(&self) -> bool {
118 self.entries.is_empty()
119 }
120
121 pub fn len(&self) -> usize {
123 self.entries.len()
124 }
125
126 pub fn verify(&self, auth_header: &str) -> bool {
131 let Some(encoded) = auth_header.strip_prefix("Basic ") else {
132 return false;
133 };
134
135 let Ok(decoded) = STANDARD.decode(encoded.trim()) else {
136 return false;
137 };
138
139 let Ok(decoded_str) = String::from_utf8(decoded) else {
140 return false;
141 };
142
143 let Some((user, password)) = decoded_str.split_once(':') else {
145 return false;
146 };
147
148 self.check(user, password)
149 }
150
151 fn check(&self, username: &str, password: &str) -> bool {
154 let mut found = false;
155 for cred in &self.entries {
156 let user_match = hash::constant_time_eq(cred.username.as_bytes(), username.as_bytes());
158 let pass_match = hash::verify(password, &cred.password);
160 if user_match && pass_match {
161 found = true;
162 }
163 }
164 found
165 }
166}
167
168impl Default for Credentials {
169 fn default() -> Self {
170 Self::new()
171 }
172}
173
174pub fn check_basic_auth(auth_header: Option<&str>, credentials: &Credentials) -> bool {
184 if credentials.is_empty() {
185 return true;
187 }
188
189 match auth_header {
190 Some(header) => credentials.verify(header),
191 None => false,
192 }
193}
194
195pub fn check_bearer_token(auth_header: Option<&str>, expected_token: Option<&str>) -> bool {
205 let Some(expected) = expected_token else {
206 return true;
208 };
209
210 if expected.is_empty() {
211 return true;
213 }
214
215 let Some(header) = auth_header else {
216 return false;
217 };
218
219 let Some(token) = header
221 .get(..7)
222 .filter(|prefix| prefix.eq_ignore_ascii_case("bearer "))
223 .map(|_| &header[7..])
224 else {
225 return false;
226 };
227
228 hash::constant_time_eq(token.trim().as_bytes(), expected.as_bytes())
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235
236 #[tokio::test]
237 async fn saturated_auth_rejects_without_waiting() {
238 use futures::FutureExt;
239
240 let slots = Arc::new(Semaphore::new(1));
241 let _permit = slots.clone().try_acquire_owned().unwrap();
242 let credentials = Credentials::from_entries(vec![Credential::new(
243 "admin".to_string(),
244 "secret".to_string(),
245 )]);
246 let header = format!("Basic {}", STANDARD.encode("admin:secret"));
247 assert!(
248 check_basic_auth_with_slots(Some(&header), &credentials, &slots)
249 .now_or_never()
250 .expect("saturated authentication must not queue")
251 .is_err()
252 );
253 }
254
255 #[tokio::test(flavor = "current_thread")]
256 async fn blocking_auth_keeps_its_slot_after_request_cancellation() {
257 let slots = Arc::new(Semaphore::new(1));
258 let permit = slots.clone().try_acquire_owned().unwrap();
259 let (started_tx, started_rx) = tokio::sync::oneshot::channel();
260 let (finish_tx, finish_rx) = std::sync::mpsc::channel();
261 let task = tokio::spawn(run_auth_check(permit, move || {
262 started_tx.send(()).unwrap();
263 finish_rx
265 .recv_timeout(std::time::Duration::from_secs(2))
266 .unwrap();
267 true
268 }));
269
270 started_rx.await.unwrap();
272 task.abort();
273 assert!(task.await.unwrap_err().is_cancelled());
274 assert!(slots.clone().try_acquire_owned().is_err());
275 finish_tx.send(()).unwrap();
276 let released =
277 tokio::time::timeout(std::time::Duration::from_secs(2), slots.acquire_owned())
278 .await
279 .expect("slot must be released when hashing finishes")
280 .unwrap();
281 drop(released);
282 }
283
284 #[tokio::test]
285 async fn bounded_basic_auth_preserves_valid_and_invalid_credentials() {
286 let credentials = Credentials::from_entries(vec![Credential::new(
287 "admin".to_string(),
288 "secret".to_string(),
289 )]);
290 let valid = format!("Basic {}", STANDARD.encode("admin:secret"));
291 let invalid = format!("Basic {}", STANDARD.encode("admin:wrong"));
292 assert!(
293 check_basic_auth_bounded(Some(&valid), &credentials)
294 .await
295 .unwrap()
296 );
297 assert!(
298 !check_basic_auth_bounded(Some(&invalid), &credentials)
299 .await
300 .unwrap()
301 );
302 assert!(!check_basic_auth_bounded(None, &credentials).await.unwrap());
303 }
304
305 #[test]
306 fn test_credential_parse_valid() {
307 let cred = Credential::parse("admin:secret").unwrap();
308 assert_eq!(cred.username(), "admin");
309 assert_eq!(cred.password(), "secret");
310 }
311
312 #[test]
313 fn test_credential_parse_with_colon_in_password() {
314 let cred = Credential::parse("admin:sec:ret").unwrap();
315 assert_eq!(cred.username(), "admin");
316 assert_eq!(cred.password(), "sec:ret");
317 }
318
319 #[test]
320 fn test_credential_parse_no_colon() {
321 assert!(Credential::parse("invalid").is_none());
322 }
323
324 #[test]
325 fn test_credential_parse_empty_user() {
326 assert!(Credential::parse(":password").is_none());
327 }
328
329 #[test]
330 fn test_credentials_empty() {
331 let creds = Credentials::new();
332 assert!(creds.is_empty());
333 assert_eq!(creds.len(), 0);
334 }
335
336 #[test]
337 fn test_credentials_from_entries() {
338 let entries = vec![
339 Credential::new("admin".to_string(), "secret".to_string()),
340 Credential::new("user".to_string(), "pass".to_string()),
341 ];
342 let creds = Credentials::from_entries(entries);
343 assert!(!creds.is_empty());
344 assert_eq!(creds.len(), 2);
345 }
346
347 #[test]
348 fn test_verify_plain_text() {
349 let creds = Credentials::from_entries(vec![Credential::new(
350 "admin".to_string(),
351 "secret".to_string(),
352 )]);
353
354 let header = format!("Basic {}", STANDARD.encode("admin:secret"));
355 assert!(creds.verify(&header));
356 }
357
358 #[test]
359 fn test_verify_wrong_password() {
360 let creds = Credentials::from_entries(vec![Credential::new(
361 "admin".to_string(),
362 "secret".to_string(),
363 )]);
364
365 let header = format!("Basic {}", STANDARD.encode("admin:wrong"));
366 assert!(!creds.verify(&header));
367 }
368
369 #[test]
370 fn test_verify_wrong_user() {
371 let creds = Credentials::from_entries(vec![Credential::new(
372 "admin".to_string(),
373 "secret".to_string(),
374 )]);
375
376 let header = format!("Basic {}", STANDARD.encode("wrong:secret"));
377 assert!(!creds.verify(&header));
378 }
379
380 #[test]
381 fn test_verify_bcrypt() {
382 let hash = "$2y$05$bvIG6Nmid91Mu9RcmmWZfO5HJIMCT8riNW0hEp8f6/FuA2/mHZFpe";
384 let creds =
385 Credentials::from_entries(vec![Credential::new("user".to_string(), hash.to_string())]);
386
387 let header = format!("Basic {}", STANDARD.encode("user:password"));
388 assert!(creds.verify(&header));
389 }
390
391 #[test]
392 fn test_verify_sha1() {
393 let hash = "{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=";
395 let creds =
396 Credentials::from_entries(vec![Credential::new("user".to_string(), hash.to_string())]);
397
398 let header = format!("Basic {}", STANDARD.encode("user:password"));
399 assert!(creds.verify(&header));
400 }
401
402 #[test]
403 fn test_verify_apr1() {
404 let hash = "$apr1$lZL6V/ci$eIMz/iKDkbtys/uU7LEK00";
406 let creds =
407 Credentials::from_entries(vec![Credential::new("user".to_string(), hash.to_string())]);
408
409 let header = format!("Basic {}", STANDARD.encode("user:password"));
410 assert!(creds.verify(&header));
411 }
412
413 #[test]
414 fn test_verify_multiple_credentials() {
415 let creds = Credentials::from_entries(vec![
416 Credential::new("admin".to_string(), "admin123".to_string()),
417 Credential::new("user1".to_string(), "pass1".to_string()),
418 Credential::new("user2".to_string(), "pass2".to_string()),
419 ]);
420
421 assert!(creds.verify(&format!("Basic {}", STANDARD.encode("admin:admin123"))));
422 assert!(creds.verify(&format!("Basic {}", STANDARD.encode("user1:pass1"))));
423 assert!(creds.verify(&format!("Basic {}", STANDARD.encode("user2:pass2"))));
424 assert!(!creds.verify(&format!("Basic {}", STANDARD.encode("unknown:pass"))));
425 }
426
427 #[test]
428 fn test_verify_invalid_base64() {
429 let creds = Credentials::from_entries(vec![Credential::new(
430 "admin".to_string(),
431 "secret".to_string(),
432 )]);
433 assert!(!creds.verify("Basic not-valid-base64!!!"));
434 }
435
436 #[test]
437 fn test_verify_non_basic_auth() {
438 let creds = Credentials::from_entries(vec![Credential::new(
439 "admin".to_string(),
440 "secret".to_string(),
441 )]);
442 assert!(!creds.verify("Bearer some-token"));
443 assert!(!creds.verify("bearer some-token"));
444 }
445
446 #[test]
447 fn test_verify_missing_colon_in_decoded() {
448 let creds = Credentials::from_entries(vec![Credential::new(
449 "admin".to_string(),
450 "secret".to_string(),
451 )]);
452 let header = format!("Basic {}", STANDARD.encode("no-colon-here"));
453 assert!(!creds.verify(&header));
454 }
455
456 #[test]
457 fn test_check_basic_auth_no_credentials() {
458 let creds = Credentials::new();
459 assert!(check_basic_auth(None, &creds));
461 assert!(check_basic_auth(Some("Basic anything"), &creds));
462 }
463
464 #[test]
465 fn test_check_basic_auth_with_credentials() {
466 let creds = Credentials::from_entries(vec![Credential::new(
467 "admin".to_string(),
468 "secret".to_string(),
469 )]);
470
471 assert!(!check_basic_auth(None, &creds));
473
474 let valid_header = format!("Basic {}", STANDARD.encode("admin:secret"));
476 assert!(check_basic_auth(Some(&valid_header), &creds));
477
478 let invalid_header = format!("Basic {}", STANDARD.encode("admin:wrong"));
480 assert!(!check_basic_auth(Some(&invalid_header), &creds));
481 }
482
483 #[test]
488 fn test_check_bearer_token_no_token_configured() {
489 assert!(check_bearer_token(None, None));
491 assert!(check_bearer_token(Some("Bearer anything"), None));
492 }
493
494 #[test]
495 fn test_check_bearer_token_empty_token() {
496 assert!(check_bearer_token(None, Some("")));
498 assert!(check_bearer_token(Some("Bearer anything"), Some("")));
499 }
500
501 #[test]
502 fn test_check_bearer_token_valid() {
503 let token = "my-secret-token";
504 let header = "Bearer my-secret-token";
505 assert!(check_bearer_token(Some(header), Some(token)));
506 }
507
508 #[test]
509 fn test_check_bearer_token_valid_with_whitespace() {
510 let token = "my-secret-token";
511 let header = "Bearer my-secret-token ";
512 assert!(check_bearer_token(Some(header), Some(token)));
513 }
514
515 #[test]
516 fn test_check_bearer_token_invalid() {
517 let token = "my-secret-token";
518 let header = "Bearer wrong-token";
519 assert!(!check_bearer_token(Some(header), Some(token)));
520 }
521
522 #[test]
523 fn test_check_bearer_token_no_header() {
524 let token = "my-secret-token";
525 assert!(!check_bearer_token(None, Some(token)));
526 }
527
528 #[test]
529 fn test_check_bearer_token_basic_auth_header() {
530 let token = "my-secret-token";
532 let header = format!("Basic {}", STANDARD.encode("user:pass"));
533 assert!(!check_bearer_token(Some(&header), Some(token)));
534 }
535
536 #[test]
537 fn test_check_bearer_token_value_case_sensitive() {
538 let token = "MySecretToken";
539 assert!(check_bearer_token(
540 Some("Bearer MySecretToken"),
541 Some(token)
542 ));
543 assert!(!check_bearer_token(
544 Some("Bearer mysecrettoken"),
545 Some(token)
546 ));
547 }
548
549 #[test]
550 fn test_check_bearer_token_prefix_case_insensitive() {
551 let token = "my-secret-token";
553 assert!(check_bearer_token(
554 Some("bearer my-secret-token"),
555 Some(token)
556 ));
557 assert!(check_bearer_token(
558 Some("BEARER my-secret-token"),
559 Some(token)
560 ));
561 assert!(check_bearer_token(
562 Some("Bearer my-secret-token"),
563 Some(token)
564 ));
565 }
566}