1use axum::http::HeaderMap;
11use crate::error::ApiError;
12use crate::tokens::LongLivedTokenStore;
13
14#[derive(Clone, Debug)]
15pub struct BearerAuth(pub String);
16
17impl BearerAuth {
18 pub async fn from_headers(
23 headers: &HeaderMap,
24 tokens: &LongLivedTokenStore,
25 ) -> Result<Self, ApiError> {
26 let token = Self::extract_token(headers)?;
27 if !tokens.is_valid(&token).await {
28 return Err(ApiError::Unauthorized);
29 }
30 Ok(Self(token))
31 }
32
33 pub fn extract_token(headers: &HeaderMap) -> Result<String, ApiError> {
36 let header = headers
37 .get(axum::http::header::AUTHORIZATION)
38 .ok_or(ApiError::Unauthorized)?;
39 let value = header.to_str().map_err(|_| ApiError::Unauthorized)?;
40 let token = value
41 .strip_prefix("Bearer ")
42 .ok_or(ApiError::Unauthorized)?
43 .trim()
44 .to_string();
45 if token.is_empty() {
46 return Err(ApiError::Unauthorized);
47 }
48 Ok(token)
49 }
50}
51
52#[cfg(test)]
53mod tests {
54 use super::*;
55 use axum::http::header::AUTHORIZATION;
56
57 fn mkheaders(value: &str) -> HeaderMap {
58 let mut h = HeaderMap::new();
59 h.insert(AUTHORIZATION, value.parse().unwrap());
60 h
61 }
62
63 #[test]
64 fn extract_strips_bearer_prefix() {
65 let h = mkheaders("Bearer abc123");
66 assert_eq!(BearerAuth::extract_token(&h).unwrap(), "abc123");
67 }
68
69 #[test]
70 fn extract_rejects_missing_prefix() {
71 let h = mkheaders("abc123");
72 assert!(matches!(BearerAuth::extract_token(&h), Err(ApiError::Unauthorized)));
73 }
74
75 #[test]
76 fn extract_rejects_missing_header() {
77 let h = HeaderMap::new();
78 assert!(matches!(BearerAuth::extract_token(&h), Err(ApiError::Unauthorized)));
79 }
80
81 #[test]
82 fn extract_rejects_empty_token() {
83 let h = mkheaders("Bearer ");
84 assert!(matches!(BearerAuth::extract_token(&h), Err(ApiError::Unauthorized)));
85 }
86
87 #[tokio::test]
88 async fn from_headers_accepts_registered_token() {
89 let store = LongLivedTokenStore::empty();
90 store.register("good_token").await;
91 let h = mkheaders("Bearer good_token");
92 let auth = BearerAuth::from_headers(&h, &store).await.unwrap();
93 assert_eq!(auth.0, "good_token");
94 }
95
96 #[tokio::test]
97 async fn from_headers_rejects_unregistered_token() {
98 let store = LongLivedTokenStore::empty();
99 store.register("good_token").await;
100 let h = mkheaders("Bearer wrong_token");
101 assert!(matches!(BearerAuth::from_headers(&h, &store).await, Err(ApiError::Unauthorized)));
102 }
103
104 #[tokio::test]
105 async fn dev_mode_still_accepts_any_non_empty() {
106 let store = LongLivedTokenStore::allow_any_non_empty();
107 let h = mkheaders("Bearer literally-anything");
108 assert!(BearerAuth::from_headers(&h, &store).await.is_ok());
109 }
110
111 #[tokio::test]
112 async fn dev_mode_still_rejects_empty() {
113 let store = LongLivedTokenStore::allow_any_non_empty();
114 let h = mkheaders("Bearer ");
115 assert!(matches!(BearerAuth::from_headers(&h, &store).await, Err(ApiError::Unauthorized)));
116 }
117}