ADR-012: External IdP integration with WorkOS and the app-owned auth boundary

1. Status

  • Status: accepted
  • Last updated: 2026-04-07
  • Notes: WorkOS remains the external IdP, but browser login/session handling now lives in the app-owned auth boundary. Traefik protects routes with ForwardAuth against that boundary, and the boundary issues a DD internal JWT for downstream business routes.

2. Context

The system is moving toward multiple services behind a shared edge. That makes several designs unattractive:

  • treating unsigned forwarded headers as the long-term proof of identity
  • pushing raw WorkOS artifacts deep into every business route
  • duplicating browser login/session logic across multiple downstream services

The question is not whether the app should understand identity. It must. The question is where browser/session mechanics should live, and what trust artifact downstream business routes should consume.

3. Decision

We standardise on:

  • WorkOS as the external browser-login and identity provider for this slice
  • an app-owned outer auth boundary that is logically separate from the inner application
  • Traefik as the shared routing layer using ForwardAuth against the app’s /auth/validate endpoint
  • a platform-issued internal JWT as the runtime identity artifact consumed by downstream business routes

3.1 WorkOS responsibilities

WorkOS remains responsible for:

  • the external identity system and browser login experience
  • upstream session lifecycle, SSO, MFA, and directory features
  • authorization-code and refresh responses consumed by the boundary

3.2 App-owned auth-boundary responsibilities

The outer auth boundary is responsible for:

  • /auth/login, /auth/callback, /logout, and /auth/validate
  • host-based tenant resolution
  • sealed app session cookies
  • validating and normalizing WorkOS identity state
  • minting a DD internal JWT for downstream business requests

The outer auth boundary is currently deployed in the same JVM service as the inner app, but it remains a deliberate logical boundary that can be extracted later if platform topology needs it.

3.3 Inner-application responsibilities

The inner application is responsible for:

  • receiving the DD internal JWT forwarded by Traefik
  • validating signature, issuer, audience, expiry, and nbf
  • mapping verified claims into its internal identity model (RequestContext in this repo)
  • performing authorization locally

3.4 Forwarded identity artifact

The auth boundary returns a signed DD internal JWT to Traefik, which forwards it to business routes in a dedicated request header.

For this slice the canonical header is:

  • X-DataDescriber-Auth-Token

Unsigned identity headers are not the production auth contract.

3.5 Boundary implication

The platform now owns the browser-facing boundary even though WorkOS remains the external IdP.

That means:

  • no blind trust in X-User-* or X-DataDescriber-* identity assertions
  • no downstream dependence on raw WorkOS access tokens as the business-route contract
  • browser/session handling lives at the platform-owned boundary rather than in a separate external proxy layer

4. Rationale

This is the narrowest strong design for the system we are actually building:

  • multiple services behind a shared edge
  • WorkOS as the external IdP
  • a platform-owned auth boundary that can evolve independently of provider-specific details
  • no willingness to make unsigned headers the long-term identity proof

Owning the boundary in the platform gives us a cleaner contract:

  • WorkOS-specific browser/session mechanics stay concentrated in one place
  • the downstream app contract is platform-owned
  • authorization and auditability stay local to business routes
  • the outer boundary can still be extracted later without changing the inner app’s identity model

5. Alternatives Considered

  1. Keep browser auth and session handling in a separate external proxy layer

    • Rejected because it leaves too much of the platform-specific browser/session boundary outside the platform code and makes later evolution harder.
  2. Edge validates auth and services trust plain identity headers

    • Rejected because it replaces a signed credential with unsigned assertions and makes correctness depend too heavily on network hygiene.
  3. Direct WorkOS-token validation as the downstream app contract

    • Rejected because it leaks provider-specific artifacts past the boundary and makes future extraction or provider evolution harder.
  4. Dedicated internal auth-token minting service

    • Deferred.
    • It could become useful later if the outer auth boundary is extracted into its own deployable service.
    • It is added complexity that is not justified yet.

6. Consequences

  • Business routes need a stable JWT-validation pattern for the DD internal token.
  • WorkOS claim design still matters, but it is now mediated by the app-owned boundary instead of being the direct downstream contract.
  • The app contract is now app-owned boundary session -> DD internal JWT -> RequestContext, not trusted normalized identity headers -> RequestContext.
  • Browser auth/session handling is now part of the platform-owned boundary for protected browser flows.
  • Local developer bypass remains a convenience for just dev, not the deployment model.

7. Implementation Notes

  • Traefik uses ForwardAuth against the app-owned /auth/validate endpoint.
  • The auth boundary uses the WorkOS JVM SDK for authorization URL generation, code exchange, refresh, and logout URL generation.
  • The auth boundary stores a sealed app session with normalized WorkOS identity state and mints the DD internal JWT for downstream routes.
  • Business routes validate the DD internal JWT and map tenant_id plus permissions into RequestContext.

8. References