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};
9
10/// A single credential entry (username and password/hash).
11#[derive(Debug, Clone)]
12pub struct Credential {
13    username: String,
14    password: String,
15}
16
17impl Credential {
18    /// Creates a new credential from username and password/hash.
19    pub fn new(username: String, password: String) -> Self {
20        Self { username, password }
21    }
22
23    /// Parses a credential string in the format "username:password".
24    ///
25    /// # Errors
26    ///
27    /// Returns `None` if the string doesn't contain a colon or has an empty username.
28    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    /// Returns the username.
37    pub fn username(&self) -> &str {
38        &self.username
39    }
40
41    /// Returns the password/hash.
42    pub fn password(&self) -> &str {
43        &self.password
44    }
45}
46
47/// Stores credentials for authentication.
48#[derive(Debug, Clone)]
49pub struct Credentials {
50    entries: Vec<Credential>,
51}
52
53impl Credentials {
54    /// Creates a new empty credentials store.
55    pub fn new() -> Self {
56        Self {
57            entries: Vec::new(),
58        }
59    }
60
61    /// Creates credentials from a slice of credential entries.
62    pub fn from_entries(entries: Vec<Credential>) -> Self {
63        Self { entries }
64    }
65
66    /// Returns true if no credentials are stored.
67    pub fn is_empty(&self) -> bool {
68        self.entries.is_empty()
69    }
70
71    /// Returns the number of stored credentials.
72    pub fn len(&self) -> usize {
73        self.entries.len()
74    }
75
76    /// Verifies credentials from an HTTP Authorization header.
77    ///
78    /// Expects the header value in the format `Basic {base64-encoded-credentials}`
79    /// per RFC 7617.
80    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        // RFC 7617: user-id cannot contain colons, password may contain colons
94        let Some((user, password)) = decoded_str.split_once(':') else {
95            return false;
96        };
97
98        self.check(user, password)
99    }
100
101    /// Checks if the given username and password match any stored credential.
102    /// Uses constant-time comparison to prevent timing attacks.
103    fn check(&self, username: &str, password: &str) -> bool {
104        let mut found = false;
105        for cred in &self.entries {
106            // Use constant-time comparison for username to prevent enumeration
107            let user_match = hash::constant_time_eq(cred.username.as_bytes(), username.as_bytes());
108            // Always verify password to prevent timing leaks
109            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
124/// Checks if the request has valid Basic authentication.
125///
126/// Returns `true` if authentication is successful or not required.
127/// Returns `false` if authentication is required but failed.
128///
129/// # Arguments
130///
131/// * `auth_header` - The value of the Authorization header, if present.
132/// * `credentials` - The credentials to check against.
133pub fn check_basic_auth(auth_header: Option<&str>, credentials: &Credentials) -> bool {
134    if credentials.is_empty() {
135        // No credentials configured, authentication not required
136        return true;
137    }
138
139    match auth_header {
140        Some(header) => credentials.verify(header),
141        None => false,
142    }
143}
144
145/// Checks if the request has a valid Bearer token.
146///
147/// Returns `true` if authentication is successful or not required.
148/// Returns `false` if authentication is required but failed.
149///
150/// # Arguments
151///
152/// * `auth_header` - The value of the Authorization header, if present.
153/// * `expected_token` - The expected bearer token, if configured.
154pub fn check_bearer_token(auth_header: Option<&str>, expected_token: Option<&str>) -> bool {
155    let Some(expected) = expected_token else {
156        // No token configured, authentication not required
157        return true;
158    };
159
160    if expected.is_empty() {
161        // Empty token means disabled
162        return true;
163    }
164
165    let Some(header) = auth_header else {
166        return false;
167    };
168
169    // RFC 6750: auth-scheme is case-insensitive
170    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    // Use constant-time comparison to prevent timing attacks
179    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        // bcrypt hash for "password"
264        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        // SHA1 hash for "password"
275        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        // APR1 hash for "password"
286        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        // No credentials = authentication not required
341        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        // No header = fail
353        assert!(!check_basic_auth(None, &creds));
354
355        // Valid header = success
356        let valid_header = format!("Basic {}", STANDARD.encode("admin:secret"));
357        assert!(check_basic_auth(Some(&valid_header), &creds));
358
359        // Invalid header = fail
360        let invalid_header = format!("Basic {}", STANDARD.encode("admin:wrong"));
361        assert!(!check_basic_auth(Some(&invalid_header), &creds));
362    }
363
364    // ===========================================
365    // Bearer token tests
366    // ===========================================
367
368    #[test]
369    fn test_check_bearer_token_no_token_configured() {
370        // No token configured = always pass
371        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        // Empty token = disabled
378        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        // Basic auth header should not work for bearer
412        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        // RFC 6750: auth-scheme is case-insensitive
433        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}