1fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
11 if a.len() != b.len() {
12 return false;
13 }
14 let mut diff = 0u8;
15 for (x, y) in a.iter().zip(b.iter()) {
16 diff |= x ^ y;
17 }
18 diff == 0
19}
20
21pub fn bearer_token_matches(authorization_header: Option<&str>, expected: &str) -> bool {
32 if expected.is_empty() {
33 return false;
34 }
35 let Some(raw) = authorization_header else {
36 return false;
37 };
38 let token = match raw.split_once(' ') {
39 Some((scheme, rest)) if scheme.eq_ignore_ascii_case("bearer") => rest.trim(),
40 _ => raw.trim(),
41 };
42 constant_time_eq(token.as_bytes(), expected.as_bytes())
43}
44
45#[cfg(test)]
46mod tests {
47 use super::*;
48
49 #[test]
50 fn missing_header_is_rejected() {
51 assert!(!bearer_token_matches(None, "s3cret"));
52 }
53
54 #[test]
55 fn empty_expected_never_matches() {
56 assert!(!bearer_token_matches(Some("Bearer "), ""));
57 assert!(!bearer_token_matches(Some("Bearer x"), ""));
58 }
59
60 #[test]
61 fn correct_bearer_matches() {
62 assert!(bearer_token_matches(Some("Bearer s3cret"), "s3cret"));
63 assert!(bearer_token_matches(Some("bearer s3cret"), "s3cret"));
65 assert!(bearer_token_matches(Some("s3cret"), "s3cret"));
66 }
67
68 #[test]
69 fn wrong_token_is_rejected() {
70 assert!(!bearer_token_matches(Some("Bearer nope"), "s3cret"));
71 assert!(!bearer_token_matches(Some("Bearer s3cre"), "s3cret"));
72 assert!(!bearer_token_matches(Some("Bearer s3cretX"), "s3cret"));
73 }
74
75 #[test]
76 fn surrounding_whitespace_is_trimmed() {
77 assert!(bearer_token_matches(Some("Bearer s3cret "), "s3cret"));
79 }
80
81 #[test]
82 fn length_mismatch_is_rejected() {
83 assert!(!constant_time_eq(b"ab", b"abc"));
84 assert!(constant_time_eq(b"abc", b"abc"));
85 }
86}