Skip to main content

wisegate_core/auth/
mod.rs

1//! Authentication module for HTTP Basic Authentication (RFC 7617) and Bearer Token (RFC 6750).
2//!
3//! Provides credential storage, verification, and HTTP header parsing for
4//! implementing Basic Authentication and Bearer Token authentication in WiseGate.
5
6pub mod hash;
7
8use base64::{Engine, engine::general_purpose::STANDARD};
9use std::sync::{Arc, LazyLock};
10use tokio::sync::Semaphore;
11
12// Process-wide: permissive requests without an attributable IP are also bounded.
13static AUTH_SLOTS: LazyLock<Arc<Semaphore>> = LazyLock::new(|| Arc::new(Semaphore::new(4)));
14
15#[derive(Debug)]
16pub(crate) struct AuthCheckUnavailable;
17
18/// Keep password hashing off the async executor, with no unbounded waiting queue.
19pub(crate) async fn check_basic_auth_bounded(
20    auth_header: Option<&str>,
21    credentials: &Credentials,
22) -> Result<bool, AuthCheckUnavailable> {
23    check_basic_auth_with_slots(auth_header, credentials, &AUTH_SLOTS).await
24}
25
26async fn check_basic_auth_with_slots(
27    auth_header: Option<&str>,
28    credentials: &Credentials,
29    slots: &Arc<Semaphore>,
30) -> Result<bool, AuthCheckUnavailable> {
31    if credentials.is_empty() {
32        return Ok(true);
33    }
34    let Some(header) = auth_header.filter(|h| h.starts_with("Basic ")) else {
35        return Ok(false);
36    };
37    let permit = slots
38        .clone()
39        .try_acquire_owned()
40        .map_err(|_| AuthCheckUnavailable)?;
41    let credentials = credentials.clone();
42    let header = header.to_owned();
43    run_auth_check(permit, move || credentials.verify(&header)).await
44}
45
46async fn run_auth_check(
47    permit: tokio::sync::OwnedSemaphorePermit,
48    check: impl FnOnce() -> bool + Send + 'static,
49) -> Result<bool, AuthCheckUnavailable> {
50    tokio::task::spawn_blocking(move || {
51        // Cancellation of the requesting task must not release the slot while
52        // a non-cancellable hash check is still running.
53        let _permit = permit;
54        check()
55    })
56    .await
57    .map_err(|_| AuthCheckUnavailable)
58}
59
60/// A single credential entry (username and password/hash).
61#[derive(Debug, Clone)]
62pub struct Credential {
63    username: String,
64    password: String,
65}
66
67impl Credential {
68    /// Creates a new credential from username and password/hash.
69    pub fn new(username: String, password: String) -> Self {
70        Self { username, password }
71    }
72
73    /// Parses a credential string in the format "username:password".
74    ///
75    /// # Errors
76    ///
77    /// Returns `None` if the string doesn't contain a colon or has an empty username.
78    pub fn parse(value: &str) -> Option<Self> {
79        let (user, pass) = value.split_once(':')?;
80        if user.is_empty() {
81            return None;
82        }
83        Some(Self::new(user.to_string(), pass.to_string()))
84    }
85
86    /// Returns the username.
87    pub fn username(&self) -> &str {
88        &self.username
89    }
90
91    /// Returns the password/hash.
92    pub fn password(&self) -> &str {
93        &self.password
94    }
95}
96
97/// Stores credentials for authentication.
98#[derive(Debug, Clone)]
99pub struct Credentials {
100    entries: Vec<Credential>,
101}
102
103impl Credentials {
104    /// Creates a new empty credentials store.
105    pub fn new() -> Self {
106        Self {
107            entries: Vec::new(),
108        }
109    }
110
111    /// Creates credentials from a slice of credential entries.
112    pub fn from_entries(entries: Vec<Credential>) -> Self {
113        Self { entries }
114    }
115
116    /// Returns true if no credentials are stored.
117    pub fn is_empty(&self) -> bool {
118        self.entries.is_empty()
119    }
120
121    /// Returns the number of stored credentials.
122    pub fn len(&self) -> usize {
123        self.entries.len()
124    }
125
126    /// Verifies credentials from an HTTP Authorization header.
127    ///
128    /// Expects the header value in the format `Basic {base64-encoded-credentials}`
129    /// per RFC 7617.
130    pub fn verify(&self, auth_header: &str) -> bool {
131        let Some(encoded) = auth_header.strip_prefix("Basic ") else {
132            return false;
133        };
134
135        let Ok(decoded) = STANDARD.decode(encoded.trim()) else {
136            return false;
137        };
138
139        let Ok(decoded_str) = String::from_utf8(decoded) else {
140            return false;
141        };
142
143        // RFC 7617: user-id cannot contain colons, password may contain colons
144        let Some((user, password)) = decoded_str.split_once(':') else {
145            return false;
146        };
147
148        self.check(user, password)
149    }
150
151    /// Checks if the given username and password match any stored credential.
152    /// Uses constant-time comparison to prevent timing attacks.
153    fn check(&self, username: &str, password: &str) -> bool {
154        let mut found = false;
155        for cred in &self.entries {
156            // Use constant-time comparison for username to prevent enumeration
157            let user_match = hash::constant_time_eq(cred.username.as_bytes(), username.as_bytes());
158            // Always verify password to prevent timing leaks
159            let pass_match = hash::verify(password, &cred.password);
160            if user_match && pass_match {
161                found = true;
162            }
163        }
164        found
165    }
166}
167
168impl Default for Credentials {
169    fn default() -> Self {
170        Self::new()
171    }
172}
173
174/// Checks if the request has valid Basic authentication.
175///
176/// Returns `true` if authentication is successful or not required.
177/// Returns `false` if authentication is required but failed.
178///
179/// # Arguments
180///
181/// * `auth_header` - The value of the Authorization header, if present.
182/// * `credentials` - The credentials to check against.
183pub fn check_basic_auth(auth_header: Option<&str>, credentials: &Credentials) -> bool {
184    if credentials.is_empty() {
185        // No credentials configured, authentication not required
186        return true;
187    }
188
189    match auth_header {
190        Some(header) => credentials.verify(header),
191        None => false,
192    }
193}
194
195/// Checks if the request has a valid Bearer token.
196///
197/// Returns `true` if authentication is successful or not required.
198/// Returns `false` if authentication is required but failed.
199///
200/// # Arguments
201///
202/// * `auth_header` - The value of the Authorization header, if present.
203/// * `expected_token` - The expected bearer token, if configured.
204pub fn check_bearer_token(auth_header: Option<&str>, expected_token: Option<&str>) -> bool {
205    let Some(expected) = expected_token else {
206        // No token configured, authentication not required
207        return true;
208    };
209
210    if expected.is_empty() {
211        // Empty token means disabled
212        return true;
213    }
214
215    let Some(header) = auth_header else {
216        return false;
217    };
218
219    // RFC 6750: auth-scheme is case-insensitive
220    let Some(token) = header
221        .get(..7)
222        .filter(|prefix| prefix.eq_ignore_ascii_case("bearer "))
223        .map(|_| &header[7..])
224    else {
225        return false;
226    };
227
228    // Use constant-time comparison to prevent timing attacks
229    hash::constant_time_eq(token.trim().as_bytes(), expected.as_bytes())
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235
236    #[tokio::test]
237    async fn saturated_auth_rejects_without_waiting() {
238        use futures::FutureExt;
239
240        let slots = Arc::new(Semaphore::new(1));
241        let _permit = slots.clone().try_acquire_owned().unwrap();
242        let credentials = Credentials::from_entries(vec![Credential::new(
243            "admin".to_string(),
244            "secret".to_string(),
245        )]);
246        let header = format!("Basic {}", STANDARD.encode("admin:secret"));
247        assert!(
248            check_basic_auth_with_slots(Some(&header), &credentials, &slots)
249                .now_or_never()
250                .expect("saturated authentication must not queue")
251                .is_err()
252        );
253    }
254
255    #[tokio::test(flavor = "current_thread")]
256    async fn blocking_auth_keeps_its_slot_after_request_cancellation() {
257        let slots = Arc::new(Semaphore::new(1));
258        let permit = slots.clone().try_acquire_owned().unwrap();
259        let (started_tx, started_rx) = tokio::sync::oneshot::channel();
260        let (finish_tx, finish_rx) = std::sync::mpsc::channel();
261        let task = tokio::spawn(run_auth_check(permit, move || {
262            started_tx.send(()).unwrap();
263            // Bounded even if a regression makes the assertions below panic.
264            finish_rx
265                .recv_timeout(std::time::Duration::from_secs(2))
266                .unwrap();
267            true
268        }));
269
270        // A blocking call on this single-thread executor would prevent progress.
271        started_rx.await.unwrap();
272        task.abort();
273        assert!(task.await.unwrap_err().is_cancelled());
274        assert!(slots.clone().try_acquire_owned().is_err());
275        finish_tx.send(()).unwrap();
276        let released =
277            tokio::time::timeout(std::time::Duration::from_secs(2), slots.acquire_owned())
278                .await
279                .expect("slot must be released when hashing finishes")
280                .unwrap();
281        drop(released);
282    }
283
284    #[tokio::test]
285    async fn bounded_basic_auth_preserves_valid_and_invalid_credentials() {
286        let credentials = Credentials::from_entries(vec![Credential::new(
287            "admin".to_string(),
288            "secret".to_string(),
289        )]);
290        let valid = format!("Basic {}", STANDARD.encode("admin:secret"));
291        let invalid = format!("Basic {}", STANDARD.encode("admin:wrong"));
292        assert!(
293            check_basic_auth_bounded(Some(&valid), &credentials)
294                .await
295                .unwrap()
296        );
297        assert!(
298            !check_basic_auth_bounded(Some(&invalid), &credentials)
299                .await
300                .unwrap()
301        );
302        assert!(!check_basic_auth_bounded(None, &credentials).await.unwrap());
303    }
304
305    #[test]
306    fn test_credential_parse_valid() {
307        let cred = Credential::parse("admin:secret").unwrap();
308        assert_eq!(cred.username(), "admin");
309        assert_eq!(cred.password(), "secret");
310    }
311
312    #[test]
313    fn test_credential_parse_with_colon_in_password() {
314        let cred = Credential::parse("admin:sec:ret").unwrap();
315        assert_eq!(cred.username(), "admin");
316        assert_eq!(cred.password(), "sec:ret");
317    }
318
319    #[test]
320    fn test_credential_parse_no_colon() {
321        assert!(Credential::parse("invalid").is_none());
322    }
323
324    #[test]
325    fn test_credential_parse_empty_user() {
326        assert!(Credential::parse(":password").is_none());
327    }
328
329    #[test]
330    fn test_credentials_empty() {
331        let creds = Credentials::new();
332        assert!(creds.is_empty());
333        assert_eq!(creds.len(), 0);
334    }
335
336    #[test]
337    fn test_credentials_from_entries() {
338        let entries = vec![
339            Credential::new("admin".to_string(), "secret".to_string()),
340            Credential::new("user".to_string(), "pass".to_string()),
341        ];
342        let creds = Credentials::from_entries(entries);
343        assert!(!creds.is_empty());
344        assert_eq!(creds.len(), 2);
345    }
346
347    #[test]
348    fn test_verify_plain_text() {
349        let creds = Credentials::from_entries(vec![Credential::new(
350            "admin".to_string(),
351            "secret".to_string(),
352        )]);
353
354        let header = format!("Basic {}", STANDARD.encode("admin:secret"));
355        assert!(creds.verify(&header));
356    }
357
358    #[test]
359    fn test_verify_wrong_password() {
360        let creds = Credentials::from_entries(vec![Credential::new(
361            "admin".to_string(),
362            "secret".to_string(),
363        )]);
364
365        let header = format!("Basic {}", STANDARD.encode("admin:wrong"));
366        assert!(!creds.verify(&header));
367    }
368
369    #[test]
370    fn test_verify_wrong_user() {
371        let creds = Credentials::from_entries(vec![Credential::new(
372            "admin".to_string(),
373            "secret".to_string(),
374        )]);
375
376        let header = format!("Basic {}", STANDARD.encode("wrong:secret"));
377        assert!(!creds.verify(&header));
378    }
379
380    #[test]
381    fn test_verify_bcrypt() {
382        // bcrypt hash for "password"
383        let hash = "$2y$05$bvIG6Nmid91Mu9RcmmWZfO5HJIMCT8riNW0hEp8f6/FuA2/mHZFpe";
384        let creds =
385            Credentials::from_entries(vec![Credential::new("user".to_string(), hash.to_string())]);
386
387        let header = format!("Basic {}", STANDARD.encode("user:password"));
388        assert!(creds.verify(&header));
389    }
390
391    #[test]
392    fn test_verify_sha1() {
393        // SHA1 hash for "password"
394        let hash = "{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=";
395        let creds =
396            Credentials::from_entries(vec![Credential::new("user".to_string(), hash.to_string())]);
397
398        let header = format!("Basic {}", STANDARD.encode("user:password"));
399        assert!(creds.verify(&header));
400    }
401
402    #[test]
403    fn test_verify_apr1() {
404        // APR1 hash for "password"
405        let hash = "$apr1$lZL6V/ci$eIMz/iKDkbtys/uU7LEK00";
406        let creds =
407            Credentials::from_entries(vec![Credential::new("user".to_string(), hash.to_string())]);
408
409        let header = format!("Basic {}", STANDARD.encode("user:password"));
410        assert!(creds.verify(&header));
411    }
412
413    #[test]
414    fn test_verify_multiple_credentials() {
415        let creds = Credentials::from_entries(vec![
416            Credential::new("admin".to_string(), "admin123".to_string()),
417            Credential::new("user1".to_string(), "pass1".to_string()),
418            Credential::new("user2".to_string(), "pass2".to_string()),
419        ]);
420
421        assert!(creds.verify(&format!("Basic {}", STANDARD.encode("admin:admin123"))));
422        assert!(creds.verify(&format!("Basic {}", STANDARD.encode("user1:pass1"))));
423        assert!(creds.verify(&format!("Basic {}", STANDARD.encode("user2:pass2"))));
424        assert!(!creds.verify(&format!("Basic {}", STANDARD.encode("unknown:pass"))));
425    }
426
427    #[test]
428    fn test_verify_invalid_base64() {
429        let creds = Credentials::from_entries(vec![Credential::new(
430            "admin".to_string(),
431            "secret".to_string(),
432        )]);
433        assert!(!creds.verify("Basic not-valid-base64!!!"));
434    }
435
436    #[test]
437    fn test_verify_non_basic_auth() {
438        let creds = Credentials::from_entries(vec![Credential::new(
439            "admin".to_string(),
440            "secret".to_string(),
441        )]);
442        assert!(!creds.verify("Bearer some-token"));
443        assert!(!creds.verify("bearer some-token"));
444    }
445
446    #[test]
447    fn test_verify_missing_colon_in_decoded() {
448        let creds = Credentials::from_entries(vec![Credential::new(
449            "admin".to_string(),
450            "secret".to_string(),
451        )]);
452        let header = format!("Basic {}", STANDARD.encode("no-colon-here"));
453        assert!(!creds.verify(&header));
454    }
455
456    #[test]
457    fn test_check_basic_auth_no_credentials() {
458        let creds = Credentials::new();
459        // No credentials = authentication not required
460        assert!(check_basic_auth(None, &creds));
461        assert!(check_basic_auth(Some("Basic anything"), &creds));
462    }
463
464    #[test]
465    fn test_check_basic_auth_with_credentials() {
466        let creds = Credentials::from_entries(vec![Credential::new(
467            "admin".to_string(),
468            "secret".to_string(),
469        )]);
470
471        // No header = fail
472        assert!(!check_basic_auth(None, &creds));
473
474        // Valid header = success
475        let valid_header = format!("Basic {}", STANDARD.encode("admin:secret"));
476        assert!(check_basic_auth(Some(&valid_header), &creds));
477
478        // Invalid header = fail
479        let invalid_header = format!("Basic {}", STANDARD.encode("admin:wrong"));
480        assert!(!check_basic_auth(Some(&invalid_header), &creds));
481    }
482
483    // ===========================================
484    // Bearer token tests
485    // ===========================================
486
487    #[test]
488    fn test_check_bearer_token_no_token_configured() {
489        // No token configured = always pass
490        assert!(check_bearer_token(None, None));
491        assert!(check_bearer_token(Some("Bearer anything"), None));
492    }
493
494    #[test]
495    fn test_check_bearer_token_empty_token() {
496        // Empty token = disabled
497        assert!(check_bearer_token(None, Some("")));
498        assert!(check_bearer_token(Some("Bearer anything"), Some("")));
499    }
500
501    #[test]
502    fn test_check_bearer_token_valid() {
503        let token = "my-secret-token";
504        let header = "Bearer my-secret-token";
505        assert!(check_bearer_token(Some(header), Some(token)));
506    }
507
508    #[test]
509    fn test_check_bearer_token_valid_with_whitespace() {
510        let token = "my-secret-token";
511        let header = "Bearer my-secret-token ";
512        assert!(check_bearer_token(Some(header), Some(token)));
513    }
514
515    #[test]
516    fn test_check_bearer_token_invalid() {
517        let token = "my-secret-token";
518        let header = "Bearer wrong-token";
519        assert!(!check_bearer_token(Some(header), Some(token)));
520    }
521
522    #[test]
523    fn test_check_bearer_token_no_header() {
524        let token = "my-secret-token";
525        assert!(!check_bearer_token(None, Some(token)));
526    }
527
528    #[test]
529    fn test_check_bearer_token_basic_auth_header() {
530        // Basic auth header should not work for bearer
531        let token = "my-secret-token";
532        let header = format!("Basic {}", STANDARD.encode("user:pass"));
533        assert!(!check_bearer_token(Some(&header), Some(token)));
534    }
535
536    #[test]
537    fn test_check_bearer_token_value_case_sensitive() {
538        let token = "MySecretToken";
539        assert!(check_bearer_token(
540            Some("Bearer MySecretToken"),
541            Some(token)
542        ));
543        assert!(!check_bearer_token(
544            Some("Bearer mysecrettoken"),
545            Some(token)
546        ));
547    }
548
549    #[test]
550    fn test_check_bearer_token_prefix_case_insensitive() {
551        // RFC 6750: auth-scheme is case-insensitive
552        let token = "my-secret-token";
553        assert!(check_bearer_token(
554            Some("bearer my-secret-token"),
555            Some(token)
556        ));
557        assert!(check_bearer_token(
558            Some("BEARER my-secret-token"),
559            Some(token)
560        ));
561        assert!(check_bearer_token(
562            Some("Bearer my-secret-token"),
563            Some(token)
564        ));
565    }
566}