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
ForwardAuthagainst 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
ForwardAuthagainst the app’s/auth/validateendpoint - 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 (
RequestContextin 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-*orX-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
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.
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.
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.
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, nottrusted 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
ForwardAuthagainst the app-owned/auth/validateendpoint. - 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_idpluspermissionsintoRequestContext.