alibi_core/middleware/cors/
mod.rs1use super::Middleware;
2use crate::error::AuthResult;
3use crate::types::{AuthRequest, AuthResponse, HttpMethod};
4use async_trait::async_trait;
5
6#[derive(Debug, Clone)]
8pub struct CorsConfig {
9 pub allowed_origins: Vec<String>,
12
13 pub allowed_methods: Vec<String>,
15
16 pub allowed_headers: Vec<String>,
18
19 pub exposed_headers: Vec<String>,
21
22 pub allow_credentials: bool,
24
25 pub max_age: u64,
27
28 pub enabled: bool,
30}
31
32impl Default for CorsConfig {
33 fn default() -> Self {
34 Self {
35 allowed_origins: Vec::new(),
36 allowed_methods: vec![
37 "GET".into(),
38 "POST".into(),
39 "PUT".into(),
40 "DELETE".into(),
41 "PATCH".into(),
42 "OPTIONS".into(),
43 ],
44 allowed_headers: vec![
45 "Content-Type".into(),
46 "Authorization".into(),
47 "X-Requested-With".into(),
48 ],
49 exposed_headers: Vec::new(),
50 allow_credentials: true,
51 max_age: 86400,
52 enabled: true,
53 }
54 }
55}
56
57impl CorsConfig {
58 #[must_use]
59 pub fn new() -> Self {
60 Self::default()
61 }
62
63 #[must_use]
64 pub fn allowed_origin(mut self, origin: impl Into<String>) -> Self {
65 self.allowed_origins.push(origin.into());
66 self
67 }
68
69 #[must_use]
70 pub const fn allow_credentials(mut self, allow: bool) -> Self {
71 self.allow_credentials = allow;
72 self
73 }
74
75 #[must_use]
76 pub const fn max_age(mut self, seconds: u64) -> Self {
77 self.max_age = seconds;
78 self
79 }
80
81 #[must_use]
82 pub const fn enabled(mut self, enabled: bool) -> Self {
83 self.enabled = enabled;
84 self
85 }
86}
87
88#[derive(Debug)]
92pub struct CorsMiddleware {
93 config: CorsConfig,
94}
95
96impl CorsMiddleware {
97 #[must_use]
98 pub const fn new(config: CorsConfig) -> Self {
99 Self { config }
100 }
101
102 fn is_origin_allowed(&self, origin: &str) -> bool {
103 self.config
104 .allowed_origins
105 .iter()
106 .any(|allowed| allowed == "*" || allowed == origin)
107 }
108
109 fn allowed_origin<'a>(&self, req: &'a AuthRequest) -> Option<&'a str> {
111 req.headers
112 .get("origin")
113 .map(String::as_str)
114 .filter(|origin| self.is_origin_allowed(origin))
115 }
116
117 fn cors_headers(&self, origin: &str) -> Vec<(String, String)> {
118 let config = &self.config;
119 let allow_origin =
121 if !config.allow_credentials && config.allowed_origins.iter().any(|o| o == "*") {
122 "*"
123 } else {
124 origin
125 };
126 let mut headers = vec![(
127 "Access-Control-Allow-Origin".to_owned(),
128 allow_origin.to_owned(),
129 )];
130 if config.allow_credentials {
131 headers.push(("Access-Control-Allow-Credentials".into(), "true".into()));
132 }
133 for (name, values) in [
134 ("Access-Control-Allow-Methods", &config.allowed_methods),
135 ("Access-Control-Allow-Headers", &config.allowed_headers),
136 ("Access-Control-Expose-Headers", &config.exposed_headers),
137 ] {
138 if !values.is_empty() {
139 headers.push((name.into(), values.join(", ")));
140 }
141 }
142 headers.push(("Access-Control-Max-Age".into(), config.max_age.to_string()));
143 headers
144 }
145}
146
147#[async_trait]
148impl Middleware for CorsMiddleware {
149 fn name(&self) -> &'static str {
150 "cors"
151 }
152
153 async fn before_request(&self, req: &AuthRequest) -> AuthResult<Option<AuthResponse>> {
154 if !self.config.enabled || req.method != HttpMethod::Options {
155 return Ok(None);
156 }
157 Ok(self.allowed_origin(req).map(|origin| {
158 self.cors_headers(origin)
159 .into_iter()
160 .fold(AuthResponse::new(204), |response, (key, value)| {
161 response.with_header(key, value)
162 })
163 }))
164 }
165
166 async fn after_request(
167 &self,
168 req: &AuthRequest,
169 mut response: AuthResponse,
170 ) -> AuthResult<AuthResponse> {
171 if self.config.enabled
172 && let Some(origin) = self.allowed_origin(req)
173 {
174 for (key, value) in self.cors_headers(origin) {
175 _ = response.headers.insert(key, value);
176 }
177 }
178 Ok(response)
179 }
180}
181
182#[cfg(test)]
184mod tests {
185 use super::*;
186 use std::collections::HashMap;
187
188 fn make_options(origin: &str) -> AuthRequest {
189 let mut headers = HashMap::new();
190 headers.insert("origin".to_owned(), origin.to_owned());
191 AuthRequest::from_parts(
192 HttpMethod::Options,
193 "/sign-in/email".to_owned(),
194 headers,
195 None,
196 HashMap::new(),
197 )
198 }
199
200 fn make_get(origin: &str) -> AuthRequest {
201 let mut headers = HashMap::new();
202 headers.insert("origin".to_owned(), origin.to_owned());
203 AuthRequest::from_parts(
204 HttpMethod::Get,
205 "/get-session".to_owned(),
206 headers,
207 None,
208 HashMap::new(),
209 )
210 }
211
212 #[tokio::test]
214 async fn test_cors_preflight_allowed() {
215 let config = CorsConfig::new().allowed_origin("http://localhost:5173");
216 let mw = CorsMiddleware::new(config);
217 let req = make_options("http://localhost:5173");
218
219 let resp = mw.before_request(&req).await.unwrap();
220 assert!(resp.is_some());
221 let resp = resp.unwrap();
222 assert_eq!(resp.status, 204);
223 assert_eq!(
224 resp.headers.get("Access-Control-Allow-Origin").unwrap(),
225 "http://localhost:5173"
226 );
227 }
228
229 #[tokio::test]
231 async fn test_cors_preflight_not_allowed() {
232 let config = CorsConfig::new().allowed_origin("http://localhost:5173");
233 let mw = CorsMiddleware::new(config);
234 let req = make_options("http://evil.com");
235
236 let resp = mw.before_request(&req).await.unwrap();
237 assert!(resp.is_none()); }
239
240 #[tokio::test]
242 async fn test_cors_adds_headers_after_request() {
243 let config = CorsConfig::new().allowed_origin("http://localhost:5173");
244 let mw = CorsMiddleware::new(config);
245 let req = make_get("http://localhost:5173");
246
247 let response = AuthResponse::json(200, &serde_json::json!({"ok": true})).unwrap();
248 let response = mw.after_request(&req, response).await.unwrap();
249
250 assert_eq!(
251 response.headers.get("Access-Control-Allow-Origin").unwrap(),
252 "http://localhost:5173"
253 );
254 assert_eq!(
255 response
256 .headers
257 .get("Access-Control-Allow-Credentials")
258 .unwrap(),
259 "true"
260 );
261 }
262
263 #[tokio::test]
265 async fn test_cors_no_origin_header() {
266 let config = CorsConfig::new().allowed_origin("http://localhost:5173");
267 let mw = CorsMiddleware::new(config);
268 let req = AuthRequest::from_parts(
269 HttpMethod::Get,
270 "/get-session".to_owned(),
271 HashMap::new(),
272 None,
273 HashMap::new(),
274 );
275
276 assert!(mw.before_request(&req).await.unwrap().is_none());
277
278 let response = AuthResponse::new(200);
279 let response = mw.after_request(&req, response).await.unwrap();
280 assert!(!response.headers.contains_key("Access-Control-Allow-Origin"));
281 }
282
283 #[tokio::test]
285 async fn test_cors_wildcard() {
286 let config = CorsConfig::new()
287 .allowed_origin("*")
288 .allow_credentials(false);
289 let mw = CorsMiddleware::new(config);
290 let req = make_get("http://any-origin.com");
291
292 let response = AuthResponse::new(200);
293 let response = mw.after_request(&req, response).await.unwrap();
294 assert_eq!(
295 response.headers.get("Access-Control-Allow-Origin").unwrap(),
296 "*"
297 );
298 }
299}
300