1pub mod hash;
7
8use base64::{Engine, engine::general_purpose::STANDARD};
9
10#[derive(Debug, Clone)]
12pub struct Credential {
13 username: String,
14 password: String,
15}
16
17impl Credential {
18 pub fn new(username: String, password: String) -> Self {
20 Self { username, password }
21 }
22
23 pub fn parse(value: &str) -> Option<Self> {
29 let (user, pass) = value.split_once(':')?;
30 if user.is_empty() {
31 return None;
32 }
33 Some(Self::new(user.to_string(), pass.to_string()))
34 }
35
36 pub fn username(&self) -> &str {
38 &self.username
39 }
40
41 pub fn password(&self) -> &str {
43 &self.password
44 }
45}
46
47#[derive(Debug, Clone)]
49pub struct Credentials {
50 entries: Vec<Credential>,
51}
52
53impl Credentials {
54 pub fn new() -> Self {
56 Self {
57 entries: Vec::new(),
58 }
59 }
60
61 pub fn from_entries(entries: Vec<Credential>) -> Self {
63 Self { entries }
64 }
65
66 pub fn is_empty(&self) -> bool {
68 self.entries.is_empty()
69 }
70
71 pub fn len(&self) -> usize {
73 self.entries.len()
74 }
75
76 pub fn verify(&self, auth_header: &str) -> bool {
81 let Some(encoded) = auth_header.strip_prefix("Basic ") else {
82 return false;
83 };
84
85 let Ok(decoded) = STANDARD.decode(encoded.trim()) else {
86 return false;
87 };
88
89 let Ok(decoded_str) = String::from_utf8(decoded) else {
90 return false;
91 };
92
93 let Some((user, password)) = decoded_str.split_once(':') else {
95 return false;
96 };
97
98 self.check(user, password)
99 }
100
101 fn check(&self, username: &str, password: &str) -> bool {
104 let mut found = false;
105 for cred in &self.entries {
106 let user_match = hash::constant_time_eq(cred.username.as_bytes(), username.as_bytes());
108 let pass_match = hash::verify(password, &cred.password);
110 if user_match && pass_match {
111 found = true;
112 }
113 }
114 found
115 }
116}
117
118impl Default for Credentials {
119 fn default() -> Self {
120 Self::new()
121 }
122}
123
124pub fn check_basic_auth(auth_header: Option<&str>, credentials: &Credentials) -> bool {
134 if credentials.is_empty() {
135 return true;
137 }
138
139 match auth_header {
140 Some(header) => credentials.verify(header),
141 None => false,
142 }
143}
144
145pub fn check_bearer_token(auth_header: Option<&str>, expected_token: Option<&str>) -> bool {
155 let Some(expected) = expected_token else {
156 return true;
158 };
159
160 if expected.is_empty() {
161 return true;
163 }
164
165 let Some(header) = auth_header else {
166 return false;
167 };
168
169 let Some(token) = header
171 .get(..7)
172 .filter(|prefix| prefix.eq_ignore_ascii_case("bearer "))
173 .map(|_| &header[7..])
174 else {
175 return false;
176 };
177
178 hash::constant_time_eq(token.trim().as_bytes(), expected.as_bytes())
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 #[test]
187 fn test_credential_parse_valid() {
188 let cred = Credential::parse("admin:secret").unwrap();
189 assert_eq!(cred.username(), "admin");
190 assert_eq!(cred.password(), "secret");
191 }
192
193 #[test]
194 fn test_credential_parse_with_colon_in_password() {
195 let cred = Credential::parse("admin:sec:ret").unwrap();
196 assert_eq!(cred.username(), "admin");
197 assert_eq!(cred.password(), "sec:ret");
198 }
199
200 #[test]
201 fn test_credential_parse_no_colon() {
202 assert!(Credential::parse("invalid").is_none());
203 }
204
205 #[test]
206 fn test_credential_parse_empty_user() {
207 assert!(Credential::parse(":password").is_none());
208 }
209
210 #[test]
211 fn test_credentials_empty() {
212 let creds = Credentials::new();
213 assert!(creds.is_empty());
214 assert_eq!(creds.len(), 0);
215 }
216
217 #[test]
218 fn test_credentials_from_entries() {
219 let entries = vec![
220 Credential::new("admin".to_string(), "secret".to_string()),
221 Credential::new("user".to_string(), "pass".to_string()),
222 ];
223 let creds = Credentials::from_entries(entries);
224 assert!(!creds.is_empty());
225 assert_eq!(creds.len(), 2);
226 }
227
228 #[test]
229 fn test_verify_plain_text() {
230 let creds = Credentials::from_entries(vec![Credential::new(
231 "admin".to_string(),
232 "secret".to_string(),
233 )]);
234
235 let header = format!("Basic {}", STANDARD.encode("admin:secret"));
236 assert!(creds.verify(&header));
237 }
238
239 #[test]
240 fn test_verify_wrong_password() {
241 let creds = Credentials::from_entries(vec![Credential::new(
242 "admin".to_string(),
243 "secret".to_string(),
244 )]);
245
246 let header = format!("Basic {}", STANDARD.encode("admin:wrong"));
247 assert!(!creds.verify(&header));
248 }
249
250 #[test]
251 fn test_verify_wrong_user() {
252 let creds = Credentials::from_entries(vec![Credential::new(
253 "admin".to_string(),
254 "secret".to_string(),
255 )]);
256
257 let header = format!("Basic {}", STANDARD.encode("wrong:secret"));
258 assert!(!creds.verify(&header));
259 }
260
261 #[test]
262 fn test_verify_bcrypt() {
263 let hash = "$2y$05$bvIG6Nmid91Mu9RcmmWZfO5HJIMCT8riNW0hEp8f6/FuA2/mHZFpe";
265 let creds =
266 Credentials::from_entries(vec![Credential::new("user".to_string(), hash.to_string())]);
267
268 let header = format!("Basic {}", STANDARD.encode("user:password"));
269 assert!(creds.verify(&header));
270 }
271
272 #[test]
273 fn test_verify_sha1() {
274 let hash = "{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=";
276 let creds =
277 Credentials::from_entries(vec![Credential::new("user".to_string(), hash.to_string())]);
278
279 let header = format!("Basic {}", STANDARD.encode("user:password"));
280 assert!(creds.verify(&header));
281 }
282
283 #[test]
284 fn test_verify_apr1() {
285 let hash = "$apr1$lZL6V/ci$eIMz/iKDkbtys/uU7LEK00";
287 let creds =
288 Credentials::from_entries(vec![Credential::new("user".to_string(), hash.to_string())]);
289
290 let header = format!("Basic {}", STANDARD.encode("user:password"));
291 assert!(creds.verify(&header));
292 }
293
294 #[test]
295 fn test_verify_multiple_credentials() {
296 let creds = Credentials::from_entries(vec![
297 Credential::new("admin".to_string(), "admin123".to_string()),
298 Credential::new("user1".to_string(), "pass1".to_string()),
299 Credential::new("user2".to_string(), "pass2".to_string()),
300 ]);
301
302 assert!(creds.verify(&format!("Basic {}", STANDARD.encode("admin:admin123"))));
303 assert!(creds.verify(&format!("Basic {}", STANDARD.encode("user1:pass1"))));
304 assert!(creds.verify(&format!("Basic {}", STANDARD.encode("user2:pass2"))));
305 assert!(!creds.verify(&format!("Basic {}", STANDARD.encode("unknown:pass"))));
306 }
307
308 #[test]
309 fn test_verify_invalid_base64() {
310 let creds = Credentials::from_entries(vec![Credential::new(
311 "admin".to_string(),
312 "secret".to_string(),
313 )]);
314 assert!(!creds.verify("Basic not-valid-base64!!!"));
315 }
316
317 #[test]
318 fn test_verify_non_basic_auth() {
319 let creds = Credentials::from_entries(vec![Credential::new(
320 "admin".to_string(),
321 "secret".to_string(),
322 )]);
323 assert!(!creds.verify("Bearer some-token"));
324 assert!(!creds.verify("bearer some-token"));
325 }
326
327 #[test]
328 fn test_verify_missing_colon_in_decoded() {
329 let creds = Credentials::from_entries(vec![Credential::new(
330 "admin".to_string(),
331 "secret".to_string(),
332 )]);
333 let header = format!("Basic {}", STANDARD.encode("no-colon-here"));
334 assert!(!creds.verify(&header));
335 }
336
337 #[test]
338 fn test_check_basic_auth_no_credentials() {
339 let creds = Credentials::new();
340 assert!(check_basic_auth(None, &creds));
342 assert!(check_basic_auth(Some("Basic anything"), &creds));
343 }
344
345 #[test]
346 fn test_check_basic_auth_with_credentials() {
347 let creds = Credentials::from_entries(vec![Credential::new(
348 "admin".to_string(),
349 "secret".to_string(),
350 )]);
351
352 assert!(!check_basic_auth(None, &creds));
354
355 let valid_header = format!("Basic {}", STANDARD.encode("admin:secret"));
357 assert!(check_basic_auth(Some(&valid_header), &creds));
358
359 let invalid_header = format!("Basic {}", STANDARD.encode("admin:wrong"));
361 assert!(!check_basic_auth(Some(&invalid_header), &creds));
362 }
363
364 #[test]
369 fn test_check_bearer_token_no_token_configured() {
370 assert!(check_bearer_token(None, None));
372 assert!(check_bearer_token(Some("Bearer anything"), None));
373 }
374
375 #[test]
376 fn test_check_bearer_token_empty_token() {
377 assert!(check_bearer_token(None, Some("")));
379 assert!(check_bearer_token(Some("Bearer anything"), Some("")));
380 }
381
382 #[test]
383 fn test_check_bearer_token_valid() {
384 let token = "my-secret-token";
385 let header = "Bearer my-secret-token";
386 assert!(check_bearer_token(Some(header), Some(token)));
387 }
388
389 #[test]
390 fn test_check_bearer_token_valid_with_whitespace() {
391 let token = "my-secret-token";
392 let header = "Bearer my-secret-token ";
393 assert!(check_bearer_token(Some(header), Some(token)));
394 }
395
396 #[test]
397 fn test_check_bearer_token_invalid() {
398 let token = "my-secret-token";
399 let header = "Bearer wrong-token";
400 assert!(!check_bearer_token(Some(header), Some(token)));
401 }
402
403 #[test]
404 fn test_check_bearer_token_no_header() {
405 let token = "my-secret-token";
406 assert!(!check_bearer_token(None, Some(token)));
407 }
408
409 #[test]
410 fn test_check_bearer_token_basic_auth_header() {
411 let token = "my-secret-token";
413 let header = format!("Basic {}", STANDARD.encode("user:pass"));
414 assert!(!check_bearer_token(Some(&header), Some(token)));
415 }
416
417 #[test]
418 fn test_check_bearer_token_value_case_sensitive() {
419 let token = "MySecretToken";
420 assert!(check_bearer_token(
421 Some("Bearer MySecretToken"),
422 Some(token)
423 ));
424 assert!(!check_bearer_token(
425 Some("Bearer mysecrettoken"),
426 Some(token)
427 ));
428 }
429
430 #[test]
431 fn test_check_bearer_token_prefix_case_insensitive() {
432 let token = "my-secret-token";
434 assert!(check_bearer_token(
435 Some("bearer my-secret-token"),
436 Some(token)
437 ));
438 assert!(check_bearer_token(
439 Some("BEARER my-secret-token"),
440 Some(token)
441 ));
442 assert!(check_bearer_token(
443 Some("Bearer my-secret-token"),
444 Some(token)
445 ));
446 }
447}