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    /// Returns an iterator over the credentials.
77    pub fn iter(&self) -> impl Iterator<Item = &Credential> {
78        self.entries.iter()
79    }
80
81    /// Verifies credentials from an HTTP Authorization header.
82    ///
83    /// Expects the header value in the format `Basic {base64-encoded-credentials}`
84    /// per RFC 7617.
85    pub fn verify(&self, auth_header: &str) -> bool {
86        let Some(encoded) = auth_header.strip_prefix("Basic ") else {
87            return false;
88        };
89
90        let Ok(decoded) = STANDARD.decode(encoded.trim()) else {
91            return false;
92        };
93
94        let Ok(decoded_str) = String::from_utf8(decoded) else {
95            return false;
96        };
97
98        // RFC 7617: user-id cannot contain colons, password may contain colons
99        let Some((user, password)) = decoded_str.split_once(':') else {
100            return false;
101        };
102
103        self.check(user, password)
104    }
105
106    /// Checks if the given username and password match any stored credential.
107    /// Uses constant-time comparison to prevent timing attacks.
108    fn check(&self, username: &str, password: &str) -> bool {
109        let mut found = false;
110        for cred in &self.entries {
111            // Use constant-time comparison for username to prevent enumeration
112            let user_match = hash::constant_time_eq(cred.username.as_bytes(), username.as_bytes());
113            // Always verify password to prevent timing leaks
114            let pass_match = hash::verify(password, &cred.password);
115            if user_match && pass_match {
116                found = true;
117            }
118        }
119        found
120    }
121}
122
123impl Default for Credentials {
124    fn default() -> Self {
125        Self::new()
126    }
127}
128
129/// Checks if the request has valid Basic authentication.
130///
131/// Returns `true` if authentication is successful or not required.
132/// Returns `false` if authentication is required but failed.
133///
134/// # Arguments
135///
136/// * `auth_header` - The value of the Authorization header, if present.
137/// * `credentials` - The credentials to check against.
138pub fn check_basic_auth(auth_header: Option<&str>, credentials: &Credentials) -> bool {
139    if credentials.is_empty() {
140        // No credentials configured, authentication not required
141        return true;
142    }
143
144    match auth_header {
145        Some(header) => credentials.verify(header),
146        None => false,
147    }
148}
149
150/// Checks if the request has a valid Bearer token.
151///
152/// Returns `true` if authentication is successful or not required.
153/// Returns `false` if authentication is required but failed.
154///
155/// # Arguments
156///
157/// * `auth_header` - The value of the Authorization header, if present.
158/// * `expected_token` - The expected bearer token, if configured.
159pub fn check_bearer_token(auth_header: Option<&str>, expected_token: Option<&str>) -> bool {
160    let Some(expected) = expected_token else {
161        // No token configured, authentication not required
162        return true;
163    };
164
165    if expected.is_empty() {
166        // Empty token means disabled
167        return true;
168    }
169
170    let Some(header) = auth_header else {
171        return false;
172    };
173
174    let Some(token) = header.strip_prefix("Bearer ") 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    }
325
326    #[test]
327    fn test_verify_missing_colon_in_decoded() {
328        let creds = Credentials::from_entries(vec![Credential::new(
329            "admin".to_string(),
330            "secret".to_string(),
331        )]);
332        let header = format!("Basic {}", STANDARD.encode("no-colon-here"));
333        assert!(!creds.verify(&header));
334    }
335
336    #[test]
337    fn test_check_basic_auth_no_credentials() {
338        let creds = Credentials::new();
339        // No credentials = authentication not required
340        assert!(check_basic_auth(None, &creds));
341        assert!(check_basic_auth(Some("Basic anything"), &creds));
342    }
343
344    #[test]
345    fn test_check_basic_auth_with_credentials() {
346        let creds = Credentials::from_entries(vec![Credential::new(
347            "admin".to_string(),
348            "secret".to_string(),
349        )]);
350
351        // No header = fail
352        assert!(!check_basic_auth(None, &creds));
353
354        // Valid header = success
355        let valid_header = format!("Basic {}", STANDARD.encode("admin:secret"));
356        assert!(check_basic_auth(Some(&valid_header), &creds));
357
358        // Invalid header = fail
359        let invalid_header = format!("Basic {}", STANDARD.encode("admin:wrong"));
360        assert!(!check_basic_auth(Some(&invalid_header), &creds));
361    }
362
363    // ===========================================
364    // Bearer token tests
365    // ===========================================
366
367    #[test]
368    fn test_check_bearer_token_no_token_configured() {
369        // No token configured = always pass
370        assert!(check_bearer_token(None, None));
371        assert!(check_bearer_token(Some("Bearer anything"), None));
372    }
373
374    #[test]
375    fn test_check_bearer_token_empty_token() {
376        // Empty token = disabled
377        assert!(check_bearer_token(None, Some("")));
378        assert!(check_bearer_token(Some("Bearer anything"), Some("")));
379    }
380
381    #[test]
382    fn test_check_bearer_token_valid() {
383        let token = "my-secret-token";
384        let header = "Bearer my-secret-token";
385        assert!(check_bearer_token(Some(header), Some(token)));
386    }
387
388    #[test]
389    fn test_check_bearer_token_valid_with_whitespace() {
390        let token = "my-secret-token";
391        let header = "Bearer my-secret-token ";
392        assert!(check_bearer_token(Some(header), Some(token)));
393    }
394
395    #[test]
396    fn test_check_bearer_token_invalid() {
397        let token = "my-secret-token";
398        let header = "Bearer wrong-token";
399        assert!(!check_bearer_token(Some(header), Some(token)));
400    }
401
402    #[test]
403    fn test_check_bearer_token_no_header() {
404        let token = "my-secret-token";
405        assert!(!check_bearer_token(None, Some(token)));
406    }
407
408    #[test]
409    fn test_check_bearer_token_basic_auth_header() {
410        // Basic auth header should not work for bearer
411        let token = "my-secret-token";
412        let header = format!("Basic {}", STANDARD.encode("user:pass"));
413        assert!(!check_bearer_token(Some(&header), Some(token)));
414    }
415
416    #[test]
417    fn test_check_bearer_token_case_sensitive() {
418        let token = "MySecretToken";
419        assert!(check_bearer_token(
420            Some("Bearer MySecretToken"),
421            Some(token)
422        ));
423        assert!(!check_bearer_token(
424            Some("Bearer mysecrettoken"),
425            Some(token)
426        ));
427    }
428}