Skip to content

feat(bff): add server-session BFF foundation and step-up (V2)#103

Open
cuioss-oliver wants to merge 32 commits into
mainfrom
feature/bff-server-session
Open

feat(bff): add server-session BFF foundation and step-up (V2)#103
cuioss-oliver wants to merge 32 commits into
mainfrom
feature/bff-server-session

Conversation

@cuioss-oliver

@cuioss-oliver cuioss-oliver commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator

BFF Foundation + Variant 2 (Server Session)

Implements doc/plan/07-bff-server-session.adoc — the BFF foundation, stateful (server-session) shape first, driving the already-wired token-sheriff-client engine as the OIDC confidential client. The gateway owns only the browser-facing side; the engine owns every OAuth leg.

Deliverables (16)

  • D1 OIDC/session config surface — extends OidcConfig + ConfigValidator (user-info + login fold keys, session.max_sessions), gateway.schema.json mirrored.
  • D2 Pending-authorization record + browser-binding __Host- cookie (single-use, TTL, same-origin return URL, cross-browser callback rejection).
  • D3 Server-side session store + opaque __Host- session cookie (in-memory, O(1) back-channel sid/sub index, no per-session timers; token material never in the cookie).
  • D4 Reserved-path registry + OIDC callback (exact-match, OIDC host, before the route table; CallbackParameters.parse(rawQuery) for BFF-13 duplicate-param defence).
  • D5 Confidential-client login flow (engine-driven; no OAuth leg re-implemented).
  • D6 require: session stage-4 runtime + removal of the boot-time session rejection (asserting tests updated).
  • D7 Fixed CSRF defence (no knob; Origin / Sec-Fetch-Site).
  • D8 RP-initiated + back-channel logout (full gateway-side logout-token check matrix).
  • D9 Transparent single-flight refresh (TOCTOU-safe) + RFC 9470 step-up.
  • D10 Readiness + session metrics + LogRecords (BffLogMessages, doc/LogMessages.adoc).
  • D11 (fold) Session/user-info reserved endpoint — curated default + config allowlist (secure default); source is validated ID-token claims + session metadata only, never raw tokens; 401-not-redirect for XHR; Cache-Control: no-store.
  • D12 (fold) Login-initiation reserved path — reuses the D2b pending record with same-origin validation (never an open redirect); explicit already-authenticated short-circuit.
  • D16 (added mid-plan) Wire the BFF runtime into the live gateway edge — a BffRuntime + BffRuntimeProducer CDI producer so reserved paths dispatch to their handlers (not 404), require: session runs through the session-aware AuthenticationStage, and the engine seams bind with lazy, boot-safe OIDC discovery. (The ITs exposed that D1–D12 were unit-green but never invoked at the live edge; this deliverable closes that gap.)
  • D14 Session-mediation k6 benchmark + converter wiring.
  • D15 Three-layer documentation — reference (configuration.adoc reserved-path list grown four → six, every new key documented) + architecture.adoc + operator (doc/user/) + developer (doc/development/).
  • D13 Keycloak-backed BFF integration tests (6 suites + gateway oidc IT config) — see the CI note below.

Two real security/config bugs the integration tests forced out

  • Build-time-seeded SecureRandom in both the BFF records and the token-sheriff-client engine flow package — GraalVM refused to bake it into the native image heap, and it was a genuine defect (a cached seed = predictable randomness). Fixed by forcing those classes/packages to --initialize-at-run-time.
  • Literal IT oidc.client_secret — moved to a bare required ${OIDC_CLIENT_SECRET} env reference per ConfigValidator's secret-field rule.

Rebase over PLAN-05

This branch is rebased over PLAN-05 (tls-edge, #100) per the plan's second-merger rebase obligation. Conflicts in GatewayEdgeRoute, LogMessages.adoc, and both doc READMEs were resolved to keep both plans' additions; the mtls IT gateway config was brought back into lockstep with the shared BFF endpoint.

⚠️ CI / known-issue note (please read before merge)

  • Integration-test verification is delegated to CI. Locally, the api-sheriff unit suite is fully green (1034 tests) and the native image builds, but the Keycloak-backed BFF ITs could not be driven green on the local machine: after rebasing over PLAN-05, the primary container's PLAN-05 SNI front listener never binds 8443 (no error, no log), so every terminated IT — BFF, pre-existing, and PLAN-05's own MtlsHandshakeIT/TlsPassthroughIT — fails the handshake. git diff origin/main HEAD proves the entire TLS-edge / config-loading path is byte-identical to main (this branch changed only OidcConfig, ConfigValidator, the schema, and application.properties), so this reads as an environment/native-image trait of the local SNI-passthrough stack, not a BFF regression. CI is the authoritative IT environment here.
  • Treat this as a flaky image/test: if the SNI-front-listener IT failure RECURS (locally or in CI), it MUST be fixed, not dismissed as environmental. Prime suspects on recurrence: native-image CDI @Observes StartupEvent registration for TlsEdgeProducer, the --initialize-at-run-time package-prefix interaction, or topology-alias resolution timing in the merged stack.
  • One item CI should confirm: the engine's IdP-discovery TLS trust (PKIX to the self-signed IT Keycloak) was left unverified locally because the front-listener breakage blocked login. If CI shows the BFF login ITs failing on DiscoveryResolver PKIX, BffRuntimeProducer needs to wire the engine HTTP client's trust to the gateway's configured truststore.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added server-side BFF require: session support with secure session cookies, token mediation, single-flight transparent refresh, required-scope enforcement, and CSRF protection.
    • Introduced reserved OIDC endpoints for login initiation, callback, RP/logout + back-channel logout, and curated user-info with allowlists.
    • Implemented RFC 9470 step-up coordination, session/refresh/logout lifecycle event logging/metrics, in-memory session bounds, and issuer reachability in readiness checks.
  • Documentation
    • Expanded operator, contributor, architecture, configuration, variant, and log documentation for the server-session BFF.
  • Tests
    • Added comprehensive unit, integration, and benchmark coverage for the new server-session flows.

cuioss-oliver and others added 23 commits July 25, 2026 17:30
… folds

Add the two BFF operator-fold config keys to OidcConfig — a UserInfo block
(path, allowedClaims allowlist, defaultView selector) and a Login block
(path) — plus a server-mode maxSessions bound on the Session block. Add
OIDC/session-section validation rules: user-info and login paths must be
absolute gateway paths, default-view claims must lie within the operator
allowlist (empty allowlist is the secure closed default), and max_sessions
must be positive. Extend the value-object contract and validator test
suites for the new records and rules.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Af6Ah5SR43Ug7eG9HN1Yyo
…ng cookie

Introduce the D2b transient pre-login state under a new bff.pending package.
PendingAuthorizationRecord wraps the engine's FlowContext (state/nonce/PKCE/
redirect_uri) and adds the gateway-only fields the engine leaves open: a short
fixed TTL, single-use semantics, and a same-origin-validated post-login return
URL. PendingAuthorizationStore is the pluggable storage seam with a bounded,
single-use, TTL-expiring in-memory implementation (oldest-eviction DoS guard).
BindingCookieCodec sets and reads the short-lived __Host- browser-binding
cookie so a callback replayed in a different browser is rejected even with a
valid state. Unit tests cover single-use consumption, TTL expiry, bounded
eviction, return-URL same-origin validation, and cross-browser rejection.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Af6Ah5SR43Ug7eG9HN1Yyo
Introduce the D3 server-session package. SessionRecord holds the mediated
access/refresh/ID tokens plus session metadata (expiry, acr, auth_time, sid,
sub); token material never leaves the server and every credential — including
the bearer session id — is redacted from toString(). SessionStore is the store
contract; InMemorySessionStore is the only implementation, keyed by opaque id
with secondary indexes by sid/sub for O(1) back-channel destruction, an
absolute TTL enforced lazily plus by a periodic sweep (no per-session timer
threads), and a documented max-session bound. SessionCookieCodec sets and
reads the hardened __Host- session cookie carrying only the opaque id. Unit
tests cover create/resolve/destroy, lazy and swept TTL eviction, O(1)
back-channel lookup, the capacity bound, and cookie attribute assertions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Af6Ah5SR43Ug7eG9HN1Yyo
Carve the gateway's reserved OIDC paths out of the proxy route table (D2): ReservedPathRegistry does exact-match, OIDC-host-only resolution consulted ahead of route selection in GatewayEdgeRoute, so a proxy route such as path_prefix: /auth never swallows /auth/callback. CallbackEndpoint parses the raw query via CallbackParameters.parse (BFF-13 duplicate-parameter defence), resolves the browser-bound pending record via state + binding cookie, drives the engine code exchange through a CodeExchange seam, creates the server session, sets the opaque session cookie, clears the single-use binding cookie, and redirects to the recorded return URL. The seam keeps the endpoint decoupled from the deferred confidential-client producer wiring (session runtime) and unit-testable without a live token endpoint.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01Af6Ah5SR43Ug7eG9HN1Yyo
LoginFlow orchestrates the browser-facing start of the OIDC auth-code flow (D1/D2/D2b): it drives the engine to build the authorization URL and transaction FlowContext (PKCE/state/nonce owned by the engine, no OAuth leg re-implemented), persists the context as a single-use pending-authorization record, sets the __Host- browser-binding cookie, and redirects to the IdP. The requested post-login return URL is same-origin-validated (falling back to / for a cross-origin or unparseable target) so the eventual redirect is never an open redirect. The engine authorization is reached through an AuthorizationInitiation seam, mirroring the callback endpoint, keeping the flow decoupled from the deferred discovery-metadata wiring and unit-testable without a live IdP.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01Af6Ah5SR43Ug7eG9HN1Yyo
Add the SessionAuthenticationStage server-session runtime — the counterpart
of the offline bearer validation — and remove the boot-time rejection that
RouteRuntimeAssembler used to raise for require:session routes.

- SessionAuthenticationStage resolves the opaque __Host- session cookie to a
  live session, offers it to the single-flight refresh seam (D9 hook), enforces
  required_scopes against the mediated token (403 SCOPE_MISSING), and records
  the mediated access token for automatic upstream Authorization: Bearer
  injection. Unauthenticated navigation is redirected 302 into the auth-code
  flow; anything else is challenged 401 application/problem+json.
- AuthenticationStage dispatches require:session routes to the wired session
  runtime; an unwired route is a boot-configuration IllegalStateException.
- RouteRuntimeAssembler compiles require:session routes like any other route.
- ForwardPolicyStage renders the mediated bearer last so it wins over any
  allow-listed inbound Authorization; PipelineRequest carries the mediated
  bearer server-side.
- Tests: new SessionAuthenticationStageTest; RouteRuntimeAssemblerTest,
  AuthenticationStageTest, and GatewayEdgeRouteTest migrated off the removed
  boot-time session rejection.
Fixed, fail-closed CSRF defence for require:session routes (D7) with no
disable knob: unsafe methods (everything except GET/HEAD/OPTIONS) must
present a trusted Origin (exact full-origin match, scheme+host+port) or,
when Origin is absent, a Sec-Fetch-Site: same-origin header; everything
else is rejected 403 with a CSRF_REJECTED event. Parameterized tests cover
the positive/negative matrix.

Co-Authored-By: Claude <noreply@anthropic.com>
Implement both OIDC logout channels for the BFF session mode:

- RpInitiatedLogout orchestrates RP-initiated logout — best-effort
  RFC 7009 token revocation, session-bound state minted into the
  single-use __Host-sheriff-logout cookie, engine-owned end-session
  redirect (id_token_hint + exact-match post_logout_redirect_uri as
  the open-redirect defence), and constant-time state verification on
  the return leg before landing on final_redirect.
- BackchannelLogoutReceiver + LogoutTokenValidator apply the full
  OIDC back-channel-logout-token claim residual (iss/aud, iat freshness
  replay guard, backchannel-logout event, nonce-absent, sub/sid present)
  on the engine-signature-verified token, then destroy affected sessions
  via the store's O(1) secondary index.
- LogoutEndpoint and BackchannelLogoutEndpoint are the framework-agnostic
  request/response edges over the reserved logout paths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Af6Ah5SR43Ug7eG9HN1Yyo
OpenRewrite import-order normalization applied by the pre-commit recipe
during D8 verification, touching prior-deliverable bff sources and tests.
No behavioral change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Af6Ah5SR43Ug7eG9HN1Yyo
- LogoutTokenValidatorTest exercises the full fail-closed check matrix
  (iss/aud wrong, iat stale/future/absent, events missing or lacking the
  backchannel-logout event, nonce present, sub+sid absent) plus the
  sub-only / sid-only success shapes and plain-string vs array aud.
- RpInitiatedLogoutTest drives the real engine EndSessionFlow +
  PostLogoutRedirectValidator: end-session redirect assembly, the
  single-use __Host-sheriff-logout state cookie, the state round-trip on
  the return leg, and the exact post_logout_redirect_uri open-redirect
  rejection.

api-sheriff quality gate + full test suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Af6Ah5SR43Ug7eG9HN1Yyo
TokenRefreshCoordinator refreshes the mediated token within its expiry
leeway through the engine, single-flighted per session so concurrent
requests on one session share one refresh; an IdP rejection or engine
refresh-token-family reuse destroys the session so a revoked family can
never be mediated from again.

StepUpCoordinator parses an upstream RFC 9470
insufficient_user_authentication challenge, attempts silent satisfaction,
and otherwise re-drives the auth-code flow with the challenge's elevated
acr_values/max_age — persisting a single-use pending-authorization record
with a same-origin-validated replay target and a browser-binding cookie.

Both coordinators are framework-agnostic and reach the engine through
functional-interface seams; no OAuth leg is re-implemented in gateway code.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Af6Ah5SR43Ug7eG9HN1Yyo
Deliverable 10 — cross-cutting BFF observability for require:session routes.

- BffLogMessages: DSL-style LogRecord catalogue for the session lifecycle,
  transparent refresh, CSRF, and logout events on the shared ApiSheriff prefix
  with disjoint ids 10-14 (INFO) / 110-112 (WARN); records only non-sensitive
  dispositions, never session ids, subjects, sids, or token material.
- EventType: SESSION_CREATED / SESSION_DESTROYED / SESSION_REFRESH_FAILED /
  BACKCHANNEL_LOGOUT (informational) and LOGOUT_TOKEN_INVALID (400,
  AUTHENTICATION) for the back-channel logout receiver.
- SheriffMetrics: sheriff_session_events_total counter keyed by the EventType
  name (bounded cardinality), bridging GatewayEventCounter to Micrometer.
- GatewayReadinessCheck: mode:server issuer reachability, reusing the JWKS-backed
  validation health check (reachable / unreachable / unverified).
- doc/LogMessages.adoc: documents every new BFF record.

Co-Authored-By: Claude <noreply@anthropic.com>
Add the D11 session/user-info reserved gateway endpoint above the
session-resolution seam:

- UserInfoEndpoint: exact-match reserved path serving a curated identity
  view (allowlisted claims via the operator default view) plus server-held
  session metadata (expires_at, auth_time, acr). Source is validated
  ID-token claims (via the ClaimSource seam) and session metadata only,
  never raw tokens. No live session yields 401 application/problem+json
  (never a redirect); 403 is reserved for CSRF / a disallowed-claim
  request; Cache-Control: no-store on every response.
- ClaimAllowlistFilter: operator-owned claim allowlist with a secure
  closed default (empty allowlist discloses nothing); the curated default
  view is capped to the allowlist at construction.
- gateway.schema.json: mirror the D1 OidcConfig additions
  (oidc.user_info, oidc.login, oidc.session.max_sessions) so downstream
  YAML binding validates cleanly under additionalProperties:false.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the D11 test suite completing the session/user-info deliverable:

- UserInfoEndpointTest: 401-not-redirect XHR probe (problem+json, no
  Location), curated default-view disclosure, allowlist enforcement (a
  non-allowlisted claim absent from the default view; an explicitly
  requested disallowed claim rejected 403), full-view capping, no raw
  token material in any response body, and Cache-Control: no-store on
  every outcome (200 / 401 / 403). Exercised with a real
  InMemorySessionStore, the real SessionCookieCodec, and a hand-built
  ClaimSource lambda — no container, no test double framework.
- ClaimAllowlistFilterTest: secure-default-closed (empty allowlist
  discloses nothing), default-view capping to the allowlist, membership,
  order-preserving filtering, immutability, and the null-argument
  contract.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ted short-circuit

The sixth reserved gateway path (oidc.login.path) folds onto the D5 LoginFlow
and the D2b pending-authorization record: an unauthenticated caller is driven
into the auth-code flow (binding cookie set) and returns to the same-origin-
validated return URL; an already-authenticated caller short-circuits straight
to the validated return URL without a fresh auth-code flow. The return URL is
same-origin-validated on both paths, so the login-initiation URL is never an
open redirect.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Close the D13 gap: the D1-D12 BFF components were unit-green but never
invoked at the live gateway edge. This deliverable (D16) makes them
reachable.

- ReservedPathRegistry.from now also registers the user_info (D11) and
  login (D12) reserved endpoints alongside the existing four.
- GatewayEdgeRoute dispatches a matched reserved path to its handler
  (callback/logout/logout-return/back-channel/user-info/login) instead of
  rendering NO_ROUTE_MATCHED, runs require:session routes through the
  session-aware AuthenticationStage/SessionAuthenticationStage, and
  enforces the fixed CSRF defence on unsafe-method session requests.
- New BffRuntime holds the wired session stage, CSRF defence, step-up
  coordinator, and reserved-endpoint handlers, and normalizes the
  heterogeneous handler outcomes for the edge to render.
- New BffRuntimeProducer instantiates the server-mode runtime only when a
  global oidc block with session.mode=server + redirect_uri is configured
  (inert bearer-only path otherwise) and binds the token-sheriff-client
  engine seams (authorize/exchange/refresh/step-up) with lazy OIDC
  discovery so boot needs no live IdP.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…line

Add the k6 session-mediated benchmark script (session_mediated.js) modelled
on bearer_proxied.js: it establishes one server session through the gateway's
login-initiation reserved path in setup() and replays the opaque session
cookie for the whole run, measuring only the steady-state mediation hot path
(cookie -> store lookup -> bearer injection -> upstream), never the IdP-bound
login handshake.

Make K6BenchmarkConverter prefer the session-mediation aspect as the report
overview headline when present, so a BFF run leads with the mediation figure
that is directly comparable against the bearer (offline-validation) and
proxiedStatic (plain-proxy) baselines measured in the same run. Runs without
the mediation aspect keep the prior first-benchmark headline unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Grow the reserved-path list in configuration.adoc from four to six (add the
oidc.user_info and oidc.login login-initiation fold paths) and document every
new/changed OIDC/session key: oidc.login.path, oidc.user_info.path with its
allowed_claims disclosure ceiling and default_view, and oidc.session.max_sessions.

Record the server-session BFF structural additions in architecture.adoc's
Authentication Layer (session-resolution seam, reserved-path registry + live-edge
dispatch, engine boundary, and the BffRuntime / BffRuntimeProducer CDI wiring),
and add the doc-first login-initiation path, pending-authorization binding cookie,
and user-info endpoint to the Variant 2 design doc.

Add the operator setup guide (doc/user/bff-session.adoc) and contributor guide
(doc/development/bff-session.adoc), with append-only index lines in the user,
development, and top-level documentation READMEs.

Incidental: the pre-commit formatter normalized continuation-line indentation in
BffRuntime.java (record component alignment); included so the tree stays clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wire the server-mode BFF into the mounted IT gateway config and add the six
Bff*IT suites that drive a scripted, browser-less OIDC auth-code flow against
the compose Keycloak integration realm (TASK-25, deliverable 13).

Config (integration-tests/src/main/docker/sheriff-config):
- gateway.yaml: add the global server-mode `oidc` block (session.mode=server,
  memory store) wiring the confidential integration-client/integration-secret
  against the integration realm; reserved OIDC paths under /auth on the oidc
  host (localhost); add the require:session `bff-session` anchor; add the
  integration-keycloak token_validation issuer so the @GatewayValidator built
  from this block validates the realm's session tokens.
- endpoints/bff-session.yaml: the require:session protected route to the
  go-httpbin echo upstream (Authorization allow-listed, Cookie stripped) so the
  mediation ITs have a target.

Tests (integration-tests/src/test/java/.../integration):
- BffKeycloakLoginFlow: shared cookie-jar flow helper with container-network
  authority rewrite (keycloak:8443 -> localhost:1443) and disjoint gateway /
  Keycloak cookie jars.
- BffSessionLoginIT, BffSessionMediationIT, BffCsrfIT, BffLogoutIT,
  BffUserInfoIT, BffLoginInitiationIT.

Engine-seam caveats verified as authored: (a) RP-initiated logout asserts local
session destruction (the IdP TokenRevocation seam is a best-effort no-op); (b)
the back-channel receiver is asserted wired + fail-closed (a real Keycloak-issued
logout token round-trip is left to the native+Docker IT run).

Verification handoff: `verify -Pintegration-tests -pl integration-tests -am` is a
native-image + Docker (Keycloak/go-httpbin) orchestrator-tier build not run in
this leaf; `test-compile -pl integration-tests -am` passed. The orchestrator must
run the native+Docker IT build to close TASK-25 verification.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GraalVM initializes static state at image build time by default, which
baked the three BFF records' static final SecureRandom into the image
heap. This both broke the native build ("instance of Random/SplittableRandom
in the image heap") and was a real security defect: a build-time-seeded
SecureRandom yields predictable randomness from a cached seed.

Force runtime initialization via per-class --initialize-at-run-time entries
in quarkus.native.additional-build-args for:
  - de.cuioss.sheriff.gateway.bff.pending.PendingAuthorizationRecord
  - de.cuioss.sheriff.gateway.bff.session.SessionRecord
  - de.cuioss.sheriff.gateway.bff.logout.RpInitiatedLogout
The native+Docker integration stack refused to start with ApiSheriff-200:
oidc.client_secret in gateway.yaml carried a literal value, but
ConfigValidator requires a bare required ${VAR} reference. Change it to
${OIDC_CLIENT_SECRET} and inject the container-side value through the
api-sheriff service environment so EnvSecretResolver resolves it at boot.
The api-sheriff-mtls container overlays only gateway.yaml over the shared
sheriff-config dir, so it must resolve the shared endpoints/ (including the
D13 bff-session endpoint) identically to the primary gateway.yaml. Mirror
the three D13 additions the shared bff-session endpoint needs into the mtls
gateway.yaml: the bff-session anchor, the global oidc block, and the
integration-keycloak token_validation issuer. Add OIDC_CLIENT_SECRET to the
api-sheriff-mtls service so the ${OIDC_CLIENT_SECRET} reference resolves,
matching the primary service. The tls block (mtls) stays the one
intentional difference.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Sorry @cuioss-oliver, your pull request is larger than the review limit of 150000 diff characters

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: cuioss/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8f87f965-818e-4786-8a40-cca0277742ac

📥 Commits

Reviewing files that changed from the base of the PR and between e4c64ea and 205e6b3.

📒 Files selected for processing (2)
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/edge/GatewayEdgeRoute.java
  • integration-tests/src/test/java/de/cuioss/sheriff/gateway/integration/BffKeycloakLoginFlow.java
🚧 Files skipped from review as they are similar to previous changes (2)
  • integration-tests/src/test/java/de/cuioss/sheriff/gateway/integration/BffKeycloakLoginFlow.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/edge/GatewayEdgeRoute.java

📝 Walkthrough

Walkthrough

The pull request adds a server-session BFF runtime with opaque sessions, browser-bound login and callback flows, token mediation and refresh, CSRF enforcement, reserved OIDC endpoints, runtime wiring, configuration validation, integration coverage, benchmarking, and documentation.

Changes

Server-session BFF runtime

Layer / File(s) Summary
Session contracts and authentication flows
api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/{session,pending,login,refresh,csrf}/*, api-sheriff/src/main/java/de/cuioss/sheriff/gateway/auth/*
Adds opaque sessions, indexed in-memory storage, hardened cookies, pending authorization records, login initiation, delegated session authentication, token refresh, step-up coordination, and CSRF enforcement.
Reserved endpoints and runtime wiring
api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/{reserved,logout,runtime}/*, api-sheriff/src/main/java/de/cuioss/sheriff/gateway/edge/*, api-sheriff/src/main/java/de/cuioss/sheriff/gateway/quarkus/*
Adds exact reserved-path dispatch, callback, user-info, login, logout, back-channel logout, active/inert runtime assembly, readiness reporting, and mediated bearer forwarding.
Validation and delivery
api-sheriff/src/test/*, integration-tests/*, benchmarks/*, doc/*, api-sheriff/src/main/resources/*
Adds unit and integration coverage, deployment configuration, session-mediated benchmarking, benchmark overview selection, schemas, log catalogues, and BFF documentation.

Estimated code review effort: 5 (Critical) | ~120 minutes

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding the Variant 2 server-session BFF foundation and step-up support.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…OIDC discovery (token-sheriff-client#597)

The token-sheriff-client OIDC engine performs its outbound OIDC discovery/token/refresh/logout calls with only the JVM default TrustManager and exposes no per-client TLS-trust seam (token-sheriff-client#597), so the per-issuer tls_profile that validates JWKS does not cover them. Against the self-signed IT Keycloak the BFF login ITs failed PKIX/500. Pass javax.net.ssl.trustStore* as runtime -D args on the native runner command line (a native runner does not honour JAVA_TOOL_OPTIONS) for both the api-sheriff and api-sheriff-mtls services, pointing the JVM default truststore at the already-mounted private-CA PKCS12. Document the mechanism in the operator guide.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cuioss-oliver

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
api-sheriff/src/main/java/de/cuioss/sheriff/gateway/forward/ForwardPolicyStage.java (1)

93-125: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Mediated-bearer override is not guaranteed case-insensitively.

applyMediatedBearer writes the fixed key "Authorization", but copyAllowedHeaders inserts headers under whatever casing the route's headers_allow config uses — a case-sensitive Java Map. If a require: session route's forward.headers_allow names the header differently (e.g. authorization), the map ends up with two distinct entries for what is semantically one HTTP header, and this class documents (Lines 47-50) that the mediated bearer must deterministically win. Whether it actually does depends entirely on downstream iteration order over Map.copyOf(headers) (which does not preserve insertion order) and the HTTP client's case-insensitive header handling — neither of which this stage controls. That breaks the stated guarantee that "the upstream therefore sees only the mediated bearer" under a plausible config typo rather than an exotic edge case.

🐛 Suggested fix
     private static void applyMediatedBearer(PipelineRequest request, Map<String, String> headers) {
-        request.mediatedBearer().ifPresent(token -> headers.put("Authorization", "Bearer " + token));
+        request.mediatedBearer().ifPresent(token -> {
+            headers.keySet().removeIf(name -> name.equalsIgnoreCase("Authorization"));
+            headers.put("Authorization", "Bearer " + token);
+        });
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@api-sheriff/src/main/java/de/cuioss/sheriff/gateway/forward/ForwardPolicyStage.java`
around lines 93 - 125, Update applyMediatedBearer and the header-building flow
in process so the mediated bearer removes or replaces any existing Authorization
header regardless of casing before inserting the canonical Authorization entry.
Preserve the documented ordering where the mediated bearer is applied last,
ensuring the resulting headers contain only the mediated bearer for session
routes.
🧹 Nitpick comments (4)
integration-tests/src/test/java/de/cuioss/sheriff/gateway/integration/BffLoginInitiationIT.java (1)

73-76: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the validated return target.

The test passes for any non-IdP redirect, including an incorrect fallback. Assert that Location resolves to /home (and remains same-origin/relative) to cover the short-circuit contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@integration-tests/src/test/java/de/cuioss/sheriff/gateway/integration/BffLoginInitiationIT.java`
around lines 73 - 76, Update the assertions in BffLoginInitiationIT to validate
that the Location header is same-origin or relative and resolves specifically to
/home, while retaining the existing checks that it is present and does not
redirect through the IdP.
api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/reserved/ReservedPathRegistry.java (1)

104-116: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

toPath ignores the host of absolute URIs, so an off-host post_logout_redirect_uri still reserves that path on the OIDC host.

If post_logout_redirect_uri (or backchannel_path) is configured as an absolute URI on a different host, only its path component is registered — the gateway then carves that path out of the proxy route table on the OIDC host, shadowing a legitimate proxy route. Consider dropping absolute URIs whose host differs from the redirect-URI host.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/reserved/ReservedPathRegistry.java`
around lines 104 - 116, Update ReservedPathRegistry’s toPath handling for
absolute post_logout_redirect_uri and backchannel_path values: compare the URI
host with the configured redirect-URI host and ignore entries whose hosts
differ, while preserving relative paths and same-host absolute URIs. Keep the
existing path registration behavior for accepted values.
api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/reserved/package-info.java (1)

16-38: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Package doc undercounts the reserved paths (says four, but six exist).

The Javadoc enumerates only the callback, logout, logout-return, and back-channel-logout paths as the carve-out set, but this same package (and cohort) also adds UserInfoEndpoint and a login-initiation endpoint, and the ReservedEndpoint dispatch in BffRuntime switches over six cases (CALLBACK, LOGOUT, LOGOUT_RETURN, BACKCHANNEL_LOGOUT, USER_INFO, LOGIN). Since this is the doc that describes the security-relevant carve-out ahead of the proxy route table, it should enumerate all six reserved paths, not four.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/reserved/package-info.java`
around lines 16 - 38, Update the package-level Javadoc to describe six
gateway-owned reserved paths instead of four, adding the UserInfoEndpoint and
login-initiation endpoints to the existing callback, logout, logout-return, and
back-channel logout enumeration. Keep the surrounding ReservedPathRegistry and
CallbackEndpoint descriptions unchanged.
api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/runtime/SessionAuthenticationStage.java (1)

142-155: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Silent cookie truncation risk: only the first Set-Cookie is ever applied.

challenge.setCookieHeaders().stream().findFirst() discards every cookie beyond the first, and the sink itself (PipelineRequest.responseHeaders(), a Map<String,String>) cannot hold multiple values for the same header name anyway. Today LoginChallenge only ever carries the single binding cookie, so this is latent rather than active — but it silently diverges from the reserved-dispatch path (GatewayEdgeRoute.renderReserved), which correctly emits every Set-Cookie as a distinct header line via Vert.x's multi-value header API. If a future change to LoginInitiation needs to emit a second cookie (e.g. clearing a stale one alongside the new binding cookie), it will be dropped without any error.

♻️ Suggested direction

Either bound LoginChallenge.setCookieHeaders() to a single value (matching current actual usage), or route the redirect challenge's cookies through the same multi-value-capable rendering path used for reserved endpoints instead of PipelineRequest.responseHeaders().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/runtime/SessionAuthenticationStage.java`
around lines 142 - 155, Update challengeUnauthenticated and its redirect
response handling so every cookie from LoginChallenge.setCookieHeaders() is
preserved, rather than selecting only the first value through the Map-backed
responseHeaders sink. Either enforce a single-cookie contract on
LoginChallenge.setCookieHeaders() or reuse the multi-value header rendering path
used by GatewayEdgeRoute.renderReserved, ensuring future multiple Set-Cookie
values are emitted distinctly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/logout/LogoutTokenValidator.java`:
- Around line 111-114: Update the events-claim validation in
LogoutTokenValidator so it reads the claim as a JSON object rather than a plain
string, and require the BACKCHANNEL_LOGOUT_EVENT member under events before
accepting the token. Preserve the existing rejection and Optional.empty behavior
when the object claim or required member is absent, ensuring session destruction
only follows structurally valid claims.

In
`@api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/pending/PendingAuthorizationRecord.java`:
- Around line 122-145: Update PendingAuthorizationRecord.sameOrigin to reject
returnUrl values beginning with a backslash, including slash-backslash forms
such as `/\evil.com`, before the gateway-relative path check. Add coverage for
these cases in PendingAuthorizationStoreTest.ReturnUrlValidation and
LoginFlowTest.ReturnUrlGuard, preserving acceptance of valid same-origin paths.

In
`@api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/refresh/TokenRefreshCoordinator.java`:
- Around line 146-161: Remove the successful-path
sessionStore.destroyById(sessionId) call in the rotation flow of
TokenRefreshCoordinator, allowing sessionStore.create(rotated) to replace the
existing record directly. Keep session destruction in the TokenSheriffException
failure path unchanged.

In
`@api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/reserved/BackchannelLogoutEndpoint.java`:
- Around line 102-109: Update the form parsing logic in
BackchannelLogoutEndpoint’s logout-token extraction method to catch
IllegalArgumentException from decode when processing malformed percent escapes
and return Optional.empty(). Preserve the existing behavior for valid parameters
and blank values.

In
`@api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/reserved/LogoutEndpoint.java`:
- Around line 108-113: Update the logout flow around RpInitiatedLogout.initiate
so failures still destroy the local session via sessionStore.destroyById, add
the clearing cookie, and return a safe fallback redirect instead of propagating
the exception. Preserve the existing IdP redirect behavior when initiation
succeeds, and ensure cleanup occurs exactly once.

In
`@api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/runtime/JsonWriter.java`:
- Around line 53-62: Update the Number branch in JsonWriter.write to detect
non-finite Float and Double values before appending them, normalizing or
rejecting NaN and positive/negative infinity so the output remains valid JSON;
preserve existing serialization for finite numbers and other Number types.

In
`@api-sheriff/src/main/java/de/cuioss/sheriff/gateway/edge/GatewayEdgeRoute.java`:
- Around line 525-544: Update readFormBody in GatewayEdgeRoute to await the
request body with a bounded timeout instead of unconditionally calling
CompletableFuture.get(). Use the existing request/body future flow, catch
timeout failures alongside interrupted and execution failures, restore
interruption where applicable, and return null so stalled or slow reads are
rejected through the existing 400 path.

In
`@api-sheriff/src/main/java/de/cuioss/sheriff/gateway/quarkus/BffRuntimeProducer.java`:
- Around line 300-312: Update originOf to omit the URI’s default port when
constructing the origin: exclude 443 for https and 80 for http, while preserving
non-default ports and existing absolute-URI validation.

In
`@api-sheriff/src/main/java/de/cuioss/sheriff/gateway/quarkus/GatewayReadinessCheck.java`:
- Around line 134-142: Update the server-mode branch in GatewayReadinessCheck so
DATA_ISSUER_REACHABILITY is marked reachable only after probing the configured
BFF OIDC issuer used by BffRuntimeProducer for login discovery. Do not treat
gatewayValidator.get() alone as proof of issuer reachability; perform discovery
against that exact issuer, or report the status as unverified when the validator
is not demonstrably bound to it.

In `@benchmarks/src/main/resources/k6-scripts/session_mediated.js`:
- Around line 31-47: Update the session-mediated benchmark constants to target
the session-gated route via targetUrl('/bff-session/get') instead of
'/secure/get'. Change the default KEYCLOAK_USERNAME to the matching
integration/session OIDC user, while preserving the existing password and
environment-variable overrides.

In `@integration-tests/src/main/docker/sheriff-config-mtls/gateway.yaml`:
- Around line 140-144: Update the OIDC URL configuration in the mTLS gateway
overlay, including redirect_uri, logout.post_logout_redirect_uri, and
csrf.trusted_origins, to use the api-sheriff-mtls host port 10444 instead of the
primary gateway port 10443.

---

Outside diff comments:
In
`@api-sheriff/src/main/java/de/cuioss/sheriff/gateway/forward/ForwardPolicyStage.java`:
- Around line 93-125: Update applyMediatedBearer and the header-building flow in
process so the mediated bearer removes or replaces any existing Authorization
header regardless of casing before inserting the canonical Authorization entry.
Preserve the documented ordering where the mediated bearer is applied last,
ensuring the resulting headers contain only the mediated bearer for session
routes.

---

Nitpick comments:
In
`@api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/reserved/package-info.java`:
- Around line 16-38: Update the package-level Javadoc to describe six
gateway-owned reserved paths instead of four, adding the UserInfoEndpoint and
login-initiation endpoints to the existing callback, logout, logout-return, and
back-channel logout enumeration. Keep the surrounding ReservedPathRegistry and
CallbackEndpoint descriptions unchanged.

In
`@api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/reserved/ReservedPathRegistry.java`:
- Around line 104-116: Update ReservedPathRegistry’s toPath handling for
absolute post_logout_redirect_uri and backchannel_path values: compare the URI
host with the configured redirect-URI host and ignore entries whose hosts
differ, while preserving relative paths and same-host absolute URIs. Keep the
existing path registration behavior for accepted values.

In
`@api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/runtime/SessionAuthenticationStage.java`:
- Around line 142-155: Update challengeUnauthenticated and its redirect response
handling so every cookie from LoginChallenge.setCookieHeaders() is preserved,
rather than selecting only the first value through the Map-backed
responseHeaders sink. Either enforce a single-cookie contract on
LoginChallenge.setCookieHeaders() or reuse the multi-value header rendering path
used by GatewayEdgeRoute.renderReserved, ensuring future multiple Set-Cookie
values are emitted distinctly.

In
`@integration-tests/src/test/java/de/cuioss/sheriff/gateway/integration/BffLoginInitiationIT.java`:
- Around line 73-76: Update the assertions in BffLoginInitiationIT to validate
that the Location header is same-origin or relative and resolves specifically to
/home, while retaining the existing checks that it is present and does not
redirect through the IdP.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: cuioss/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b484f4b3-fd89-4b70-aadb-faefc48f76b7

📥 Commits

Reviewing files that changed from the base of the PR and between 9f5eb5d and 55be9ee.

📒 Files selected for processing (97)
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/auth/AuthenticationStage.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/BffLogMessages.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/csrf/CsrfDefence.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/csrf/package-info.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/login/LoginFlow.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/login/package-info.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/logout/BackchannelLogoutReceiver.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/logout/LogoutTokenValidator.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/logout/RpInitiatedLogout.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/logout/package-info.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/package-info.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/pending/BindingCookieCodec.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/pending/PendingAuthorizationRecord.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/pending/PendingAuthorizationStore.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/pending/package-info.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/refresh/StepUpCoordinator.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/refresh/TokenRefreshCoordinator.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/refresh/package-info.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/reserved/BackchannelLogoutEndpoint.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/reserved/CallbackEndpoint.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/reserved/ClaimAllowlistFilter.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/reserved/LoginInitiationEndpoint.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/reserved/LogoutEndpoint.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/reserved/ReservedPathRegistry.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/reserved/UserInfoEndpoint.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/reserved/package-info.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/runtime/BffRuntime.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/runtime/JsonWriter.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/runtime/SessionAuthenticationStage.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/runtime/package-info.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/session/InMemorySessionStore.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/session/SessionCookieCodec.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/session/SessionRecord.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/session/SessionStore.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/session/package-info.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/model/OidcConfig.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/validation/ConfigValidator.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/edge/GatewayEdgeRoute.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/edge/RouteRuntimeAssembler.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/events/EventType.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/forward/ForwardPolicyStage.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/pipeline/PipelineRequest.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/quarkus/BffRuntimeProducer.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/quarkus/GatewayReadinessCheck.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/quarkus/SheriffMetrics.java
  • api-sheriff/src/main/resources/application.properties
  • api-sheriff/src/main/resources/schema/gateway.schema.json
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/auth/AuthenticationStageTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/bff/csrf/CsrfDefenceTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/bff/login/LoginFlowTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/bff/logout/LogoutTokenValidatorTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/bff/logout/RpInitiatedLogoutTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/bff/pending/BindingCookieCodecTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/bff/pending/PendingAuthorizationStoreTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/bff/refresh/StepUpCoordinatorTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/bff/refresh/TokenRefreshCoordinatorTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/bff/reserved/CallbackEndpointTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/bff/reserved/ClaimAllowlistFilterTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/bff/reserved/LoginInitiationEndpointTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/bff/reserved/ReservedPathRegistryTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/bff/reserved/UserInfoEndpointTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/bff/runtime/SessionAuthenticationStageTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/bff/session/InMemorySessionStoreTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/bff/session/SessionCookieCodecTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/config/model/ConfigModelContractTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/config/validation/ConfigValidatorTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/edge/GatewayEdgePipelineTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/edge/GatewayEdgeRouteBffWiringTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/edge/GatewayEdgeRouteTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/edge/RouteRuntimeAssemblerTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/edge/WebSocketRelayStageTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/events/EventTypeTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/quarkus/BffRuntimeProducerTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/quarkus/SheriffMetricsTest.java
  • benchmarks/src/main/java/de/cuioss/sheriff/gateway/k6/benchmark/K6BenchmarkConverter.java
  • benchmarks/src/main/resources/k6-scripts/session_mediated.js
  • benchmarks/src/test/java/de/cuioss/sheriff/gateway/k6/benchmark/K6BenchmarkConverterTest.java
  • doc/LogMessages.adoc
  • doc/README.adoc
  • doc/architecture.adoc
  • doc/configuration.adoc
  • doc/development/README.adoc
  • doc/development/bff-session.adoc
  • doc/user/README.adoc
  • doc/user/bff-session.adoc
  • doc/variants/02-bff-session.adoc
  • integration-tests/docker-compose.yml
  • integration-tests/src/main/docker/sheriff-config-mtls/gateway.yaml
  • integration-tests/src/main/docker/sheriff-config/endpoints/bff-session.yaml
  • integration-tests/src/main/docker/sheriff-config/gateway.yaml
  • integration-tests/src/test/java/de/cuioss/sheriff/gateway/integration/BffCsrfIT.java
  • integration-tests/src/test/java/de/cuioss/sheriff/gateway/integration/BffKeycloakLoginFlow.java
  • integration-tests/src/test/java/de/cuioss/sheriff/gateway/integration/BffLoginInitiationIT.java
  • integration-tests/src/test/java/de/cuioss/sheriff/gateway/integration/BffLogoutIT.java
  • integration-tests/src/test/java/de/cuioss/sheriff/gateway/integration/BffSessionLoginIT.java
  • integration-tests/src/test/java/de/cuioss/sheriff/gateway/integration/BffSessionMediationIT.java
  • integration-tests/src/test/java/de/cuioss/sheriff/gateway/integration/BffUserInfoIT.java

Comment thread benchmarks/src/main/resources/k6-scripts/session_mediated.js Outdated
Comment thread integration-tests/src/main/docker/sheriff-config-mtls/gateway.yaml Outdated
…ve DslJson deserialization

The confidential-client engine parses every back-channel IdP response
(OIDC discovery, token endpoint, PAR, userinfo) with a DSL-JSON instance.
For each response type the DSL-JSON annotation processor ships a compile-time
_<Type>_DslJsonConverter implementing com.dslplatform.json.Configuration,
which DSL-JSON's ExternalConverterAnalyzer discovers by reflectively loading
the <pkg>._<Name>_DslJsonConverter class, instantiating it, and calling
configure(dslJson). That reflective loadClass+newInstance works on the JVM but
fails in native (Unable to find reader for provided type ... fallback
serialization is not registered) unless the converters are registered for
reflection.

Add TokenClientDslJsonReflection registering the four generated converters
(ProviderMetadata, TokenResponse, ParResponse, UserInfoResponse) plus their
DTOs for native reflection. Fixes native OIDC discovery against Keycloak.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
cuioss-oliver and others added 6 commits July 25, 2026 21:46
…open-redirect, local logout on failure, CSRF port normalization, bounded logout body, benchmark route

Applies the valid CodeRabbit findings on PR #103:

- LogoutTokenValidator: validate events structurally as a JSON object carrying the back-channel-logout member as a key (reject scalar/array/value-position occurrences).

- PendingAuthorizationRecord.sameOrigin: reject backslash-authority return URLs (/\evil.com etc.) that browsers normalize into protocol-relative open redirects.

- TokenRefreshCoordinator: drop the redundant pre-create destroyById on the success path (create upserts by id), closing the resolve() miss window.

- BackchannelLogoutEndpoint: fail closed to 400 on malformed percent-encoding instead of surfacing a 500.

- LogoutEndpoint: always destroy the local session and clear the cookie even when the IdP end-session redirect construction throws.

- JsonWriter: normalize non-finite doubles/floats to null so the user-info body is always valid JSON.

- GatewayEdgeRoute.readFormBody: bound the back-channel body read with a 5s deadline, fail-closed on timeout.

- BffRuntimeProducer.originOf: drop default ports (443/80) so the derived gateway origin matches the browser Origin for CSRF/same-origin checks.

- session_mediated.js: target /bff-session/get with an integration-realm user.

- sheriff-config-mtls/gateway.yaml: point redirect_uri/post_logout/trusted_origins at this instance's port 10444.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…cated require:session navigations redirect (302) instead of reaching the upstream

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ugs + smells)

Fix the SonarCloud gate blocker for PR #103: two java:S3655 reliability BUGs
(TokenRefreshCoordinator, LogoutEndpoint) that accessed an Optional value
without a guard, plus the 56 new-code code smells, taking new-code findings to
zero. Mechanical fixes (restricted-identifier renames, unnamed patterns,
single-throw assertThrows lambdas, JSpecify null-safety guards) where a fix is
correct; in-code suppressions with rationale where a fix fights the design
(S107 wiring holder, S6206 behavior class, S1075 config pointers, S2589
load-bearing Map.get guard, S2925 thread-ordering sleep).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…parsed code preserving BFF-13) and adapt the Keycloak IT flow

The engine hardcodes response_mode=form_post in the authorization request, so
Keycloak returns a 200 auto-submit form that POSTs the code/state to
/auth/callback as an application/x-www-form-urlencoded body rather than a 302
with the code in the query. The reserved callback now accepts POST: the edge
reads the form body via the existing bounded read and carries the HTTP method,
and BffRuntime.dispatch parses the code/state from the body for a POST callback
(the query for a GET), reusing the same CallbackParameters.parse so the BFF-13
duplicate-parameter rejection holds identically. The reserved dispatch already
runs before route selection, so the cross-site form_post is never subject to the
require:session CSRF guard. The Keycloak IT harness scrapes the 200 form_post
action + hidden inputs (tolerant of attribute ordering and &amp;) and replays
them to the gateway callback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… surface CI login page in IT form-scrape failure

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ters null-return

Two follow-ups surfaced by CI:

- IT harness: the login flow replays authorization/callback URLs that the
  gateway/IdP already percent-encoded. REST Assured's default
  urlEncodingEnabled=true re-encodes their literal '+' to %2B, so the scope
  "openid+profile+email" reached Keycloak as one literal scope -> invalid_scope
  (form_post error page, "login form action not found"). Disable re-encoding on
  the gateway() and keycloak() request specs so the URLs go out as-is.

- BffRuntime.callbackParameters: replace Objects.requireNonNullElse with an
  explicit null check so Sonar's dataflow recognizes the non-null return
  (clears the S2637 @NullMarked new-code reliability bug).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/runtime/SessionAuthenticationStage.java (1)

162-165: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

returnUrl() drops the query string on the post-login redirect target.

canonicalPath()/requestPath() appear to carry the path only, so a request like /report?year=2026 redirected into the login challenge returns the user to /report after authentication, losing ?year=2026. Consider appending the raw query string (mirroring GatewayEdgeRoute#renderQuery) when building the return URL.

♻️ Proposed direction
 private static String returnUrl(PipelineRequest request) {
     String canonicalPath = request.canonicalPath();
-    return canonicalPath != null ? canonicalPath : request.requestPath();
+    String path = canonicalPath != null ? canonicalPath : request.requestPath();
+    String query = renderQuery(request.queryParameters()); // reuse/extract shared helper
+    return query.isEmpty() ? path : path + query;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/runtime/SessionAuthenticationStage.java`
around lines 162 - 165, Update returnUrl() in SessionAuthenticationStage to
preserve the request’s raw query string when constructing the post-login
redirect target. Use the canonicalPath() value when available, fall back to
requestPath(), and append the query using the existing
GatewayEdgeRoute#renderQuery behavior without altering paths that have no query.
api-sheriff/src/main/java/de/cuioss/sheriff/gateway/edge/GatewayEdgeRoute.java (1)

483-497: 🩺 Stability & Availability | 🔵 Trivial

Reserved-endpoint traffic is metered under the generic no_route label.

handleReservedPath dispatches before ctx.put(ROUTE_KEY, ...) is ever set (that happens only after routeSelectionStage/requireSelectedRoute later in process()), so every login/callback/logout/user-info/backchannel-logout request records against SheriffMetrics.NO_ROUTE in recordRequestMetrics/recordError. This conflates normal BFF traffic with genuinely-unmatched requests and removes per-endpoint observability into the new reserved paths. Consider stashing a distinct route label (e.g. "reserved:" + kind) via ctx.put(ROUTE_KEY, ...) in handleReservedPath/dispatchReserved.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@api-sheriff/src/main/java/de/cuioss/sheriff/gateway/edge/GatewayEdgeRoute.java`
around lines 483 - 497, Update handleReservedPath and/or dispatchReserved to
assign ctx.put(ROUTE_KEY, ...) before reserved-endpoint dispatch or error
handling, using a distinct reserved label that includes the matched
ReservedEndpoint kind (for example, a “reserved:” prefix). Ensure
recordRequestMetrics and recordError use this label for all reserved paths
instead of SheriffMetrics.NO_ROUTE, while leaving genuinely unmatched requests
on the existing no_route label.
api-sheriff/src/test/java/de/cuioss/sheriff/gateway/edge/GatewayEdgeRouteBffWiringTest.java (1)

256-354: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

formPostRuntime() duplicates the pre-existing activeRuntime(SessionStore, SessionCookieCodec) wiring.

The two helpers build an almost-identical BffRuntime (same SessionAuthenticationStage, StepUpCoordinator, LoginFlow, BackchannelLogoutEndpoint, UserInfoEndpoint, LoginInitiationEndpoint wiring), differing only in the CallbackEndpoint exchange seam and whether pendingStore/bindingCodec are shared fields vs fresh instances. Consider extracting a shared private helper parameterized by the exchange seam (and optionally the pending store/binding codec) to avoid the ~35-line near-duplicate.

♻️ Sketch of a shared helper
-        private BffRuntime formPostRuntime() {
-            CallbackEndpoint callback = new CallbackEndpoint((context, params) -> {
-                Map<String, ClaimValue> accessClaims = new HashMap<>();
-                accessClaims.put(ClaimName.SUBJECT.getName(), ClaimValue.forPlainString(SUBJECT));
-                AccessTokenContent access = new AccessTokenContent(accessClaims, RAW_ACCESS_TOKEN);
-                Map<String, ClaimValue> idClaims = new HashMap<>();
-                idClaims.put(ClaimName.SUBJECT.getName(), ClaimValue.forPlainString(SUBJECT));
-                IdTokenContent id = new IdTokenContent(idClaims, RAW_ID_TOKEN);
-                return new AuthorizationCodeFlow.AuthenticationResult(access, id);
-            }, pendingStore, bindingCodec, sessionStore, codec, Duration.ofHours(1));
-            ... (rest duplicated from activeRuntime)
-        }
+        private BffRuntime formPostRuntime() {
+            return wireRuntime(sessionStore, codec, pendingStore, bindingCodec, (context, params) -> {
+                Map<String, ClaimValue> accessClaims = Map.of(ClaimName.SUBJECT.getName(), ClaimValue.forPlainString(SUBJECT));
+                AccessTokenContent access = new AccessTokenContent(accessClaims, RAW_ACCESS_TOKEN);
+                IdTokenContent id = new IdTokenContent(accessClaims, RAW_ID_TOKEN);
+                return new AuthorizationCodeFlow.AuthenticationResult(access, id);
+            });
+        }

Also applies to: 381-423

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@api-sheriff/src/test/java/de/cuioss/sheriff/gateway/edge/GatewayEdgeRouteBffWiringTest.java`
around lines 256 - 354, Refactor formPostRuntime() and the existing
activeRuntime(SessionStore, SessionCookieCodec) wiring to use one shared private
BffRuntime factory. Parameterize only the differing CallbackEndpoint exchange
seam and, if needed, the pendingStore and bindingCodec instances; preserve each
helper’s current stores, codecs, and behavior while removing the duplicated
SessionAuthenticationStage, StepUpCoordinator, LoginFlow, logout, backchannel,
user-info, and login wiring.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@api-sheriff/src/main/java/de/cuioss/sheriff/gateway/edge/GatewayEdgeRoute.java`:
- Around line 529-543: Update dispatchReserved and its readFormBody flow to
enforce an explicit small maximum body size before buffering reserved POST
payloads. Apply the cap to BACKCHANNEL_LOGOUT and POST CALLBACK requests, reject
or handle oversized bodies before BffRuntime.ReservedHttpRequest is created, and
preserve the existing behavior for non-body reserved paths.

---

Nitpick comments:
In
`@api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/runtime/SessionAuthenticationStage.java`:
- Around line 162-165: Update returnUrl() in SessionAuthenticationStage to
preserve the request’s raw query string when constructing the post-login
redirect target. Use the canonicalPath() value when available, fall back to
requestPath(), and append the query using the existing
GatewayEdgeRoute#renderQuery behavior without altering paths that have no query.

In
`@api-sheriff/src/main/java/de/cuioss/sheriff/gateway/edge/GatewayEdgeRoute.java`:
- Around line 483-497: Update handleReservedPath and/or dispatchReserved to
assign ctx.put(ROUTE_KEY, ...) before reserved-endpoint dispatch or error
handling, using a distinct reserved label that includes the matched
ReservedEndpoint kind (for example, a “reserved:” prefix). Ensure
recordRequestMetrics and recordError use this label for all reserved paths
instead of SheriffMetrics.NO_ROUTE, while leaving genuinely unmatched requests
on the existing no_route label.

In
`@api-sheriff/src/test/java/de/cuioss/sheriff/gateway/edge/GatewayEdgeRouteBffWiringTest.java`:
- Around line 256-354: Refactor formPostRuntime() and the existing
activeRuntime(SessionStore, SessionCookieCodec) wiring to use one shared private
BffRuntime factory. Parameterize only the differing CallbackEndpoint exchange
seam and, if needed, the pendingStore and bindingCodec instances; preserve each
helper’s current stores, codecs, and behavior while removing the duplicated
SessionAuthenticationStage, StepUpCoordinator, LoginFlow, logout, backchannel,
user-info, and login wiring.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: cuioss/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 7f71d40d-2b53-41ea-a481-c94241576124

📥 Commits

Reviewing files that changed from the base of the PR and between 55be9ee and e4c64ea.

📒 Files selected for processing (39)
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/login/LoginFlow.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/logout/LogoutTokenValidator.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/pending/PendingAuthorizationRecord.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/pending/PendingAuthorizationStore.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/refresh/StepUpCoordinator.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/refresh/TokenRefreshCoordinator.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/reserved/BackchannelLogoutEndpoint.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/reserved/CallbackEndpoint.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/reserved/ClaimAllowlistFilter.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/reserved/LoginInitiationEndpoint.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/reserved/LogoutEndpoint.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/reserved/ReservedPathRegistry.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/runtime/BffRuntime.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/runtime/JsonWriter.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/runtime/SessionAuthenticationStage.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/session/InMemorySessionStore.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/session/SessionStore.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/validation/ConfigValidator.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/edge/GatewayEdgeRoute.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/quarkus/BffRuntimeProducer.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/quarkus/TokenClientDslJsonReflection.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/bff/login/LoginFlowTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/bff/logout/LogoutTokenValidatorTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/bff/pending/PendingAuthorizationStoreTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/bff/refresh/StepUpCoordinatorTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/bff/refresh/TokenRefreshCoordinatorTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/bff/reserved/BackchannelLogoutEndpointTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/bff/reserved/CallbackEndpointTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/bff/reserved/ClaimAllowlistFilterTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/bff/reserved/LogoutEndpointTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/bff/runtime/JsonWriterTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/bff/runtime/SessionAuthenticationStageTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/bff/session/InMemorySessionStoreTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/bff/session/SessionCookieCodecTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/edge/GatewayEdgeRouteBffWiringTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/quarkus/BffRuntimeProducerTest.java
  • benchmarks/src/main/resources/k6-scripts/session_mediated.js
  • integration-tests/src/main/docker/sheriff-config-mtls/gateway.yaml
  • integration-tests/src/test/java/de/cuioss/sheriff/gateway/integration/BffKeycloakLoginFlow.java
🚧 Files skipped from review as they are similar to previous changes (28)
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/bff/session/SessionCookieCodecTest.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/session/SessionStore.java
  • integration-tests/src/main/docker/sheriff-config-mtls/gateway.yaml
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/login/LoginFlow.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/pending/PendingAuthorizationStore.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/runtime/JsonWriter.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/bff/reserved/CallbackEndpointTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/bff/login/LoginFlowTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/bff/refresh/TokenRefreshCoordinatorTest.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/reserved/BackchannelLogoutEndpoint.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/session/InMemorySessionStore.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/logout/LogoutTokenValidator.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/pending/PendingAuthorizationRecord.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/reserved/ReservedPathRegistry.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/refresh/StepUpCoordinator.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/bff/reserved/ClaimAllowlistFilterTest.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/reserved/LoginInitiationEndpoint.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/bff/pending/PendingAuthorizationStoreTest.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/quarkus/BffRuntimeProducer.java
  • benchmarks/src/main/resources/k6-scripts/session_mediated.js
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/bff/session/InMemorySessionStoreTest.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/refresh/TokenRefreshCoordinator.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/reserved/ClaimAllowlistFilter.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/bff/refresh/StepUpCoordinatorTest.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/validation/ConfigValidator.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/bff/logout/LogoutTokenValidatorTest.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/reserved/LogoutEndpoint.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/bff/reserved/CallbackEndpoint.java

Comment on lines +529 to +543
private void dispatchReserved(RoutingContext ctx, PipelineRequest request, ReservedEndpoint kind) {
String cookieHeader = request.firstHeader(COOKIE_HEADER).orElse(null);
String method = ctx.request().method().name();
// The reserved form body is read for two POST reserved paths: back-channel logout, and an OIDC
// response_mode=form_post callback (Keycloak POSTs the code/state to redirect_uri as an
// urlencoded body rather than returning a 302 with the code in the query). Both reuse the same
// bounded read; every other reserved path (and a GET callback) carries no body.
boolean callbackFormPost = kind == ReservedEndpoint.CALLBACK && "POST".equalsIgnoreCase(method);
String rawFormBody = kind == ReservedEndpoint.BACKCHANNEL_LOGOUT || callbackFormPost ? readFormBody(ctx) : null;
BffRuntime.ReservedHttpRequest reservedRequest = new BffRuntime.ReservedHttpRequest(
ctx.request().query(), cookieHeader, firstQueryParam(request, CLAIMS_PARAM),
firstQueryParam(request, RETURN_TO_PARAM), firstQueryParam(request, STATE_PARAM), rawFormBody, method);
BffRuntime.ReservedHttpResponse response = bffRuntime.dispatch(kind, reservedRequest, Instant.now());
renderReserved(ctx, request, response);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n "max-body-size|max_body_size|http.limits" api-sheriff/src/main/resources/application.properties

Repository: cuioss/API-Sheriff

Length of output: 156


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== relevant GatewayEdgeRoute section =="
file="api-sheriff/src/main/java/de/cuioss/sheriff/gateway/edge/GatewayEdgeRoute.java"
wc -l "$file"
sed -n '480,570p' "$file"

echo
echo "== readFormBody implementation/definition =="
rg -n "readFormBody|firstHeader|firstQueryParam|ReservedHttpRequest|dispatchReserved|backchannel" api-sheriff/src/main/java/de/cuioss/sheriff/gateway/edge/GatewayEdgeRoute.java

echo
echo "== max-body-size/http limits across repository =="
rg -n "max-body-size|max_body_size|max-body-size|limits/max-body-size|limit" . -g '!**/.git/**' | head -200

echo
echo "== Quarkus/Vert.x dependency references =="
rg -n "quarkus|Vertx|vertx|quarkus-vertx|quarkus-http" api-sheriff/pom.xml pom.xml api-sheriff/src -g '*.properties' -g '*.xml' | head -200

echo
echo "== application properties files =="
fd -a 'application.*properties|application.*yml|application.*yaml' api-sheriff/src/main/resources || true
for f in $(fd 'application.*properties|application.*yml|application.*yaml' api-sheriff/src/main/resources || true); do
  echo "--- $f"
  wc -l "$f"
  sed -n '1,220p' "$f"
done

Repository: cuioss/API-Sheriff

Length of output: 47843


🌐 Web query:

Quarkus 3.37 quarkus.http.limits.max-body-size default BodyHandler vert.x Router buffer body size limit

💡 Result:

In Quarkus 3.37, the default value for the configuration property quarkus.http.limits.max-body-size is 10240K (10 MB) [1][2][3]. This setting serves as a global limit enforced by the Quarkus Vert.x HTTP layer to prevent resource exhaustion from excessively large request bodies [4][5]. When a request exceeds this limit, the server will typically respond with a 413 Request Entity Too Large status code [6]. Regarding the interaction with the Vert.x Router and BodyHandler: 1. Integration: Quarkus uses Vert.x as its underlying HTTP engine [6]. The quarkus.http.limits.max-body-size configuration is designed to be respected by the Quarkus Vert.x HTTP handlers [2][6]. 2. BodyHandler Behavior: While the upstream Vert.x BodyHandler also has a default limit (which is also 10 MB, or 10,485,760 bytes) [7], the Quarkus configuration provides a unified way to manage these limits across the application without needing to manually configure individual Vert.x handlers [1][2]. 3. Buffer Pre-allocation: You may also encounter settings related to how buffers are handled. For example, when using BodyHandler, there is a mechanism to pre-allocate buffers [8][7]. By default, if pre-allocation is not enabled or optimized, the buffer may start at 1 KB and grow dynamically as data is read [8][7]. If pre-allocation is enabled based on the Content-Length header, the buffer is allocated according to that size (often capped, for instance at 64 KB in some Vert.x versions) [8][7]. If you find that the limit is not being respected in a specific context (such as certain legacy Resteasy integrations), it is typically due to how the handler is registered or processed in the request lifecycle, but in standard Quarkus applications, the quarkus.http.limits.max-body-size property is the authoritative setting [1][6].

Citations:


🌐 Web query:

Quarkus 3.37 vertx-web Router ctx.request().body() max-body-size limit BodyHandler default

💡 Result:

In Quarkus 3.37, the request body size limit is primarily governed by the Quarkus HTTP configuration, not the Vert.x BodyHandler default [1]. The default maximum size for a request body in Quarkus is 10240K (10 MB) [1]. This limit is configured via the property quarkus.http.limits.max-body-size [1]. It is important to distinguish between the two: 1. Quarkus Global Limit: Quarkus enforces a global limit on request body size using the configuration property quarkus.http.limits.max-body-size [1]. This setting applies to the application and defaults to 10240K [1]. If a request exceeds this size, the server typically rejects it before the request reaches your application-level handlers. 2. Vert.x BodyHandler Default: While the underlying Vert.x Web BodyHandler also has a default limit of 10 MB (defined as 10485760 bytes in its DEFAULT_BODY_LIMIT constant) [2][3][4], when you use Quarkus, the framework manages the HTTP server configuration [1][5]. If you are manually adding a BodyHandler to a Vert.x Router within Quarkus, you can override this by explicitly calling setBodyLimit(long) on your BodyHandler instance [2][6]. If you need to change the limit, you should update the Quarkus configuration property in your application.properties file: quarkus.http.limits.max-body-size=20M This will ensure the global limit is respected across your Quarkus application [1].

Citations:


🌐 Web query:

Quarkus 3.37 vertx.io.vertx:vertx-web 4.x ctx.request().body() maxBodySize limit

💡 Result:

In Quarkus, the maximum request body size is primarily controlled by the configuration property quarkus.http.limits.max-body-size [1]. This property is part of the Quarkus HTTP configuration and defaults to 10240K (10 MB) [2][1]. Because Quarkus manages the HTTP server lifecycle and automatically configures underlying Vert.x components, you should generally use the Quarkus configuration property rather than manually setting the setBodyLimit on a Vert.x BodyHandler [3][1]. Key details regarding this limit: 1. Configuration: You can set the limit in your application.properties (or application.yaml) file. The value accepts standard memory size formats (e.g., 10M, 10240K, 100M) [2][1]. Example: quarkus.http.limits.max-body-size=20M 2. Underlying Behavior: When quarkus.http.limits.max-body-size is defined, Quarkus automatically enforces this limit for your application routes [3]. If a request exceeds this size, the server typically returns an HTTP 413 (Request Entity Too Large) status code [4][5]. 3. Vert.x Defaults vs. Quarkus: While the underlying Vert.x BodyHandler has its own DEFAULT_BODY_LIMIT of 10 MB [6][7], Quarkus overrides this behavior through its HTTP recorder infrastructure [3]. By configuring the property in Quarkus, you ensure the global limit is applied consistently across your routes [3]. 4. Practical Recommendation: Do not attempt to override the body limit programmatically within the Vert.x BodyHandler unless you have specific, non-standard requirements that bypass the standard Quarkus HTTP configuration. Relying on quarkus.http.limits.max-body-size is the standard, supported approach for Quarkus applications [1].

Citations:


Size-limit the reserved path POST bodies before buffering them.

readFormBody(ctx) only uses a 5s timeout while ctx.request().body() still buffers the payload first. The project does not set a global body limit, so this reserved logout/callback path should apply an explicit endpoint-specific cap consistent with the expected small logout_token or code/state payload before the BFF dispatch.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@api-sheriff/src/main/java/de/cuioss/sheriff/gateway/edge/GatewayEdgeRoute.java`
around lines 529 - 543, Update dispatchReserved and its readFormBody flow to
enforce an explicit small maximum body size before buffering reserved POST
payloads. Apply the cap to BACKCHANNEL_LOGOUT and POST CALLBACK requests, reject
or handle oversized bodies before BffRuntime.ReservedHttpRequest is created, and
preserve the existing behavior for non-body reserved paths.

…uest

The OIDC response_mode=form_post callback (and back-channel logout) POST their
parameters as an x-www-form-urlencoded body. The edge pauses every request in
handle() before dispatching to a virtual thread; readFormBody then called
ctx.request().body() from that virtual thread. body() attaches its collector but
does NOT re-arm an explicitly-paused stream, so the read stalled to the 5s
deadline and returned null — the callback saw an empty body, reported "missing
state", and rejected the login 400. Every full-login IT (BffSessionLoginIT,
BffSessionMediationIT, BffUserInfoIT, BffLoginInitiationIT, BffLogoutIT) failed
this way once the earlier scope-encoding blocker was cleared.

Fix: capture the request's event-loop context in handle() before the pause, and
in readFormBody marshal the body() collector + an explicit request.resume() back
onto that context — mirroring what the proxy path's pipeTo(send) already does for
a streamed upstream body. The buffered body is then delivered and drained.
Verified end-to-end against the native stack + Keycloak: callback returns 302
(~18ms, was a 5s timeout) with the session cookie set, and a mediated
require:session request with that cookie returns 200.

Also revert the gateway() request spec to default URL encoding: it only issues a
plain GET and the callback POST, whose code/state/iss body params must be
percent-encoded by REST Assured (disabling it corrupted the body -> 400). Only
keycloak() replays a pre-encoded URL and keeps urlEncodingEnabled(false).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

1 participant