ADR-0013: Optional OAuth Plug-In Architecture
Context and Problem Statement
The IDP BFF servers need to support authenticated users while remaining deployable without any OAuth dependency. Different deployments require different OAuth providers (local testing, GitHub, and future enterprise IdPs), and contract testing requires a fully automated, provider-independent path that does not depend on live external services.
How should the IDP architecture integrate OAuth authentication so that:
- The default deployment requires no auth configuration?
- Auth can be layered on through environment-driven provider selection?
- Contract tests can exercise the full OAuth round-trip without a browser or external service?
- New providers can be added without rewriting the core BFF logic?
Gate Assessment
- Cross-cutting scope — provider selection, session cookie strategy, auth
routes, contract test profile (
auth-profile), and future provider implementations are all affected. - Costly to reverse — once session cookie names and auth route paths are published and consumed by front-end code and tests, changing them requires coordinated migration.
- Contract surface — defines the HTTP auth interface (
/auth/login,/auth/callback,/auth/logout,/auth/me), the session cookie contract, and the opt-in conformance model (auth-profilevs. default profiles). - Multi-quarter longevity — provider abstraction and session cookie conventions will be relevant for the lifetime of any auth-enabled IDP deployment.
- Drift risk — without a recorded model, future stacks may implement auth with incompatible routes, cookie names, or provider-selection conventions.
All five gates are true. Two are hard gates (costly to reverse, contract surface). The intake threshold is met.
Decision Drivers
- Auth must be opt-in so that the default IDP stack works without any OAuth configuration — no environment variable required, no routes registered
- The provider selection mechanism must be a single, well-known environment variable so that deployment environments can be switched without code changes
- Contract tests must be fully automated — no browser interaction, no external service dependency
- The auth contract must be testable against any compliant BFF implementation, not just the Go reference stack
- New OAuth providers must be addable without modifying route registration or session logic
Considered Options
- Bundle a single fixed OAuth provider in the BFF (no plug-in model)
- Implement auth as an optional plug-in selected by environment variable, with each stack free to choose its own OAuth client implementation
- Implement auth as an optional plug-in selected by environment variable, using only Go standard library HTTP primitives in every reference stack
Decision Outcome
Chosen option: "Implement auth as an optional plug-in selected by environment variable, with each stack free to choose its own OAuth client implementation", because the durable decision is the shared auth contract, not a single library choice. This keeps the default BFF free of mandatory auth configuration, preserves a single provider-selection model across stacks, and still enables automated contract testing through the dedicated mock OAuth service without requiring a browser.
Provider Selection
Auth is enabled by setting OUR_IDP_OAUTH_PROVIDER to a recognized provider
name. When the variable is unset or set to "none", no auth routes are
registered and the BFF behaves identically to a non-auth deployment.
OUR_IDP_OAUTH_PROVIDER | Behavior |
|---|---|
none (default, or unset) | No auth routes registered; BFF runs without OAuth |
mock | Auth routes registered using the local mock OAuth service |
github | Auth routes registered using the GitHub OAuth App flow |
The default is none for backward compatibility. All existing non-auth BFF
behavior is preserved exactly when OUR_IDP_OAUTH_PROVIDER is absent.
Auth Routes (registered only when provider ≠ none)
| Method | Path | Purpose |
|---|---|---|
GET | /auth/login | Redirect to provider authorization URL with CSRF state |
GET | /auth/callback | Exchange code for token, fetch user profile, set session cookie |
POST | /auth/logout | Clear session and expire session cookie |
GET | /auth/me | Return authenticated user profile, or 401 |
Session Cookie
| Property | Value |
|---|---|
| Name | idp_session |
| HttpOnly | true |
| SameSite | Lax |
| Secure | Controlled by OUR_IDP_OAUTH_SECURE_COOKIE=true; defaults to false for local HTTP |
| Path | / |
Reference Stack Implementations
The auth contract is shared across reference stacks, but the OAuth client
implementation remains a stack-level detail. A stack may use standard-library
HTTP primitives, a maintained ecosystem OAuth client, or a framework-native
integration as long as it preserves the published contract defined in this ADR
and passes auth-profile.
The current Go reference stack (stacks/go/net-http/rest) resolves provider
endpoints from environment variables and presently implements the flow with
repo-local logic. A follow-on refactor may adopt golang.org/x/oauth2
without requiring another contract change, because the library choice is not
itself part of the durable architecture.
Provider endpoint variables used by the mock provider:
| Variable | Default |
|---|---|
OUR_IDP_OAUTH_AUTH_URL | http://127.0.0.1:<OUR_IDP_OAUTH_MOCK_PORT>/oauth/authorize |
OUR_IDP_OAUTH_TOKEN_URL | http://127.0.0.1:<OUR_IDP_OAUTH_MOCK_PORT>/oauth/token |
OUR_IDP_OAUTH_USERINFO_URL | http://127.0.0.1:<OUR_IDP_OAUTH_MOCK_PORT>/userinfo |
OUR_IDP_OAUTH_MOCK_PORT | 9000 |
The github provider uses fixed GitHub API URLs and requires
OUR_IDP_OAUTH_CLIENT_ID, OUR_IDP_OAUTH_CLIENT_SECRET, and
OUR_IDP_OAUTH_REDIRECT_URL.
Other stacks are expected to follow the same contract while selecting the implementation approach that best matches their ecosystem. The Node.js React + Fastify reference stack, for example, may use maintained Fastify-compatible OAuth and session libraries when its auth capability is implemented.
Mock OAuth Service for Automated Testing
Automated contract testing of the auth flow requires a fully programmable
OAuth service that responds to the authorization code grant without a
browser. The IDP uses a dedicated mock OAuth service (tools/mock-oauth) —
a Spring Boot application that exists solely for test purposes and is never
deployed to any real environment.
The mock service exposes the minimal OAuth 2.0 authorization code endpoints:
| Path | Purpose |
|---|---|
GET /oauth/authorize | Issues an authorization redirect with a predictable code |
POST /oauth/token | Exchanges the code for a test access token |
GET /userinfo | Returns a minimal test user profile |
Using a dedicated mock service (rather than stubbing the BFF internals) verifies the actual HTTP round-trip — the same path exercised in real deployments — while remaining fully deterministic and requiring no external network access.
auth-profile Conformance Profile
Auth contract testing is implemented as a distinct, opt-in conformance
profile (auth-profile) rather than an extension of the default core or
operational profiles.
A stack must explicitly declare both of the following in stack.json to
activate auth contract testing:
{
"contractProfiles": ["core", "operational", "auth-profile"],
"capabilities": {
"auth": {
"enabled": true
}
}
}
When either declaration is absent, the auth-profile scenarios are skipped automatically. This ensures that stacks without auth support are not penalized in CI.
Consequences
- Good, because the default BFF requires no OAuth configuration and all
existing behavior is fully preserved when
OUR_IDP_OAUTH_PROVIDERis absent ornone - Good, because the mock OAuth service enables fully automated, browser-free contract testing of the complete OAuth round-trip
- Good, because provider-endpoint URLs are resolvable from environment variables, making the test harness portable across CI and local environments
- Good, because the opt-in
auth-profilemodel means non-auth stacks never fail auth contract checks - Good, because the architecture stays focused on stable behavior while allowing each stack to adopt the OAuth client approach that best fits its ecosystem
- Bad, because each new provider requires an explicit implementation in the BFF — there is no generic OIDC auto-discovery in this MVP
- Bad, because the mock OAuth service requires Java 21, which is not managed
by the repo's
prototoolchain — contributors must install it separately - Neutral, because the
githubprovider path cannot be fully exercised by the automated contract test harness; browser-based or manual testing is required for real GitHub OAuth flows
Confirmation
stacks/go/net-http/rest/bff/auth.goimplements provider selection viaOUR_IDP_OAUTH_PROVIDERand registers routes only when provider ≠nonetools/mock-oauthprovides the Spring Boot mock OAuth service used by automated auth contract teststests/features/auth-profile.featurespecifies the Layer 1 auth intenttests/src/profiles/auth-profile.tsimplements the Layer 2 contract tests against theauth-profileconformance profile- Auth-capable stacks declare
auth-profileincontractProfilesandcapabilities.auth.enabled = trueinstack.json
Pros and Cons of the Options
Bundle a single fixed OAuth provider
- Good, because simplest initial implementation
- Bad, because all deployments inherit OAuth dependencies even when auth is not needed
- Bad, because switching providers requires code changes
- Bad, because contract tests cannot run without a specific provider
Optional plug-in selected by environment variable with stack-specific implementation choice (chosen)
- Good, because default deployment is auth-free with no code or env changes
- Good, because a single
OUR_IDP_OAUTH_PROVIDERvariable controls the entire auth surface — no per-endpoint configuration required in the common case - Good, because the mock provider enables fully automated testing without any browser or live external service
- Good, because endpoint URL overrides allow the same BFF binary to target different environments without recompilation
- Good, because the durable contract stays stable even if one stack later migrates from repo-local HTTP handling to a maintained OAuth library
- Good, because stack implementations can adopt ecosystem libraries for protocol handling, session integration, or framework ergonomics
- Bad, because each new provider must be explicitly implemented in BFF code
- Bad, because ecosystem-library choices still add dependency review and version-maintenance overhead inside each stack that adopts them
Optional plug-in selected by environment variable using only Go standard library HTTP primitives in every stack
- Good, because avoids external OAuth dependency drift entirely
- Good, because makes the implementation model uniform across stacks
- Bad, because pushes protocol and error-handling edge cases into each stack's custom code
- Bad, because it constrains non-Go stacks to implementation rules that do not naturally match their ecosystem
- Bad, because the library choice is reversible implementation detail, not the durable decision this ADR needs to record
More Information
Future Extension Path
This ADR covers the current auth contract scope: mock and github
providers, with session-cookie-based state and opt-in auth-profile
conformance. Future work may include:
- General OIDC federation (Okta, Azure AD, Google) — requires provider auto-discovery from the OIDC well-known configuration endpoint
- Token-based stateless sessions (JWT) — replaces the in-process session store
- Multi-provider selection at runtime — requires a provider registry rather than a switch statement
These extensions are explicitly out of scope for this ADR. When they are implemented, a follow-on ADR should be recorded if the changes are cross-cutting and expensive to reverse.
References
Related Decisions
- 0001 — Layered intent/contract/implementation architecture
- 0003 — Contract harness mechanics and runtime port defaults
- 0005 — Profile-based conformance model (auth-profile is an opt-in profile)
- 0009 — Gherkin as the Layer 1 intent
specification format (auth intent is in
tests/features/auth-profile.feature)