Skip to content

Commit 25fc179

Browse files
authored
security fix for horizon fallback API calls not going through authn middleware (#279)
* security fix for horizon fallback API calls not going through authn middleware * Refactor tests in horizon fallback API to use fixture request builder and improve request handling. Added default API key in configuration if not provided, enhancing security and usability.
1 parent 8f66328 commit 25fc179

File tree

3 files changed

+86
-18
lines changed

3 files changed

+86
-18
lines changed

pdp-server/src/api/horizon_fallback.rs

Lines changed: 79 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -129,11 +129,74 @@ pub(super) async fn fallback_to_horizon(
129129
mod tests {
130130
use super::*;
131131
use crate::test_utils::TestFixture;
132-
use http::{Method, StatusCode};
132+
use http::{header::AUTHORIZATION, Method, StatusCode};
133133
use serde_json::json;
134134
use std::time::Duration;
135135
use wiremock::{matchers, Mock, ResponseTemplate};
136136

137+
#[tokio::test]
138+
async fn test_forward_unmatched_fail_with_wrong_auth() {
139+
let fixture = TestFixture::with_config_modifier(|config| {
140+
config.api_key = "test-token".to_string();
141+
})
142+
.await;
143+
144+
Mock::given(matchers::method("GET"))
145+
.and(matchers::path("/test"))
146+
.respond_with(ResponseTemplate::new(200).set_body_string("reached horizon unsafely"))
147+
.expect(0)
148+
.mount(&fixture.horizon_mock)
149+
.await;
150+
151+
let mut request = fixture
152+
.request_builder(Method::GET, "/test")
153+
.body(Body::empty())
154+
.expect("Failed to build request");
155+
156+
// replace authorization header with wrong token
157+
request.headers_mut().remove(AUTHORIZATION);
158+
request.headers_mut().insert(
159+
AUTHORIZATION,
160+
HeaderValue::from_str("Bearer wrong-token").unwrap(),
161+
);
162+
163+
let response = fixture.send(request).await;
164+
165+
response.assert_status(StatusCode::FORBIDDEN);
166+
assert_eq!(
167+
response.json(),
168+
"You are not authorized to access this resource, please check your API key."
169+
);
170+
171+
fixture.horizon_mock.verify().await;
172+
}
173+
174+
#[tokio::test]
175+
async fn test_forward_unmatched_fail_with_no_auth() {
176+
let fixture = TestFixture::new().await;
177+
178+
Mock::given(matchers::method("GET"))
179+
.and(matchers::path("/test"))
180+
.respond_with(ResponseTemplate::new(200).set_body_string("reached horizon unsafely"))
181+
.expect(0)
182+
.mount(&fixture.horizon_mock)
183+
.await;
184+
185+
let mut request = fixture
186+
.request_builder(Method::GET, "/test")
187+
.body(Body::empty())
188+
.expect("Failed to build request");
189+
190+
request.headers_mut().remove(AUTHORIZATION);
191+
192+
let response = fixture.send(request).await;
193+
194+
response.assert_status(StatusCode::UNAUTHORIZED);
195+
assert_eq!(response.json(), "Missing Authorization header");
196+
197+
fixture.horizon_mock.verify().await;
198+
}
199+
137200
#[tokio::test]
138201
async fn test_forward_unmatched_basic() {
139202
// Setup test fixture
@@ -309,11 +372,10 @@ mod tests {
309372
.await;
310373

311374
// Create test request
312-
let req = Request::builder()
313-
.method(method.clone())
314-
.uri("/method-test")
375+
let req = fixture
376+
.request_builder(method.clone(), "/method-test")
315377
.body(Body::empty())
316-
.unwrap();
378+
.expect("Failed to build request");
317379

318380
// Forward request
319381
let response = fixture.send(req).await;
@@ -337,11 +399,10 @@ mod tests {
337399
let fixture = TestFixture::new().await;
338400

339401
// Create test request with CONNECT method (not supported in our implementation)
340-
let req = Request::builder()
341-
.method(Method::CONNECT)
342-
.uri("/test")
402+
let req = fixture
403+
.request_builder(Method::CONNECT, "/test")
343404
.body(Body::empty())
344-
.unwrap();
405+
.expect("Failed to build request");
345406

346407
// Forward request
347408
let response = fixture.send(req).await;
@@ -362,14 +423,14 @@ mod tests {
362423
("X-Server", "Test-Server"),
363424
("Vary", "Accept-Encoding"),
364425
];
365-
426+
let auth_header = format!("Bearer {}", fixture.config.api_key);
366427
// Setup mock that demonstrates header forwarding
367428
// We use multiple headers in the request and in the response
368429
Mock::given(matchers::method("GET"))
369430
.and(matchers::path_regex(".*headers.*"))
370431
// Use header matchers to verify headers are forwarded correctly
371432
.and(matchers::header("Content-Type", "application/json"))
372-
.and(matchers::header("Authorization", "Bearer token123"))
433+
.and(matchers::header("Authorization", &auth_header))
373434
.and(matchers::header("X-Custom-Header", "custom value"))
374435
.respond_with({
375436
let mut template =
@@ -388,7 +449,7 @@ mod tests {
388449
// Create test request with multiple headers
389450
let request_headers = [
390451
("Content-Type", "application/json"),
391-
("Authorization", "Bearer token123"),
452+
("Authorization", &auth_header),
392453
("X-Custom-Header", "custom value"),
393454
("Accept-Language", "en-US,en;q=0.9"),
394455
("Cache-Control", "no-cache"),
@@ -437,14 +498,15 @@ mod tests {
437498
.await;
438499

439500
// Create test request with empty body
440-
let req = Request::builder()
441-
.method(Method::POST)
442-
.uri("/empty-body")
501+
let request = fixture
502+
.request_builder(Method::POST, "/empty-body")
443503
.body(Body::empty())
444-
.unwrap();
504+
.expect("Failed to build request");
505+
506+
let response = fixture.send(request).await;
445507

446508
// Forward request
447-
let response = fixture.send(req).await;
509+
// let response = fixture.send(req).await;
448510
response.assert_status(StatusCode::OK);
449511
assert_eq!(response.json(), "Empty body received");
450512

pdp-server/src/api/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ fn protected_routes(state: &AppState) -> Router<AppState> {
2424
.merge(authzen::router())
2525
// Add fallback route to handle any unmatched requests
2626
.fallback(any(fallback_to_horizon))
27-
.route_layer(middleware::from_fn_with_state(
27+
// we must use layer here and not route_layer because, route_layer only
28+
// affects routes that are defined on the router which doesn't affect fallback
29+
.layer(middleware::from_fn_with_state(
2830
state.clone(),
2931
authentication_middleware,
3032
))

pdp-server/src/config/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@ impl PDPConfig {
102102
}
103103
});
104104

105+
if config.api_key.is_empty() {
106+
config.api_key = "default-test-api-key".to_string();
107+
}
108+
105109
// Override with mock server addresses
106110
config.horizon.host = horizon_mock.address().ip().to_string();
107111
config.horizon.port = horizon_mock.address().port();

0 commit comments

Comments
 (0)