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 extract_query(parts, self.0)
48 }
49}
50
51pub struct CookieSource(pub &'static str);
56
57impl TokenSource for CookieSource {
58 fn extract(&self, parts: &Parts) -> Option<String> {
59 extract_cookie(parts, self.0)
60 }
61}
62
63pub struct HeaderSource(pub &'static str);
67
68impl TokenSource for HeaderSource {
69 fn extract(&self, parts: &Parts) -> Option<String> {
70 extract_header(parts, self.0)
71 }
72}
73
74struct OwnedQuerySource(String);
75
76impl TokenSource for OwnedQuerySource {
77 fn extract(&self, parts: &Parts) -> Option<String> {
78 extract_query(parts, &self.0)
79 }
80}
81
82struct OwnedCookieSource(String);
83
84impl TokenSource for OwnedCookieSource {
85 fn extract(&self, parts: &Parts) -> Option<String> {
86 extract_cookie(parts, &self.0)
87 }
88}
89
90struct OwnedHeaderSource(String);
91
92impl TokenSource for OwnedHeaderSource {
93 fn extract(&self, parts: &Parts) -> Option<String> {
94 extract_header(parts, &self.0)
95 }
96}
97
98fn extract_query(parts: &Parts, name: &str) -> Option<String> {
99 let query = parts.uri.query()?;
100 for pair in query.split('&') {
101 if let Some((key, value)) = pair.split_once('=')
102 && key == name
103 && !value.is_empty()
104 {
105 return Some(value.to_string());
106 }
107 }
108 None
109}
110
111fn extract_cookie(parts: &Parts, name: &str) -> Option<String> {
112 let cookie_header = parts.headers.get(http::header::COOKIE)?.to_str().ok()?;
113 for cookie in cookie_header.split(';') {
114 let cookie = cookie.trim();
115 if let Some((cname, value)) = cookie.split_once('=')
116 && cname.trim() == name
117 && !value.is_empty()
118 {
119 return Some(value.trim().to_string());
120 }
121 }
122 None
123}
124
125fn extract_header(parts: &Parts, name: &str) -> Option<String> {
126 let value = parts.headers.get(name)?.to_str().ok()?;
127 if value.is_empty() {
128 return None;
129 }
130 Some(value.to_string())
131}
132
133#[derive(Debug, Clone, Deserialize)]
151#[serde(tag = "kind", rename_all = "snake_case")]
152pub enum TokenSourceConfig {
153 Bearer,
155 Cookie { name: String },
157 Header { name: String },
159 Query { name: String },
161 Body { field: String },
164}
165
166impl TokenSourceConfig {
167 pub fn build(&self) -> Box<dyn TokenSource> {
172 match self {
173 Self::Bearer => Box::new(BearerSource),
174 Self::Cookie { name } => Box::new(OwnedCookieSource(name.clone())),
175 Self::Header { name } => Box::new(OwnedHeaderSource(name.clone())),
176 Self::Query { name } => Box::new(OwnedQuerySource(name.clone())),
177 Self::Body { .. } => Box::new(BearerSource),
179 }
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 fn parts_with_header(name: &str, value: &str) -> Parts {
188 let (parts, _) = http::Request::builder()
189 .header(name, value)
190 .body(())
191 .unwrap()
192 .into_parts();
193 parts
194 }
195
196 fn parts_with_uri(uri: &str) -> Parts {
197 let (parts, _) = http::Request::builder()
198 .uri(uri)
199 .body(())
200 .unwrap()
201 .into_parts();
202 parts
203 }
204
205 fn empty_parts() -> Parts {
206 let (parts, _) = http::Request::builder().body(()).unwrap().into_parts();
207 parts
208 }
209
210 #[test]
212 fn bearer_extracts_token() {
213 let parts = parts_with_header("Authorization", "Bearer my-token");
214 assert_eq!(BearerSource.extract(&parts), Some("my-token".into()));
215 }
216
217 #[test]
218 fn bearer_case_insensitive_prefix() {
219 let parts = parts_with_header("Authorization", "bearer my-token");
220 assert_eq!(BearerSource.extract(&parts), Some("my-token".into()));
221 }
222
223 #[test]
224 fn bearer_uppercase_scheme_works() {
225 let parts = parts_with_header("Authorization", "BEARER my-token");
226 assert_eq!(BearerSource.extract(&parts), Some("my-token".into()));
227 }
228
229 #[test]
230 fn bearer_mixed_case_scheme_works() {
231 let parts = parts_with_header("Authorization", "BeArEr my-token");
232 assert_eq!(BearerSource.extract(&parts), Some("my-token".into()));
233 }
234
235 #[test]
236 fn bearer_returns_none_when_missing() {
237 assert!(BearerSource.extract(&empty_parts()).is_none());
238 }
239
240 #[test]
241 fn bearer_returns_none_for_non_bearer_scheme() {
242 let parts = parts_with_header("Authorization", "Basic abc123");
243 assert!(BearerSource.extract(&parts).is_none());
244 }
245
246 #[test]
247 fn bearer_returns_none_for_empty_token() {
248 let parts = parts_with_header("Authorization", "Bearer ");
249 assert!(BearerSource.extract(&parts).is_none());
250 }
251
252 #[test]
254 fn query_extracts_token() {
255 let parts = parts_with_uri("/path?token=my-token&other=val");
256 assert_eq!(
257 QuerySource("token").extract(&parts),
258 Some("my-token".into())
259 );
260 }
261
262 #[test]
263 fn query_returns_none_when_missing() {
264 let parts = parts_with_uri("/path?other=val");
265 assert!(QuerySource("token").extract(&parts).is_none());
266 }
267
268 #[test]
269 fn query_returns_none_for_empty_value() {
270 let parts = parts_with_uri("/path?token=");
271 assert!(QuerySource("token").extract(&parts).is_none());
272 }
273
274 #[test]
275 fn query_returns_none_without_query_string() {
276 let parts = parts_with_uri("/path");
277 assert!(QuerySource("token").extract(&parts).is_none());
278 }
279
280 #[test]
282 fn cookie_extracts_token() {
283 let parts = parts_with_header("Cookie", "jwt=my-token; other=val");
284 assert_eq!(CookieSource("jwt").extract(&parts), Some("my-token".into()));
285 }
286
287 #[test]
288 fn cookie_returns_none_when_missing() {
289 let parts = parts_with_header("Cookie", "other=val");
290 assert!(CookieSource("jwt").extract(&parts).is_none());
291 }
292
293 #[test]
294 fn cookie_returns_none_without_cookie_header() {
295 assert!(CookieSource("jwt").extract(&empty_parts()).is_none());
296 }
297
298 #[test]
300 fn header_extracts_token() {
301 let parts = parts_with_header("X-API-Token", "my-token");
302 assert_eq!(
303 HeaderSource("X-API-Token").extract(&parts),
304 Some("my-token".into())
305 );
306 }
307
308 #[test]
309 fn header_returns_none_when_missing() {
310 assert!(
311 HeaderSource("X-API-Token")
312 .extract(&empty_parts())
313 .is_none()
314 );
315 }
316
317 #[test]
318 fn header_returns_none_for_empty_value() {
319 let parts = parts_with_header("X-API-Token", "");
320 assert!(HeaderSource("X-API-Token").extract(&parts).is_none());
321 }
322}