1use crate::access::{Principal, UserIdentity};
5use async_trait::async_trait;
6use base64::Engine as _;
7use http::header::AUTHORIZATION;
8use http::{HeaderMap, HeaderValue};
9use std::sync::Arc;
10use subtle::ConstantTimeEq;
11
12pub fn verify_bearer_token(provided: &str, expected: &str) -> bool {
21 if provided.len() != expected.len() {
22 return false;
23 }
24 if expected.is_empty() {
25 return false;
26 }
27 provided.as_bytes().ct_eq(expected.as_bytes()).into()
28}
29
30pub fn extract_bearer_from_header(value: &str) -> Option<&str> {
38 let (scheme, rest) = value.split_once(' ')?;
39 if !scheme.eq_ignore_ascii_case("bearer") {
40 return None;
41 }
42 if rest.is_empty() || rest.starts_with(' ') {
43 return None;
44 }
45 Some(rest)
46}
47
48pub fn verify_basic_credentials(
53 provided_username: &str,
54 provided_password: &str,
55 expected_username: &str,
56 expected_password: &str,
57) -> bool {
58 if provided_username.is_empty()
59 || provided_password.is_empty()
60 || expected_username.is_empty()
61 || expected_password.is_empty()
62 {
63 return false;
64 }
65
66 if provided_username.len() != expected_username.len()
67 || provided_password.len() != expected_password.len()
68 {
69 return false;
70 }
71
72 let username_match = provided_username
73 .as_bytes()
74 .ct_eq(expected_username.as_bytes());
75 let password_match = provided_password
76 .as_bytes()
77 .ct_eq(expected_password.as_bytes());
78 let both_match = username_match & password_match;
79 bool::from(both_match)
80}
81
82pub fn extract_basic_from_header(value: &str) -> Option<(String, String)> {
88 let (scheme, rest) = value.split_once(' ')?;
89 if !scheme.eq_ignore_ascii_case("basic") {
90 return None;
91 }
92 if rest.starts_with(' ') {
93 return None;
94 }
95
96 let decoded = base64::engine::general_purpose::STANDARD
97 .decode(rest)
98 .ok()?;
99 let decoded_str = String::from_utf8(decoded).ok()?;
100 let (username, password) = decoded_str.split_once(':')?;
101 if username.is_empty() || password.is_empty() {
102 return None;
103 }
104 Some((username.to_string(), password.to_string()))
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub struct CredentialRefused;
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117pub enum Schemes {
118 Bearer,
120 BasicOrBearer,
122}
123
124#[derive(Debug, Clone, PartialEq, Eq)]
129pub struct TokenRejected {
130 pub reason: String,
132}
133
134impl TokenRejected {
135 pub fn new(reason: impl Into<String>) -> Self {
137 Self {
138 reason: reason.into(),
139 }
140 }
141}
142
143#[async_trait]
149pub trait TokenVerifier: Send + Sync {
150 async fn verify(&self, token: &str) -> Result<UserIdentity, TokenRejected>;
152}
153
154#[derive(Debug, Clone, PartialEq, Eq)]
160pub struct ProtectedResource {
161 pub resource: String,
163 pub authorization_servers: Vec<String>,
165 pub metadata_url: String,
167}
168
169impl ProtectedResource {
170 pub fn document(&self) -> serde_json::Value {
172 serde_json::json!({
173 "resource": self.resource,
174 "authorization_servers": self.authorization_servers,
175 "bearer_methods_supported": ["header"],
176 })
177 }
178}
179
180pub struct Authenticator {
188 service_token: String,
189 basic: Option<(String, String)>,
190 verifier: Option<Arc<dyn TokenVerifier>>,
191 protected_resource: Option<ProtectedResource>,
192}
193
194impl std::fmt::Debug for Authenticator {
195 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196 f.debug_struct("Authenticator")
197 .field("service_token", &"<redacted>")
198 .field(
199 "basic",
200 &self.basic.as_ref().map(|(user, _)| (user, "<redacted>")),
201 )
202 .field("verifier", &self.verifier.as_ref().map(|_| "<configured>"))
203 .field("protected_resource", &self.protected_resource)
204 .finish()
205 }
206}
207
208impl Authenticator {
209 pub fn new(service_token: impl Into<String>) -> Self {
211 Self {
212 service_token: service_token.into(),
213 basic: None,
214 verifier: None,
215 protected_resource: None,
216 }
217 }
218
219 #[must_use]
221 pub fn with_basic(mut self, username: impl Into<String>, password: impl Into<String>) -> Self {
222 self.basic = Some((username.into(), password.into()));
223 self
224 }
225
226 #[must_use]
228 pub fn with_token_verifier(mut self, verifier: Arc<dyn TokenVerifier>) -> Self {
229 self.verifier = Some(verifier);
230 self
231 }
232
233 #[must_use]
235 pub fn with_protected_resource(mut self, resource: ProtectedResource) -> Self {
236 self.protected_resource = Some(resource);
237 self
238 }
239
240 pub fn accepts_identity_tokens(&self) -> bool {
242 self.verifier.is_some()
243 }
244
245 pub fn protected_resource(&self) -> Option<&ProtectedResource> {
247 self.protected_resource.as_ref()
248 }
249
250 pub fn bearer_challenge(&self) -> Option<HeaderValue> {
256 let resource = self.protected_resource.as_ref()?;
257 HeaderValue::from_str(&format!(
258 "Bearer resource_metadata=\"{}\"",
259 resource.metadata_url
260 ))
261 .ok()
262 }
263
264 pub async fn resolve(
271 &self,
272 headers: &HeaderMap,
273 accept: Schemes,
274 ) -> Result<Principal, CredentialRefused> {
275 let mut values = headers.get_all(AUTHORIZATION).iter();
276 let Some(header) = values.next() else {
277 return Ok(Principal::Anyone);
278 };
279 if values.next().is_some() {
280 return Err(CredentialRefused);
281 }
282 let header = header.to_str().map_err(|_| CredentialRefused)?;
283
284 if let Some(token) = extract_bearer_from_header(header) {
285 return self.resolve_bearer(token).await;
286 }
287 if accept == Schemes::BasicOrBearer
288 && let Some((username, password)) = extract_basic_from_header(header)
289 && let Some((expected_user, expected_pass)) = &self.basic
290 && verify_basic_credentials(&username, &password, expected_user, expected_pass)
291 {
292 return Ok(Principal::service_token());
293 }
294 Err(CredentialRefused)
295 }
296
297 async fn resolve_bearer(&self, token: &str) -> Result<Principal, CredentialRefused> {
298 if verify_bearer_token(token, &self.service_token) {
302 return Ok(Principal::service_token());
303 }
304 let Some(verifier) = &self.verifier else {
305 return Err(CredentialRefused);
306 };
307 match verifier.verify(token).await {
308 Ok(identity) => Ok(Principal::SignedIn(crate::access::Identity::User(identity))),
309 Err(rejected) => {
310 tracing::debug!(reason = %rejected.reason, "bearer token rejected");
311 Err(CredentialRefused)
312 }
313 }
314 }
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320
321 #[test]
322 fn test_verify_bearer_token_matching() {
323 assert!(verify_bearer_token("secret-token", "secret-token"));
324 }
325
326 #[test]
327 fn test_verify_bearer_token_different_same_length() {
328 assert!(!verify_bearer_token("aaaabbbb", "aaaacccc"));
329 }
330
331 #[test]
332 fn test_verify_bearer_token_empty_provided() {
333 assert!(!verify_bearer_token("", "some-token"));
334 }
335
336 #[test]
337 fn test_verify_bearer_token_empty_expected() {
338 assert!(!verify_bearer_token("some-token", ""));
339 }
340
341 #[test]
342 fn test_verify_bearer_token_different_lengths() {
343 assert!(!verify_bearer_token("short", "much-longer-token"));
344 }
345
346 #[test]
347 fn test_verify_bearer_token_case_sensitive() {
348 assert!(!verify_bearer_token("AbC", "abc"));
349 }
350
351 #[test]
352 fn test_verify_bearer_token_whitespace_sensitive() {
353 assert!(!verify_bearer_token("tok", "tok "));
354 }
355
356 #[test]
357 fn test_extract_bearer_lowercase_scheme() {
358 assert_eq!(extract_bearer_from_header("Bearer abc123"), Some("abc123"));
359 }
360
361 #[test]
362 fn test_extract_bearer_lowercase_bearer() {
363 assert_eq!(extract_bearer_from_header("bearer abc123"), Some("abc123"));
364 }
365
366 #[test]
367 fn test_extract_bearer_uppercase_scheme() {
368 assert_eq!(extract_bearer_from_header("BEARER abc123"), Some("abc123"));
369 }
370
371 #[test]
372 fn test_extract_bearer_wrong_scheme_basic() {
373 assert_eq!(extract_bearer_from_header("Basic dXNlcjpwYXNz"), None);
374 }
375
376 #[test]
377 fn test_extract_bearer_no_scheme() {
378 assert_eq!(extract_bearer_from_header("abc123"), None);
379 }
380
381 #[test]
382 fn test_extract_bearer_empty_header() {
383 assert_eq!(extract_bearer_from_header(""), None);
384 }
385
386 #[test]
387 fn test_extract_bearer_empty_token_after_space() {
388 assert_eq!(extract_bearer_from_header("Bearer "), None);
389 }
390
391 #[test]
392 fn test_extract_bearer_double_space_rejected() {
393 assert_eq!(extract_bearer_from_header("Bearer abc"), None);
394 }
395
396 #[test]
397 fn test_extract_bearer_tab_not_space_rejected() {
398 assert_eq!(extract_bearer_from_header("Bearer\tabc"), None);
399 }
400
401 #[test]
402 fn test_extract_bearer_mixed_case_scheme() {
403 assert_eq!(
404 extract_bearer_from_header("BeArEr mytoken"),
405 Some("mytoken")
406 );
407 }
408
409 #[test]
410 fn test_extract_bearer_token_with_dots() {
411 assert_eq!(
412 extract_bearer_from_header("Bearer eyJ0.eyJz.SflK"),
413 Some("eyJ0.eyJz.SflK")
414 );
415 }
416
417 #[test]
418 fn test_verify_bearer_both_empty() {
419 assert!(!verify_bearer_token("", ""));
420 }
421
422 #[test]
423 fn verify_matching_credentials() {
424 assert!(verify_basic_credentials("user", "pass", "user", "pass"));
425 }
426
427 #[test]
428 fn verify_wrong_username() {
429 assert!(!verify_basic_credentials("user", "pass", "xxxx", "pass"));
430 }
431
432 #[test]
433 fn verify_wrong_password() {
434 assert!(!verify_basic_credentials("user", "pass", "user", "xxxx"));
435 }
436
437 #[test]
438 fn verify_empty_provided_username() {
439 assert!(!verify_basic_credentials("", "pass", "user", "pass"));
440 }
441
442 #[test]
443 fn verify_empty_provided_password() {
444 assert!(!verify_basic_credentials("user", "", "user", "pass"));
445 }
446
447 #[test]
448 fn verify_empty_expected_username() {
449 assert!(!verify_basic_credentials("user", "pass", "", "pass"));
450 }
451
452 #[test]
453 fn verify_empty_expected_password() {
454 assert!(!verify_basic_credentials("user", "pass", "user", ""));
455 }
456
457 #[test]
458 fn verify_different_lengths_username_longer() {
459 assert!(!verify_basic_credentials("users", "pass", "user", "pass"));
460 }
461
462 #[test]
463 fn verify_different_lengths_username_shorter() {
464 assert!(!verify_basic_credentials("usr", "pass", "user", "pass"));
465 }
466
467 #[test]
468 fn verify_different_lengths_password_longer() {
469 assert!(!verify_basic_credentials("user", "passw", "user", "pass"));
470 }
471
472 #[test]
473 fn verify_different_lengths_password_shorter() {
474 assert!(!verify_basic_credentials("user", "pas", "user", "pass"));
475 }
476
477 #[test]
478 fn extract_basic_valid() {
479 assert_eq!(
480 extract_basic_from_header("Basic dXNlcjpwYXNz"),
481 Some(("user".to_string(), "pass".to_string()))
482 );
483 }
484
485 #[test]
486 fn extract_basic_mixed_case_scheme() {
487 assert_eq!(
488 extract_basic_from_header("bAsIc dXNlcjpwYXNz"),
489 Some(("user".to_string(), "pass".to_string()))
490 );
491 }
492
493 #[test]
494 fn extract_basic_wrong_scheme() {
495 assert_eq!(extract_basic_from_header("Bearer dXNlcjpwYXNz"), None);
496 }
497
498 #[test]
499 fn extract_basic_empty_header() {
500 assert_eq!(extract_basic_from_header(""), None);
501 }
502
503 #[test]
504 fn extract_basic_double_space_rejected() {
505 assert_eq!(extract_basic_from_header("Basic dXNlcjpwYXNz"), None);
506 }
507
508 #[test]
509 fn extract_basic_invalid_base64() {
510 assert_eq!(extract_basic_from_header("Basic not-valid-base64!!!"), None);
511 }
512
513 #[test]
514 fn extract_basic_non_utf8_after_decode() {
515 use base64::Engine as _;
516 let encoded = base64::engine::general_purpose::STANDARD.encode([0xff, 0xfe]);
517 assert_eq!(extract_basic_from_header(&format!("Basic {encoded}")), None);
518 }
519
520 #[test]
521 fn extract_basic_no_colon() {
522 use base64::Engine as _;
523 let encoded = base64::engine::general_purpose::STANDARD.encode("userpass");
524 assert_eq!(extract_basic_from_header(&format!("Basic {encoded}")), None);
525 }
526
527 #[test]
528 fn extract_basic_colon_at_start() {
529 use base64::Engine as _;
530 let encoded = base64::engine::general_purpose::STANDARD.encode(":pass");
531 assert_eq!(extract_basic_from_header(&format!("Basic {encoded}")), None);
532 }
533
534 #[test]
535 fn extract_basic_colon_at_end() {
536 use base64::Engine as _;
537 let encoded = base64::engine::general_purpose::STANDARD.encode("user:");
538 assert_eq!(extract_basic_from_header(&format!("Basic {encoded}")), None);
539 }
540
541 #[test]
542 fn extract_basic_password_with_colon() {
543 use base64::Engine as _;
544 let encoded = base64::engine::general_purpose::STANDARD.encode("user:pass:word");
545 assert_eq!(
546 extract_basic_from_header(&format!("Basic {encoded}")),
547 Some(("user".to_string(), "pass:word".to_string()))
548 );
549 }
550}
551
552#[cfg(test)]
553mod authenticator_tests {
554 use super::*;
555 use crate::access::Identity;
556 use crate::testing::StubTokenVerifier;
557
558 fn headers(values: &[&str]) -> HeaderMap {
559 let mut headers = HeaderMap::new();
560 for value in values {
561 headers.append(AUTHORIZATION, HeaderValue::from_str(value).expect("header"));
562 }
563 headers
564 }
565
566 fn with_stub() -> Authenticator {
567 Authenticator::new("service")
568 .with_basic("dav-user", "dav-pass")
569 .with_token_verifier(Arc::new(StubTokenVerifier::default().accepting(
570 "jwt-alice",
571 "alice",
572 ["editors"],
573 )))
574 }
575
576 #[tokio::test]
577 async fn an_absent_header_is_anonymous() {
578 assert_eq!(
579 with_stub()
580 .resolve(&HeaderMap::new(), Schemes::Bearer)
581 .await,
582 Ok(Principal::Anyone)
583 );
584 }
585
586 #[tokio::test]
587 async fn the_service_token_wins_before_the_verifier_is_consulted() {
588 let auth = Authenticator::new("service").with_token_verifier(Arc::new(
590 StubTokenVerifier::default().accepting("service", "impostor", []),
591 ));
592
593 assert_eq!(
595 auth.resolve(&headers(&["Bearer service"]), Schemes::Bearer)
596 .await,
597 Ok(Principal::service_token())
598 );
599 }
600
601 #[tokio::test]
602 async fn a_verified_identity_token_is_signed_in_as_that_user() {
603 let principal = with_stub()
605 .resolve(&headers(&["Bearer jwt-alice"]), Schemes::Bearer)
606 .await
607 .expect("verified");
608
609 let Principal::SignedIn(Identity::User(user)) = principal else {
611 panic!("expected a user identity, got {principal:?}");
612 };
613 assert_eq!(user.subject, "alice");
614 assert!(user.groups.contains("editors"));
615 }
616
617 #[tokio::test]
618 async fn a_bearer_that_is_neither_the_service_token_nor_verifiable_is_refused_not_downgraded() {
619 assert_eq!(
620 with_stub()
621 .resolve(&headers(&["Bearer nobody"]), Schemes::Bearer)
622 .await,
623 Err(CredentialRefused)
624 );
625 }
626
627 #[tokio::test]
628 async fn without_a_verifier_every_foreign_bearer_is_refused() {
629 assert_eq!(
630 Authenticator::new("service")
631 .resolve(&headers(&["Bearer jwt-alice"]), Schemes::Bearer)
632 .await,
633 Err(CredentialRefused)
634 );
635 }
636
637 #[tokio::test]
638 async fn two_authorization_headers_are_refused() {
639 assert_eq!(
640 with_stub()
641 .resolve(
642 &headers(&["Bearer service", "Bearer service"]),
643 Schemes::Bearer
644 )
645 .await,
646 Err(CredentialRefused)
647 );
648 }
649
650 #[tokio::test]
651 async fn a_basic_credential_is_refused_where_only_bearer_is_accepted() {
652 let basic = format!(
654 "Basic {}",
655 base64::engine::general_purpose::STANDARD.encode("dav-user:dav-pass")
656 );
657
658 assert_eq!(
660 with_stub()
661 .resolve(&headers(&[&basic]), Schemes::Bearer)
662 .await,
663 Err(CredentialRefused)
664 );
665 assert_eq!(
666 with_stub()
667 .resolve(&headers(&[&basic]), Schemes::BasicOrBearer)
668 .await,
669 Ok(Principal::service_token())
670 );
671 }
672
673 #[tokio::test]
674 async fn a_wrong_basic_credential_is_refused_and_basic_needs_a_configured_pair() {
675 let basic = format!(
676 "Basic {}",
677 base64::engine::general_purpose::STANDARD.encode("dav-user:wrong")
678 );
679 assert_eq!(
680 with_stub()
681 .resolve(&headers(&[&basic]), Schemes::BasicOrBearer)
682 .await,
683 Err(CredentialRefused)
684 );
685 assert_eq!(
686 Authenticator::new("service")
687 .resolve(&headers(&[&basic]), Schemes::BasicOrBearer)
688 .await,
689 Err(CredentialRefused)
690 );
691 }
692
693 #[tokio::test]
694 async fn a_bearer_is_accepted_on_a_basic_surface_too() {
695 assert_eq!(
696 with_stub()
697 .resolve(&headers(&["Bearer jwt-alice"]), Schemes::BasicOrBearer)
698 .await
699 .map(|p| p.is_signed_in()),
700 Ok(true)
701 );
702 }
703
704 #[test]
705 fn debug_output_never_contains_a_secret() {
706 let auth = Authenticator::new("sk-top-secret").with_basic("dav-user", "dav-pass");
707 let rendered = format!("{auth:?}");
708 assert!(!rendered.contains("sk-top-secret"), "{rendered}");
709 assert!(!rendered.contains("dav-pass"), "{rendered}");
710 assert!(rendered.contains("dav-user"), "{rendered}");
711 }
712
713 #[test]
714 fn the_challenge_names_the_metadata_document_only_when_published() {
715 let bare = Authenticator::new("service");
717 let published = Authenticator::new("service").with_protected_resource(ProtectedResource {
718 resource: "https://notes.example.com".into(),
719 authorization_servers: vec!["https://auth.example.com".into()],
720 metadata_url: "https://notes.example.com/.well-known/oauth-protected-resource".into(),
721 });
722
723 assert!(bare.bearer_challenge().is_none());
725 assert_eq!(
726 published.bearer_challenge().expect("challenge"),
727 "Bearer resource_metadata=\"https://notes.example.com/.well-known/oauth-protected-resource\""
728 );
729 assert_eq!(
730 published
731 .protected_resource()
732 .expect("published")
733 .document(),
734 serde_json::json!({
735 "resource": "https://notes.example.com",
736 "authorization_servers": ["https://auth.example.com"],
737 "bearer_methods_supported": ["header"],
738 })
739 );
740 }
741}