Skip to content

Allow ESI shared-template caching behind basic auth - #1070

Open
prk-Jr wants to merge 2 commits into
mainfrom
esi-edge-terminated-auth-main
Open

Allow ESI shared-template caching behind basic auth#1070
prk-Jr wants to merge 2 commits into
mainfrom
esi-edge-terminated-auth-main

Conversation

@prk-Jr

@prk-Jr prk-Jr commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Allow ESI shared-template caching when Trusted Server has already validated the request Basic Auth credential at the edge.
  • Preserve the fail-closed bypass for publisher-origin credentials and ambiguous repeated Authorization fields.
  • Carry the server-only authentication decision through all four adapter middleware stacks without modifying the forwarded header.

Changes

File Change
crates/trusted-server-core/src/auth.rs Add the request marker, fail closed on repeated Authorization fields, and add marker lifecycle regressions.
crates/trusted-server-core/src/publisher.rs Exempt exactly one edge-terminated credential from cache bypass and independently reject repeated values.
crates/trusted-server-adapter-fastly/src/middleware.rs Pass the mutable request through Basic Auth middleware.
crates/trusted-server-adapter-axum/src/middleware.rs Pass the mutable request through Basic Auth middleware.
crates/trusted-server-adapter-cloudflare/src/middleware.rs Pass the mutable request through Basic Auth middleware.
crates/trusted-server-adapter-spin/src/middleware.rs Pass the mutable request through Basic Auth middleware.

Test plan

  • cargo test-fastly && cargo test-axum
  • cargo test-cloudflare && cargo test-spin
  • All six target-specific Clippy gates
  • cargo fmt --all -- --check
  • Native core suite: 2,216 tests
  • Regression tests cover pass-through, edge-terminated, and repeated Authorization behavior
  • JS tests
  • JS format
  • Docs format
  • Manual staging validation

Checklist

  • Changes follow CLAUDE.md conventions
  • No unwrap calls added to production code
  • No println calls added
  • New code has regression tests
  • No secrets or credentials committed

No linked issue for this PR, per request.

@prk-Jr prk-Jr self-assigned this Aug 25, 2026
@ChristianPavilonis ChristianPavilonis changed the title Allow shared templates behind edge-terminated auth Allow ESI shared-template caching behind basic auth Aug 25, 2026
@prk-Jr prk-Jr added this to the 202608 milestone Aug 26, 2026
prk-Jr added a commit that referenced this pull request Aug 26, 2026

@ChristianPavilonis ChristianPavilonis left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

The edge-auth marker is carried consistently through all four adapters, and the focused cache and authentication tests pass. I found two medium-risk follow-ups around preserving the marker's meaning and documenting the new cache exception. The remaining comments are cleanup nitpicks for the repeated and overly long explanations.

Operator documentation

docs/guide/configuration.md:1612 still says authorization bypasses the template cache unconditionally. It should distinguish pass-through and repeated values, which still bypass, from one edge-validated Basic credential, which may now share a template. Please also document that the credential remains forwarded and that an origin using it must declare Vary: Authorization.

let authorization_value_count = req.headers().get_all(header::AUTHORIZATION).iter().count();
let request_had_authorization = match authorization_value_count {
0 => false,
1 => req

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 Bind the exemption to the Authorization value that passed authentication

This checks only that one Authorization value exists and that the marker is present. Request filters run after AuthMiddleware, and DataDome can replace Authorization through X-DataDome-request-headers because that name is not forbidden by is_forbidden_filter_header. The replacement leaves this marker intact, so a publisher-origin credential can be treated as edge-terminated and reach the shared-template lookup/store path.

Please store a digest of the uniquely validated raw value in EdgeTerminatedAuthorization and require it to match here, or clear the marker whenever a later filter mutates Authorization. A regression should cover authentication followed by header replacement.

Comment on lines +14 to +38
/// Marker recording that this request's `Authorization` header was consumed and
/// validated by a Trusted Server handler at the edge.
///
/// The shared template cache refuses every request carrying `Authorization`,
/// because an authorized response must never become a reader-neutral template.
/// That rule exists for credentials bound for the publisher origin, whose
/// response content TS cannot reason about.
///
/// A credential this edge terminated is a different case. [`enforce_basic_auth`]
/// runs as middleware ahead of routing, so a request that reaches a handler for a
/// gated path has necessarily already satisfied that same handler. Every reader
/// able to look up a template stored from such a request has authenticated
/// against the same credential, so reuse is not a cross-reader disclosure.
///
/// Absence of this marker on a request that still carries `Authorization` means
/// the credential is pass-through, and the template cache continues to refuse it.
///
/// # Invariants
///
/// The private field makes [`enforce_basic_auth`] the only code that can produce
/// this marker. It grants shared-template eligibility to a request that would
/// otherwise be refused, so being unforgeable outside this module is the whole
/// point: a caller cannot assert "already authenticated" without having actually
/// checked. [`enforce_basic_auth`] also clears any inherited marker before it
/// decides, so the value can never outlive the check that produced it.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: shorten the marker documentation

Most of this repeats the function documentation and explains privacy that the type already enforces. The invariant fits in a few lines.

Suggested change
/// Marker recording that this request's `Authorization` header was consumed and
/// validated by a Trusted Server handler at the edge.
///
/// The shared template cache refuses every request carrying `Authorization`,
/// because an authorized response must never become a reader-neutral template.
/// That rule exists for credentials bound for the publisher origin, whose
/// response content TS cannot reason about.
///
/// A credential this edge terminated is a different case. [`enforce_basic_auth`]
/// runs as middleware ahead of routing, so a request that reaches a handler for a
/// gated path has necessarily already satisfied that same handler. Every reader
/// able to look up a template stored from such a request has authenticated
/// against the same credential, so reuse is not a cross-reader disclosure.
///
/// Absence of this marker on a request that still carries `Authorization` means
/// the credential is pass-through, and the template cache continues to refuse it.
///
/// # Invariants
///
/// The private field makes [`enforce_basic_auth`] the only code that can produce
/// this marker. It grants shared-template eligibility to a request that would
/// otherwise be refused, so being unforgeable outside this module is the whole
/// point: a caller cannot assert "already authenticated" without having actually
/// checked. [`enforce_basic_auth`] also clears any inherited marker before it
/// decides, so the value can never outlive the check that produced it.
/// Marks a request whose single `Authorization` value Trusted Server validated.
///
/// The shared template cache may exempt this value from its normal authorization
/// bypass. [`enforce_basic_auth`] clears any existing marker before checking and
/// inserts a new marker only after successful authentication.

Comment on lines 4343 to +4352
// Recorded before the request is consumed by the origin send: the template cache gate
// below needs it, and an authorized response must never become a shared
// template.
let request_had_authorization = req.headers().contains_key(header::AUTHORIZATION);
//
// A credential this edge already terminated is exempt. Basic auth runs as
// middleware ahead of routing, so reaching here on a gated path means the same
// handler already validated the request; every reader that can look the template
// up has satisfied that same credential. Without the marker the credential is
// pass-through to the origin and still disqualifies. See
// [`crate::auth::EdgeTerminatedAuthorization`].

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: keep the cache-gate explanation local and short

The longer authentication argument already lives in auth.rs. This site only needs to explain the local distinction.

Suggested change
// Recorded before the request is consumed by the origin send: the template cache gate
// below needs it, and an authorized response must never become a shared
// template.
let request_had_authorization = req.headers().contains_key(header::AUTHORIZATION);
//
// A credential this edge already terminated is exempt. Basic auth runs as
// middleware ahead of routing, so reaching here on a gated path means the same
// handler already validated the request; every reader that can look the template
// up has satisfied that same credential. Without the marker the credential is
// pass-through to the origin and still disqualifies. See
// [`crate::auth::EdgeTerminatedAuthorization`].
// Capture cache eligibility before the origin send consumes the request. A marked,
// uniquely valued Authorization header was already validated by edge auth;
// unmarked or repeated values remain pass-through and bypass sharing.

Comment on lines +128 to +131
// Takes the request mutably because `enforce_basic_auth` marks requests
// whose credential it consumed itself; the shared template cache gate
// reads that marker later.
match enforce_basic_auth(&self.settings, ctx.request_mut()) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: remove the repeated middleware comment

The mutable request and call name already make this clear, while enforce_basic_auth owns the full explanation. I would remove this comment here and from the Axum, Cloudflare, and Spin copies.

Suggested change
// Takes the request mutably because `enforce_basic_auth` marks requests
// whose credential it consumed itself; the shared template cache gate
// reads that marker later.
match enforce_basic_auth(&self.settings, ctx.request_mut()) {
match enforce_basic_auth(&self.settings, ctx.request_mut()) {

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants