Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions crates/trusted-server-adapter-fastly/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,13 @@ use log_fastly::Logger;

use trusted_server_core::auction::endpoints::handle_auction;
use trusted_server_core::auction::{build_orchestrator, AuctionOrchestrator};
use trusted_server_core::auth::enforce_basic_auth;
use trusted_server_core::compat;
use trusted_server_core::constants::{
ENV_FASTLY_IS_STAGING, ENV_FASTLY_SERVICE_VERSION, HEADER_X_GEO_INFO_AVAILABLE,
HEADER_X_TS_ENV, HEADER_X_TS_VERSION,
};
use trusted_server_core::error::TrustedServerError;
use trusted_server_core::geo::GeoInfo;
use trusted_server_core::http_util::sanitize_forwarded_headers;
use trusted_server_core::integrations::IntegrationRegistry;
use trusted_server_core::platform::RuntimeServices;
use trusted_server_core::proxy::{
Expand Down Expand Up @@ -97,7 +96,7 @@ async fn route_request(
// Strip client-spoofable forwarded headers at the edge.
// On Fastly this service IS the first proxy — these headers from
// clients are untrusted and can hijack URL rewriting (see #409).
sanitize_forwarded_headers(&mut req);
compat::sanitize_forwarded_headers_fastly(&mut req);

// Look up geo info via the platform abstraction using the client IP
// already captured in RuntimeServices at the entry point.
Expand All @@ -112,7 +111,7 @@ async fn route_request(
// `get_settings()` should already have rejected invalid handler regexes.
// Keep this fallback so manually-constructed or otherwise unprepared
// settings still become an error response instead of panicking.
match enforce_basic_auth(settings, &req) {
match compat::enforce_basic_auth_fastly(settings, &req) {
Ok(Some(mut response)) => {
finalize_response(settings, geo_info.as_ref(), &mut response);
return Ok(response);
Expand Down
12 changes: 6 additions & 6 deletions crates/trusted-server-core/src/auction/endpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@ use error_stack::{Report, ResultExt};
use fastly::{Request, Response};

use crate::auction::formats::AdRequest;
use crate::compat;
use crate::consent;
use crate::cookies::handle_request_cookies;
use crate::edge_cookie::get_or_generate_ec_id;
use crate::error::TrustedServerError;
use crate::geo::GeoInfo;
use crate::platform::RuntimeServices;
Expand Down Expand Up @@ -49,13 +48,14 @@ pub async fn handle_auction(

// Generate EC ID early so the consent pipeline can use it for
// KV Store fallback/write operations.
let ec_id =
get_or_generate_ec_id(settings, &req).change_context(TrustedServerError::Auction {
let ec_id = compat::get_or_generate_ec_id_fastly(settings, &req).change_context(
TrustedServerError::Auction {
message: "Failed to generate EC ID".to_string(),
})?;
},
)?;

// Extract consent from request cookies, headers, and geo.
let cookie_jar = handle_request_cookies(&req)?;
let cookie_jar = compat::handle_request_cookies_fastly(&req)?;
#[allow(deprecated)]
let geo = GeoInfo::from_request(&req);
let consent_context = consent::build_consent_context(&consent::ConsentPipelineInput {
Expand Down
10 changes: 6 additions & 4 deletions crates/trusted-server-core/src/auction/formats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ use std::collections::HashMap;
use uuid::Uuid;

use crate::auction::context::ContextValue;
use crate::compat;
use crate::consent::ConsentContext;
use crate::constants::{HEADER_X_TS_EC, HEADER_X_TS_EC_FRESH};
use crate::creative;
use crate::edge_cookie::generate_ec_id;
use crate::error::TrustedServerError;
use crate::geo::GeoInfo;
use crate::openrtb::{to_openrtb_i32, OpenRtbBid, OpenRtbResponse, ResponseExt, SeatBid, ToExt};
Expand Down Expand Up @@ -88,9 +88,11 @@ pub fn convert_tsjs_to_auction_request(
ec_id: &str,
) -> Result<AuctionRequest, Report<TrustedServerError>> {
let ec_id = ec_id.to_owned();
let fresh_id = generate_ec_id(settings, req).change_context(TrustedServerError::Auction {
message: "Failed to generate fresh EC ID".to_string(),
})?;
let fresh_id = compat::generate_ec_id_fastly(settings, req).change_context(
TrustedServerError::Auction {
message: "Failed to generate fresh EC ID".to_string(),
},
)?;

// Convert ad units to slots
let mut slots = Vec::new();
Expand Down
97 changes: 58 additions & 39 deletions crates/trusted-server-core/src/auth.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use base64::{engine::general_purpose::STANDARD, Engine as _};
use edgezero_core::body::Body;
use error_stack::Report;
use fastly::http::{header, StatusCode};
use fastly::{Request, Response};
use http::{header, StatusCode};
use sha2::{Digest as _, Sha256};
use subtle::ConstantTimeEq as _;

Expand All @@ -27,9 +27,10 @@ const BASIC_AUTH_REALM: &str = r#"Basic realm="Trusted Server""#;
/// un-compilable path regex.
pub fn enforce_basic_auth(
settings: &Settings,
req: &Request,
) -> Result<Option<Response>, Report<TrustedServerError>> {
let Some(handler) = settings.handler_for_path(req.get_path())? else {
req: &http::Request<Body>,
) -> Result<Option<http::Response<Body>>, Report<TrustedServerError>> {
let path = req.uri().path();
let Some(handler) = settings.handler_for_path(path)? else {
return Ok(None);
};

Expand All @@ -53,14 +54,15 @@ pub fn enforce_basic_auth(
if bool::from(username_match & password_match) {
Ok(None)
} else {
log::warn!("Basic auth failed for path: {}", req.get_path());
log::warn!("Basic auth failed for path: {}", path);
Ok(Some(unauthorized_response()))
}
}

fn extract_credentials(req: &Request) -> Option<(String, String)> {
fn extract_credentials(req: &http::Request<Body>) -> Option<(String, String)> {
let header_value = req
.get_header(header::AUTHORIZATION)
.headers()
.get(header::AUTHORIZATION)
.and_then(|value| value.to_str().ok())?;

let mut parts = header_value.splitn(2, ' ');
Expand All @@ -84,25 +86,43 @@ fn extract_credentials(req: &Request) -> Option<(String, String)> {
Some((username, password))
}

fn unauthorized_response() -> Response {
Response::from_status(StatusCode::UNAUTHORIZED)
.with_header(header::WWW_AUTHENTICATE, BASIC_AUTH_REALM)
.with_header(header::CONTENT_TYPE, "text/plain; charset=utf-8")
.with_body_text_plain("Unauthorized")
fn unauthorized_response() -> http::Response<Body> {
http::Response::builder()
.status(StatusCode::UNAUTHORIZED)
.header(header::WWW_AUTHENTICATE, BASIC_AUTH_REALM)
.header(header::CONTENT_TYPE, "text/plain; charset=utf-8")
.body(Body::from("Unauthorized"))
.expect("should build unauthorized response")
}

#[cfg(test)]
mod tests {
use super::*;
use base64::engine::general_purpose::STANDARD;
use fastly::http::{header, Method};

use crate::test_support::tests::{crate_test_settings_str, create_test_settings};

fn make_request(method: &str, uri: &str) -> http::Request<Body> {
http::Request::builder()
.method(method)
.uri(uri)
.body(Body::empty())
.expect("should build test request")
}

fn make_request_with_auth(method: &str, uri: &str, token: &str) -> http::Request<Body> {
http::Request::builder()
.method(method)
.uri(uri)
.header(header::AUTHORIZATION, format!("Basic {token}"))
.body(Body::empty())
.expect("should build test request with auth header")
}

#[test]
fn no_challenge_for_non_protected_path() {
let settings = create_test_settings();
let req = Request::new(Method::GET, "https://example.com/open");
let req = make_request("GET", "https://example.com/open");

assert!(enforce_basic_auth(&settings, &req)
.expect("should evaluate auth")
Expand All @@ -112,24 +132,24 @@ mod tests {
#[test]
fn challenge_when_missing_credentials() {
let settings = create_test_settings();
let req = Request::new(Method::GET, "https://example.com/secure");
let req = make_request("GET", "https://example.com/secure");

let response = enforce_basic_auth(&settings, &req)
.expect("should evaluate auth")
.expect("should challenge");
assert_eq!(response.get_status(), StatusCode::UNAUTHORIZED);
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
let realm = response
.get_header(header::WWW_AUTHENTICATE)
.headers()
.get(header::WWW_AUTHENTICATE)
.expect("should have WWW-Authenticate header");
assert_eq!(realm, BASIC_AUTH_REALM);
}

#[test]
fn allow_when_credentials_match() {
let settings = create_test_settings();
let mut req = Request::new(Method::GET, "https://example.com/secure/data");
let token = STANDARD.encode("user:pass");
req.set_header(header::AUTHORIZATION, format!("Basic {token}"));
let req = make_request_with_auth("GET", "https://example.com/secure/data", &token);

assert!(enforce_basic_auth(&settings, &req)
.expect("should evaluate auth")
Expand All @@ -139,29 +159,27 @@ mod tests {
#[test]
fn challenge_when_both_credentials_wrong() {
let settings = create_test_settings();
let mut req = Request::new(Method::GET, "https://example.com/secure/data");
let token = STANDARD.encode("wrong:wrong");
req.set_header(header::AUTHORIZATION, format!("Basic {token}"));
let req = make_request_with_auth("GET", "https://example.com/secure/data", &token);

let response = enforce_basic_auth(&settings, &req)
.expect("should evaluate auth")
.expect("should challenge");
assert_eq!(response.get_status(), StatusCode::UNAUTHORIZED);
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}

#[test]
fn challenge_when_username_wrong_password_correct() {
// Validates that both fields are always evaluated — no short-circuit username oracle.
let settings = create_test_settings();
let mut req = Request::new(Method::GET, "https://example.com/secure/data");
let token = STANDARD.encode("wrong-user:pass");
req.set_header(header::AUTHORIZATION, format!("Basic {token}"));
let req = make_request_with_auth("GET", "https://example.com/secure/data", &token);

let response = enforce_basic_auth(&settings, &req)
.expect("should evaluate auth")
.expect("should challenge");
assert_eq!(
response.get_status(),
response.status(),
StatusCode::UNAUTHORIZED,
"should reject wrong username even with correct password"
);
Expand All @@ -170,15 +188,14 @@ mod tests {
#[test]
fn challenge_when_username_correct_password_wrong() {
let settings = create_test_settings();
let mut req = Request::new(Method::GET, "https://example.com/secure/data");
let token = STANDARD.encode("user:wrong-pass");
req.set_header(header::AUTHORIZATION, format!("Basic {token}"));
let req = make_request_with_auth("GET", "https://example.com/secure/data", &token);

let response = enforce_basic_auth(&settings, &req)
.expect("should evaluate auth")
.expect("should challenge");
assert_eq!(
response.get_status(),
response.status(),
StatusCode::UNAUTHORIZED,
"should reject correct username with wrong password"
);
Expand All @@ -187,13 +204,17 @@ mod tests {
#[test]
fn challenge_when_scheme_is_not_basic() {
let settings = create_test_settings();
let mut req = Request::new(Method::GET, "https://example.com/secure");
req.set_header(header::AUTHORIZATION, "Bearer token");
let req = http::Request::builder()
.method("GET")
.uri("https://example.com/secure")
.header(header::AUTHORIZATION, "Bearer token")
.body(Body::empty())
.expect("should build test request");

let response = enforce_basic_auth(&settings, &req)
.expect("should evaluate auth")
.expect("should challenge");
assert_eq!(response.get_status(), StatusCode::UNAUTHORIZED);
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}

#[test]
Expand All @@ -210,9 +231,8 @@ mod tests {
#[test]
fn allow_admin_path_with_valid_credentials() {
let settings = create_test_settings();
let mut req = Request::new(Method::POST, "https://example.com/admin/keys/rotate");
let token = STANDARD.encode("admin:admin-pass");
req.set_header(header::AUTHORIZATION, format!("Basic {token}"));
let req = make_request_with_auth("POST", "https://example.com/admin/keys/rotate", &token);

assert!(
enforce_basic_auth(&settings, &req)
Expand All @@ -225,24 +245,23 @@ mod tests {
#[test]
fn challenge_admin_path_with_wrong_credentials() {
let settings = create_test_settings();
let mut req = Request::new(Method::POST, "https://example.com/admin/keys/rotate");
let token = STANDARD.encode("admin:wrong");
req.set_header(header::AUTHORIZATION, format!("Basic {token}"));
let req = make_request_with_auth("POST", "https://example.com/admin/keys/rotate", &token);

let response = enforce_basic_auth(&settings, &req)
.expect("should evaluate auth")
.expect("should challenge admin path with wrong credentials");
assert_eq!(response.get_status(), StatusCode::UNAUTHORIZED);
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}

#[test]
fn challenge_admin_path_with_missing_credentials() {
let settings = create_test_settings();
let req = Request::new(Method::POST, "https://example.com/admin/keys/rotate");
let req = make_request("POST", "https://example.com/admin/keys/rotate");

let response = enforce_basic_auth(&settings, &req)
.expect("should evaluate auth")
.expect("should challenge admin path with missing credentials");
assert_eq!(response.get_status(), StatusCode::UNAUTHORIZED);
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
}
Loading
Loading