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;
17
18const BASIC: &str = "basic ";
19
20#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
21pub struct RouteRuleConfig {
22    pub methods: Vec<String>,
23    pub path: String,
24}
25
26#[derive(Debug)]
27struct RouteRule {
28    methods: Vec<String>,
29    paths: GlobSet,
30}
31
32impl RouteRule {
33    fn matches(&self, method: &str, path: &str) -> bool {
34        let method_match = self.methods.iter().any(|m| m == "*" || m == method);
35        let path_match = self.paths.is_match(path);
36
37        trace_fmt!(
38            "BasicAuthProvider",
39            "Basic Auth bypass rule check: method={} path={} -> method_match={} path_match={}",
40            method,
41            path,
42            method_match,
43            path_match
44        );
45
46        method_match && path_match
47    }
48}
49
50/// Configuration for the Basic Auth provider.
51#[derive(Debug, Clone, Deserialize, serde::Serialize)]
52pub struct BasicAuthConfig {
53    /// List of valid username:password pairs.
54    pub credentials: Vec<String>,
55    /// Routes to bypass authentication for.
56    #[serde(default)]
57    pub bypass: Vec<RouteRuleConfig>,
58}
59
60/// Basic authentication security provider.
61#[derive(Debug)]
62pub struct BasicAuthProvider {
63    valid_credentials: Vec<(String, String)>,
64    rules: Vec<RouteRule>,
65}
66
67impl BasicAuthProvider {
68    pub fn new(cfg: BasicAuthConfig) -> Result<Self, ProxyError> {
69        let mut valid_credentials = Vec::new();
70        for cred_pair in cfg.credentials {
71            let parts: Vec<&str> = cred_pair.splitn(2, ':').collect();
72            if parts.len() == 2 {
73                valid_credentials.push((parts[0].to_string(), parts[1].to_string()));
74            } else {
75                let err =
76                    ProxyError::SecurityError(format!("Invalid credential format: {cred_pair}"));
77                error_fmt!("BasicAuthProvider", "{}", err);
78                return Err(err);
79            }
80        }
81
82        let mut rules = Vec::with_capacity(cfg.bypass.len());
83        for raw in cfg.bypass {
84            let mut builder = GlobSetBuilder::new();
85            match Glob::new(&raw.path) {
86                Ok(glob) => {
87                    builder.add(glob);
88                    rules.push(RouteRule {
89                        methods: raw.methods.iter().map(|m| m.to_ascii_uppercase()).collect(),
90                        paths: match builder.build() {
91                            Ok(set) => set,
92                            Err(e) => {
93                                let err = ProxyError::SecurityError(format!(
94                                    "Failed to build glob set for path {}: {}",
95                                    raw.path, e
96                                ));
97                                error_fmt!("BasicAuthProvider", "{}", err);
98                                return Err(err);
99                            }
100                        },
101                    });
102                    debug_fmt!(
103                        "BasicAuthProvider",
104                        "Added Basic Auth bypass rule: methods={:?}, path={}",
105                        raw.methods,
106                        raw.path
107                    );
108                }
109                Err(e) => {
110                    let err = ProxyError::SecurityError(format!(
111                        "Invalid glob pattern in bypass rule: {e}"
112                    ));
113                    error_fmt!("BasicAuthProvider", "{}", err);
114                    return Err(err);
115                }
116            }
117        }
118
119        Ok(Self {
120            valid_credentials,
121            rules,
122        })
123    }
124
125    #[inline]
126    fn is_bypassed(&self, method: &str, path: &str) -> bool {
127        let bypassed = self.rules.iter().any(|r| r.matches(method, path));
128        if bypassed {
129            debug_fmt!(
130                "BasicAuthProvider",
131                "Basic Auth bypass for {} {}",
132                method,
133                path
134            );
135        }
136        bypassed
137    }
138}
139
140#[async_trait]
141impl SecurityProvider for BasicAuthProvider {
142    fn name(&self) -> &str {
143        "Basic"
144    }
145
146    fn stage(&self) -> SecurityStage {
147        SecurityStage::Pre
148    }
149
150    async fn pre(&self, req: ProxyRequest) -> Result<ProxyRequest, ProxyError> {
151        // 0) Bypass?
152        if self.is_bypassed(&req.method.to_string(), &req.path) {
153            debug_fmt!(
154                "BasicAuthProvider",
155                "Basic Auth bypass for {} {}",
156                req.method,
157                req.path
158            );
159            return Ok(req);
160        }
161
162        debug_fmt!(
163            "BasicAuthProvider",
164            "Basic Auth validating request: {} {}",
165            req.method,
166            req.path
167        );
168
169        // 1) Extract Authorization header
170        let auth_header = match req.headers.get("authorization") {
171            Some(h) => match h.to_str() {
172                Ok(s) => s,
173                Err(e) => {
174                    let err =
175                        ProxyError::SecurityError(format!("Invalid authorization header: {e}"));
176                    warn_fmt!("BasicAuthProvider", "{}", err);
177                    return Err(err);
178                }
179            },
180            None => {
181                let err = ProxyError::SecurityError("Missing authorization header".to_string());
182                warn_fmt!("BasicAuthProvider", "{}", err);
183                return Err(err);
184            }
185        };
186
187        if !auth_header.to_lowercase().starts_with(BASIC) {
188            let err = ProxyError::SecurityError(format!(
189                "Invalid authorization scheme: expected 'Basic', got '{}'",
190                auth_header.split_whitespace().next().unwrap_or("")
191            ));
192            warn_fmt!("BasicAuthProvider", "{}", err);
193            return Err(err);
194        }
195
196        let encoded_credentials = &auth_header[BASIC.len()..];
197        if encoded_credentials.is_empty() {
198            let err = ProxyError::SecurityError("Empty basic auth credentials".to_string());
199            warn_fmt!("BasicAuthProvider", "{}", err);
200            return Err(err);
201        }
202
203        // 2) Decode credentials
204        let decoded_credentials = match general_purpose::STANDARD.decode(encoded_credentials) {
205            Ok(bytes) => match String::from_utf8(bytes) {
206                Ok(s) => s,
207                Err(e) => {
208                    let err =
209                        ProxyError::SecurityError(format!("Invalid UTF-8 in credentials: {e}"));
210                    warn_fmt!("BasicAuthProvider", "{}", err);
211                    return Err(err);
212                }
213            },
214            Err(e) => {
215                let err =
216                    ProxyError::SecurityError(format!("Failed to base64 decode credentials: {e}"));
217                warn_fmt!("BasicAuthProvider", "{}", err);
218                return Err(err);
219            }
220        };
221
222        let parts: Vec<&str> = decoded_credentials.splitn(2, ':').collect();
223        if parts.len() != 2 {
224            let err = ProxyError::SecurityError("Invalid basic auth credential format".to_string());
225            warn_fmt!("BasicAuthProvider", "{}", err);
226            return Err(err);
227        }
228        let username = parts[0];
229        let password = parts[1];
230
231        // 3) Validate credentials
232        if self
233            .valid_credentials
234            .iter()
235            .any(|(u, p)| u == username && p == password)
236        {
237            debug_fmt!(
238                "BasicAuthProvider",
239                "Basic Auth validation successful for user: {}",
240                username
241            );
242            Ok(req)
243        } else {
244            let err = ProxyError::SecurityError("Invalid basic auth credentials".to_string());
245            warn_fmt!("BasicAuthProvider", "{}", err);
246            Err(err)
247        }
248    }
249}