tapir JS client: send the pac4j CSRF token on every request, and set the session cookie Secure flag behind a path callbackBase #72

Open
opened 2026-10-17 18:54:41 +00:00 by claude · 0 comments
Member

Context

MEDECA (medeca-modul-poptavky, MEDECA-552) protects its server-rendered routes with pac4j CSRF (csrfToken matcher + csrfCheck authorizer). The token is rendered into <meta name="csrf-token"> and htmx sends it as the pac4jCsrfToken header. The JSON/file API (/api, /zadatel/api) has no CSRF check yet, because the ScalaJS clients built from this library cannot send the token.

Facts (v0.1.26):

  • tapir/shared/.../LiveClientEndpointFactory.scala:31 is the single place every tapir client request is built; it already adds X-Requested-With: XMLHttpRequest.
  • tapir/js/.../CustomTapirPlatformSpecific.scala:14-23 builds the FetchZioBackend (clientLayer).
  • files/adapters/rest/js/.../LiveFileStore.scala uploads through the same factory.
  • The tapir 1.13 http4s server interpreter decodes a JSON body regardless of Content-Type, so a cross-site text/plain form reaches a JSON endpoint without a CORS preflight. Only SameSite=Lax on pac4jSessionId stops it today.
  • server/http/.../pac4j/Pac4jConfigFactory.scala:30-38 sets the session cookie secure = callbackBase.startsWith("https://"). Applications that configure callbackBase as a path (MEDECA: /oauth2) never get the Secure flag.

Proposal

  1. JS client: read the CSRF token from the page (meta[name="csrf-token"], name configurable) at request time and send it as a configurable header (default pac4jCsrfToken) on every non-GET request built by LiveClientEndpointFactory (or by a backend wrapper installed by clientLayer). No token in the page -> no header.
  2. Session cookie: make the Secure flag configurable (or derive it from the externally visible base URL) instead of inferring it from callbackBase.

Interim in MEDECA

MEDECA-552 wraps the sttp backend in the application (a DelegateSttpBackend that adds the header) and uses LiveClientEndpointFactory.layer with it. Once this issue ships, that wrapper is removed.

## Context MEDECA (medeca-modul-poptavky, MEDECA-552) protects its server-rendered routes with pac4j CSRF (`csrfToken` matcher + `csrfCheck` authorizer). The token is rendered into `<meta name="csrf-token">` and htmx sends it as the `pac4jCsrfToken` header. The JSON/file API (`/api`, `/zadatel/api`) has no CSRF check yet, because the ScalaJS clients built from this library cannot send the token. Facts (v0.1.26): - `tapir/shared/.../LiveClientEndpointFactory.scala:31` is the single place every tapir client request is built; it already adds `X-Requested-With: XMLHttpRequest`. - `tapir/js/.../CustomTapirPlatformSpecific.scala:14-23` builds the `FetchZioBackend` (`clientLayer`). - `files/adapters/rest/js/.../LiveFileStore.scala` uploads through the same factory. - The tapir 1.13 http4s server interpreter decodes a JSON body regardless of `Content-Type`, so a cross-site `text/plain` form reaches a JSON endpoint without a CORS preflight. Only `SameSite=Lax` on `pac4jSessionId` stops it today. - `server/http/.../pac4j/Pac4jConfigFactory.scala:30-38` sets the session cookie `secure = callbackBase.startsWith("https://")`. Applications that configure `callbackBase` as a path (MEDECA: `/oauth2`) never get the `Secure` flag. ## Proposal 1. JS client: read the CSRF token from the page (`meta[name="csrf-token"]`, name configurable) at request time and send it as a configurable header (default `pac4jCsrfToken`) on every non-GET request built by `LiveClientEndpointFactory` (or by a backend wrapper installed by `clientLayer`). No token in the page -> no header. 2. Session cookie: make the `Secure` flag configurable (or derive it from the externally visible base URL) instead of inferring it from `callbackBase`. ## Interim in MEDECA MEDECA-552 wraps the sttp backend in the application (a `DelegateSttpBackend` that adds the header) and uses `LiveClientEndpointFactory.layer` with it. Once this issue ships, that wrapper is removed.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
iterative-works/support#72
No description provided.