foxy/security/
basic.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Basic authentication provider.
6
7use crate::{
8    core::{ProxyError, ProxyRequest},
9    debug_fmt, error_fmt,
10    security::{SecurityProvider, SecurityStage},
11    trace_fmt, warn_fmt,
12};
13use async_trait::async_trait;
14use base64::{Engine as _, engine::general_purpose};
15use globset::{Glob, GlobSet, GlobSetBuilder};
16use serde::Deserialize;
17use subtle::ConstantTimeEq;
18
19const BASIC: &str = "basic ";
20
21#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
22pub struct RouteRuleConfig {
23    pub methods: Vec<String>,
24    pub path: String,
25}
26
27#[derive(Debug)]
28struct RouteRule {
29    methods: Vec<String>,
30    paths: GlobSet,
31}
32
33impl RouteRule {
34    fn matches(&self, method: &str, path: &str) -> bool {
35        let method_match = self.methods.iter().any(|m| m == "*" || m == method);
36        let path_match = self.paths.is_match(path);
37
38        trace_fmt!(
39            "BasicAuthProvider",
40            "Basic Auth bypass rule check: method={} path={} -> method_match={} path_match={}",
41            method,
42            path,
43            method_match,
44            path_match
45        );
46
47        method_match && path_match
48    }
49}
50
51/// Configuration for the Basic Auth provider.
52#[derive(Debug, Clone, Deserialize, serde::Serialize)]
53pub struct BasicAuthConfig {
54    /// List of valid username:password pairs.
55    pub credentials: Vec<String>,
56    /// Routes to bypass authentication for.
57    #[serde(default)]
58    pub bypass: Vec<RouteRuleConfig>,
59}
60
61/// Basic authentication security provider.
62#[derive(Debug)]
63pub struct BasicAuthProvider {
64    valid_credentials: Vec<(String, String)>,
65    rules: Vec<RouteRule>,
66}
67
68impl BasicAuthProvider {
69    pub fn new(cfg: BasicAuthConfig) -> Result<Self, ProxyError> {
70        let mut valid_credentials = Vec::new();
71        for cred_pair in cfg.credentials {
72            let parts: Vec<&str> = cred_pair.splitn(2, ':').collect();
73            if parts.len() == 2 {
74                valid_credentials.push((parts[0].to_string(), parts[1].to_string()));
75            } else {
76                let err =
77                    ProxyError::SecurityError(format!("Invalid credential format: {cred_pair}"));
78                error_fmt!("BasicAuthProvider", "{}", err);
79                return Err(err);
80            }
81        }
82
83        let mut rules = Vec::with_capacity(cfg.bypass.len());
84        for raw in cfg.bypass {
85            let mut builder = GlobSetBuilder::new();
86            match Glob::new(&raw.path) {
87                Ok(glob) => {
88                    builder.add(glob);
89                    rules.push(RouteRule {
90                        methods: raw.methods.iter().map(|m| m.to_ascii_uppercase()).collect(),
91                        paths: match builder.build() {
92                            Ok(set) => set,
93                            Err(e) => {
94                                let err = ProxyError::SecurityError(format!(
95                                    "Failed to build glob set for path {}: {}",
96                                    raw.path, e
97                                ));
98                                error_fmt!("BasicAuthProvider", "{}", err);
99                                return Err(err);
100                            }
101                        },
102                    });
103                    debug_fmt!(
104                        "BasicAuthProvider",
105                        "Added Basic Auth bypass rule: methods={:?}, path={}",
106                        raw.methods,
107                        raw.path
108                    );
109                }
110                Err(e) => {
111                    let err = ProxyError::SecurityError(format!(
112                        "Invalid glob pattern in bypass rule: {e}"
113                    ));
114                    error_fmt!("BasicAuthProvider", "{}", err);
115                    return Err(err);
116                }
117            }
118        }
119
120        Ok(Self {
121            valid_credentials,
122            rules,
123        })
124    }
125
126    /// Validate credentials using constant-time comparison to prevent timing attacks.
127    ///
128    /// This method performs constant-time comparison of both username and password
129    /// to prevent timing-based username enumeration attacks.
130    pub fn validate_credentials_constant_time(&self, username: &str, password: &str) -> bool {
131        let mut valid = false;
132
133        // SECURITY: Always check against all credentials to maintain constant time
134        // This prevents timing attacks that could enumerate valid usernames
135        for (stored_username, stored_password) in &self.valid_credentials {
136            let username_match = stored_username.as_bytes().ct_eq(username.as_bytes());
137            let password_match = stored_password.as_bytes().ct_eq(password.as_bytes());
138
139            // Use constant-time AND operation
140            let both_match = username_match & password_match;
141
142            // Use constant-time OR to accumulate the result
143            valid |= bool::from(both_match);
144        }
145
146        valid
147    }
148
149    #[inline]
150    fn is_bypassed(&self, method: &str, path: &str) -> bool {
151        let bypassed = self.rules.iter().any(|r| r.matches(method, path));
152        if bypassed {
153            debug_fmt!(
154                "BasicAuthProvider",
155                "Basic Auth bypass for {} {}",
156                method,
157                path
158            );
159        }
160        bypassed
161    }
162}
163
164#[async_trait]
165impl SecurityProvider for BasicAuthProvider {
166    fn name(&self) -> &str {
167        "Basic"
168    }
169
170    fn stage(&self) -> SecurityStage {
171        SecurityStage::Pre
172    }
173
174    async fn pre(&self, req: ProxyRequest) -> Result<ProxyRequest, ProxyError> {
175        // 0) Bypass?
176        if self.is_bypassed(&req.method.to_string(), &req.path) {
177            debug_fmt!(
178                "BasicAuthProvider",
179                "Basic Auth bypass for {} {}",
180                req.method,
181                req.path
182            );
183            return Ok(req);
184        }
185
186        debug_fmt!(
187            "BasicAuthProvider",
188            "Basic Auth validating request: {} {}",
189            req.method,
190            req.path
191        );
192
193        // 1) Extract Authorization header
194        let auth_header = match req.headers.get("authorization") {
195            Some(h) => match h.to_str() {
196                Ok(s) => s,
197                Err(e) => {
198                    let err =
199                        ProxyError::SecurityError(format!("Invalid authorization header: {e}"));
200                    warn_fmt!("BasicAuthProvider", "{}", err);
201                    return Err(err);
202                }
203            },
204            None => {
205                let err = ProxyError::SecurityError("Missing authorization header".to_string());
206                warn_fmt!("BasicAuthProvider", "{}", err);
207                return Err(err);
208            }
209        };
210
211        if !auth_header.to_lowercase().starts_with(BASIC) {
212            let err = ProxyError::SecurityError(format!(
213                "Invalid authorization scheme: expected 'Basic', got '{}'",
214                auth_header.split_whitespace().next().unwrap_or("")
215            ));
216            warn_fmt!("BasicAuthProvider", "{}", err);
217            return Err(err);
218        }
219
220        let encoded_credentials = &auth_header[BASIC.len()..];
221        if encoded_credentials.is_empty() {
222            let err = ProxyError::SecurityError("Empty basic auth credentials".to_string());
223            warn_fmt!("BasicAuthProvider", "{}", err);
224            return Err(err);
225        }
226
227        // 2) Decode credentials
228        let decoded_credentials = match general_purpose::STANDARD.decode(encoded_credentials) {
229            Ok(bytes) => match String::from_utf8(bytes) {
230                Ok(s) => s,
231                Err(e) => {
232                    let err =
233                        ProxyError::SecurityError(format!("Invalid UTF-8 in credentials: {e}"));
234                    warn_fmt!("BasicAuthProvider", "{}", err);
235                    return Err(err);
236                }
237            },
238            Err(e) => {
239                let err =
240                    ProxyError::SecurityError(format!("Failed to base64 decode credentials: {e}"));
241                warn_fmt!("BasicAuthProvider", "{}", err);
242                return Err(err);
243            }
244        };
245
246        let parts: Vec<&str> = decoded_credentials.splitn(2, ':').collect();
247        if parts.len() != 2 {
248            let err = ProxyError::SecurityError("Invalid basic auth credential format".to_string());
249            warn_fmt!("BasicAuthProvider", "{}", err);
250            return Err(err);
251        }
252        let username = parts[0];
253        let password = parts[1];
254
255        // 3) Validate credentials using constant-time comparison
256        // SECURITY: Use constant-time comparison to prevent timing attacks
257        if self.validate_credentials_constant_time(username, password) {
258            debug_fmt!(
259                "BasicAuthProvider",
260                "Basic Auth validation successful for user: {}",
261                username
262            );
263            Ok(req)
264        } else {
265            let err = ProxyError::SecurityError("Invalid basic auth credentials".to_string());
266            warn_fmt!("BasicAuthProvider", "{}", err);
267            Err(err)
268        }
269    }
270}