modo/auth/session/jwt/
source.rs1use serde::Deserialize;
2
3use http::request::Parts;
4
5pub trait TokenSource: Send + Sync {
10 fn extract(&self, parts: &Parts) -> Option<String>;
13}
14
15pub struct BearerSource;
20
21impl TokenSource for BearerSource {
22 fn extract(&self, parts: &Parts) -> Option<String> {
23 let value = parts
24 .headers
25 .get(http::header::AUTHORIZATION)?
26 .to_str()
27 .ok()?;
28 let (scheme, rest) = value.split_once(' ')?;
29 if !scheme.eq_ignore_ascii_case("Bearer") {
30 return None;
31 }
32 let token = rest.trim_start();
33 if token.is_empty() {
34 return None;
35 }
36 Some(token.to_string())
37 }
38}
39
40pub struct QuerySource(pub &'static str);
44
45impl TokenSource for QuerySource {
46 fn extract(&self, parts: &Parts) -> Option<String> {
47 let query = parts.uri.query()?;
48 for pair in query.split('&') {
49 if let Some((key, value)) = pair.split_once('=')
50 && key == self.0
51 && !value.is_empty()
52 {
53 return Some(value.to_string());
54 }
55 }
56 None
57 }
58}
59
60pub struct CookieSource(pub &'static str);
65
66impl TokenSource for CookieSource {
67 fn extract(&self, parts: &Parts) -> Option<String> {
68 let cookie_header = parts.headers.get(http::header::COOKIE)?.to_str().ok()?;
69 for cookie in cookie_header.split(';') {
70 let cookie = cookie.trim();
71 if let Some((name, value)) = cookie.split_once('=')
72 && name.trim() == self.0
73 && !value.is_empty()
74 {
75 return Some(value.trim().to_string());
76 }
77 }
78 None
79 }
80}
81
82pub struct HeaderSource(pub &'static str);
86
87impl TokenSource for HeaderSource {
88 fn extract(&self, parts: &Parts) -> Option<String> {
89 let value = parts.headers.get(self.0)?.to_str().ok()?;
90 if value.is_empty() {
91 return None;
92 }
93 Some(value.to_string())
94 }
95}
96
97struct OwnedQuerySource(String);
100
101impl TokenSource for OwnedQuerySource {
102 fn extract(&self, parts: &Parts) -> Option<String> {
103 let query = parts.uri.query()?;
104 for pair in query.split('&') {
105 if let Some((key, value)) = pair.split_once('=')
106 && key == self.0
107 && !value.is_empty()
108 {
109 return Some(value.to_string());
110 }
111 }
112 None
113 }
114}
115
116struct OwnedCookieSource(String);
117
118impl TokenSource for OwnedCookieSource {
119 fn extract(&self, parts: &Parts) -> Option<String> {
120 let cookie_header = parts.headers.get(http::header::COOKIE)?.to_str().ok()?;
121 for cookie in cookie_header.split(';') {
122 let cookie = cookie.trim();
123 if let Some((name, value)) = cookie.split_once('=')
124 && name.trim() == self.0
125 && !value.is_empty()
126 {
127 return Some(value.trim().to_string());
128 }
129 }
130 None
131 }
132}
133
134struct OwnedHeaderSource(String);
135
136impl TokenSource for OwnedHeaderSource {
137 fn extract(&self, parts: &Parts) -> Option<String> {
138 let value = parts.headers.get(self.0.as_str())?.to_str().ok()?;
139 if value.is_empty() {
140 return None;
141 }
142 Some(value.to_string())
143 }
144}
145
146#[derive(Debug, Clone, Deserialize)]
164#[serde(tag = "kind", rename_all = "snake_case")]
165pub enum TokenSourceConfig {
166 Bearer,
168 Cookie { name: String },
170 Header { name: String },
172 Query { name: String },
174 Body { field: String },
177}
178
179impl TokenSourceConfig {
180 pub fn build(&self) -> Box<dyn TokenSource> {
185 match self {
186 Self::Bearer => Box::new(BearerSource),
187 Self::Cookie { name } => Box::new(OwnedCookieSource(name.clone())),
188 Self::Header { name } => Box::new(OwnedHeaderSource(name.clone())),
189 Self::Query { name } => Box::new(OwnedQuerySource(name.clone())),
190 Self::Body { .. } => Box::new(BearerSource),
192 }
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 fn parts_with_header(name: &str, value: &str) -> Parts {
201 let (parts, _) = http::Request::builder()
202 .header(name, value)
203 .body(())
204 .unwrap()
205 .into_parts();
206 parts
207 }
208
209 fn parts_with_uri(uri: &str) -> Parts {
210 let (parts, _) = http::Request::builder()
211 .uri(uri)
212 .body(())
213 .unwrap()
214 .into_parts();
215 parts
216 }
217
218 fn empty_parts() -> Parts {
219 let (parts, _) = http::Request::builder().body(()).unwrap().into_parts();
220 parts
221 }
222
223 #[test]
225 fn bearer_extracts_token() {
226 let parts = parts_with_header("Authorization", "Bearer my-token");
227 assert_eq!(BearerSource.extract(&parts), Some("my-token".into()));
228 }
229
230 #[test]
231 fn bearer_case_insensitive_prefix() {
232 let parts = parts_with_header("Authorization", "bearer my-token");
233 assert_eq!(BearerSource.extract(&parts), Some("my-token".into()));
234 }
235
236 #[test]
237 fn bearer_uppercase_scheme_works() {
238 let parts = parts_with_header("Authorization", "BEARER my-token");
239 assert_eq!(BearerSource.extract(&parts), Some("my-token".into()));
240 }
241
242 #[test]
243 fn bearer_mixed_case_scheme_works() {
244 let parts = parts_with_header("Authorization", "BeArEr my-token");
245 assert_eq!(BearerSource.extract(&parts), Some("my-token".into()));
246 }
247
248 #[test]
249 fn bearer_returns_none_when_missing() {
250 assert!(BearerSource.extract(&empty_parts()).is_none());
251 }
252
253 #[test]
254 fn bearer_returns_none_for_non_bearer_scheme() {
255 let parts = parts_with_header("Authorization", "Basic abc123");
256 assert!(BearerSource.extract(&parts).is_none());
257 }
258
259 #[test]
260 fn bearer_returns_none_for_empty_token() {
261 let parts = parts_with_header("Authorization", "Bearer ");
262 assert!(BearerSource.extract(&parts).is_none());
263 }
264
265 #[test]
267 fn query_extracts_token() {
268 let parts = parts_with_uri("/path?token=my-token&other=val");
269 assert_eq!(
270 QuerySource("token").extract(&parts),
271 Some("my-token".into())
272 );
273 }
274
275 #[test]
276 fn query_returns_none_when_missing() {
277 let parts = parts_with_uri("/path?other=val");
278 assert!(QuerySource("token").extract(&parts).is_none());
279 }
280
281 #[test]
282 fn query_returns_none_for_empty_value() {
283 let parts = parts_with_uri("/path?token=");
284 assert!(QuerySource("token").extract(&parts).is_none());
285 }
286
287 #[test]
288 fn query_returns_none_without_query_string() {
289 let parts = parts_with_uri("/path");
290 assert!(QuerySource("token").extract(&parts).is_none());
291 }
292
293 #[test]
295 fn cookie_extracts_token() {
296 let parts = parts_with_header("Cookie", "jwt=my-token; other=val");
297 assert_eq!(CookieSource("jwt").extract(&parts), Some("my-token".into()));
298 }
299
300 #[test]
301 fn cookie_returns_none_when_missing() {
302 let parts = parts_with_header("Cookie", "other=val");
303 assert!(CookieSource("jwt").extract(&parts).is_none());
304 }
305
306 #[test]
307 fn cookie_returns_none_without_cookie_header() {
308 assert!(CookieSource("jwt").extract(&empty_parts()).is_none());
309 }
310
311 #[test]
313 fn header_extracts_token() {
314 let parts = parts_with_header("X-API-Token", "my-token");
315 assert_eq!(
316 HeaderSource("X-API-Token").extract(&parts),
317 Some("my-token".into())
318 );
319 }
320
321 #[test]
322 fn header_returns_none_when_missing() {
323 assert!(
324 HeaderSource("X-API-Token")
325 .extract(&empty_parts())
326 .is_none()
327 );
328 }
329
330 #[test]
331 fn header_returns_none_for_empty_value() {
332 let parts = parts_with_header("X-API-Token", "");
333 assert!(HeaderSource("X-API-Token").extract(&parts).is_none());
334 }
335}