Auth interaction architecture
The headless auth server pattern — OAuth2 / OIDC server delegates login, consent, and account selection to a web app via HTTP redirects.
Overview
This document describes the authentication architecture used when an OAuth2/OIDC authorization server (auth server) has no user-facing UI. All user interactions — login, consent, account selection — are delegated to a client application (typically a web app) via HTTP redirects. The auth server remains a pure API.
This pattern is known as the interaction model (or headless auth server pattern). It separates identity logic (token issuance, session management, social provider integration) from presentation (login forms, consent screens, account pickers).
Interaction Model
Roles
| Component | Responsibility |
|---|---|
| Auth Server | OAuth2/OIDC endpoints, session storage, token issuance, social provider integration. No HTML rendering. |
| Web App | Renders all user-facing auth pages. Communicates with the auth server via API calls. |
| Native Client | Opens the web app's auth pages in a system browser or embedded WebView for login. |
Flow
When a client initiates an OAuth2 authorization request, the auth server determines whether user interaction is needed (login, consent, account selection). If so, it redirects the browser to the web app:
Client (SPA / native app)
|
|-- GET /oauth2/authorize?client_id=...&redirect_uri=...&scope=...
|
v
Auth Server
|
|-- User not logged in?
| redirect --> Web App /login?continue=<encoded_authorize_url>
|
|-- Consent required?
| redirect --> Web App /oauth/consent?...
|
|-- Multiple sessions?
| redirect --> Web App /oauth/select-account?...
|
|-- Post-login step (e.g., org selection)?
| redirect --> Web App /oauth/select-organization?...
|
|-- All interactions complete
| redirect --> Client redirect_uri with auth code
|
v
Client receives authorization code, exchanges for tokensThe web app pages communicate with the auth server API to perform the actual operations (authenticate credentials, record consent, etc.), then redirect back to the auth server to continue the OAuth flow.
Configuration
The auth server is configured with URLs pointing to the web app's interaction pages:
loginPage: https://app.example.com/login
consentPage: https://app.example.com/oauth/consent
selectAccountPage: https://app.example.com/oauth/select-account
postLoginPage: https://app.example.com/oauth/select-organizationThese can be set via an environment variable (e.g., OAUTH_UI_ORIGIN) so the same auth server image serves multiple environments by changing the config.
Communication: BFF Proxy vs. Direct
The web app's interaction pages need to call the auth server API (e.g., to submit credentials, check session status). There are two approaches:
Approach A: BFF Proxy (Recommended)
The web app includes a Backend-for-Frontend proxy layer. All auth API calls from the browser go to the web app's own origin, and the server-side proxy forwards them to the auth server.
Browser Web App Server Auth Server
| | |
|-- POST /api/auth/login --> | |
| |-- POST /api/auth/login --> |
| |<-- 200 + Set-Cookie ----- |
|<-- 200 + Set-Cookie ---- | |The proxy:
- Forwards request headers (Cookie, Content-Type, Authorization)
- Forwards response headers (Set-Cookie, Location)
- Uses
redirect: 'manual'to pass redirects through without following them - Strips hop-by-hop headers (Transfer-Encoding, Connection, etc.)
Advantages:
- No CORS configuration needed (all requests are same-origin from the browser's perspective).
- Client secrets never leave the server. Token exchange, introspection, and revocation can include secrets without exposing them to the browser.
- The auth server can be deployed on an internal network, unreachable from the public internet. The BFF proxy is the only public-facing entry point.
- Cookie scope is minimal — session cookies are set on the web app's origin only.
- Consistent behavior across all clients. Native apps open the same web pages; the BFF handles auth communication uniformly.
Trade-offs:
- Additional network hop (typically < 10ms within the same region).
- Proxy code to maintain (though it is straightforward — a single catch-all route handler).
Minimal proxy implementation (Next.js App Router):
async function proxyToAuth(request: NextRequest, segments?: string[]) {
const path = segments?.join('/') ?? ''
const target = new URL(`${AUTH_SERVER_URL}/api/auth/${path}`)
target.search = request.nextUrl.search
const response = await fetch(target, {
method: request.method,
headers: forwardHeaders(request),
body: hasBody(request) ? await request.arrayBuffer() : undefined,
redirect: 'manual',
})
return new NextResponse(await response.arrayBuffer(), {
status: response.status,
headers: stripHopByHopHeaders(response.headers),
})
}Approach B: Cross-Subdomain Cookies (Direct)
The browser calls the auth server directly. Session cookies are set on a shared parent domain so both the web app and auth server can access them.
Browser Auth Server
| |
|-- POST https://auth.example.com/login ----> |
|<-- 200 + Set-Cookie (domain=.example.com) - |
| |
|-- GET https://app.example.com/ ----------> Web App
| (cookie on .example.com is sent) |Advantages:
- Simpler architecture — no proxy layer.
- One fewer network hop.
- Auth server and web app share the same session natively.
Trade-offs:
- Requires CORS headers on the auth server (
Access-Control-Allow-Origin,Access-Control-Allow-Credentials). - Cookie domain is broadened to the parent domain (
.example.com), increasing the scope of cookie exposure. - Auth server must be publicly accessible (browser connects directly).
- Harder to add server-side logic (e.g., secret injection) without an intermediary.
Comparison
| Aspect | BFF Proxy | Direct (Cross-Subdomain) |
|---|---|---|
| CORS | Not needed | Required |
| Cookie scope | Web app origin only | Shared parent domain |
| Auth server exposure | Can be internal | Must be public |
| Client secret handling | Server-side | Requires separate token endpoint |
| Latency | +1 hop | Direct |
| Implementation effort | Proxy route handler | CORS + cookie config |
| Multi-platform consistency | High (one entry point) | Varies by client |
Recommendation
Use the BFF Proxy approach for production web applications. It aligns with the OAuth 2.0 for Browser-Based Apps recommendation and provides stronger security defaults.
The direct approach is acceptable for internal tools or environments where the auth server and web app are tightly coupled on a shared domain, and the simplicity trade-off is worthwhile.
Social Login Callback
Regardless of which approach is used, OAuth social login callbacks (e.g., Google redirecting back after authentication) are an exception. The redirect_uri registered with the social provider must point to the auth server, not the web app:
Google
|
|-- 302 --> https://auth.example.com/api/auth/callback/google?code=...
|
Auth Server
|
|-- Exchanges code for tokens with Google
|-- Creates/links user account
|-- Sets session cookie (on auth server domain)
|-- 302 --> https://app.example.com/ (callbackURL from the original request)This means:
- The browser briefly visits the auth server directly during social login.
- The auth server sets session cookies on its own domain during the callback.
- If using BFF Proxy, the web app and auth server must share a cookie domain for the session to be accessible after the redirect. Otherwise, the session established during the callback is invisible to the web app.
This is the primary reason a shared cookie domain (e.g., .example.com) is needed even with the BFF Proxy approach. The BFF proxy handles API calls, but the social callback redirect bypasses it entirely.
Domain Architecture
A typical production deployment:
app.example.com Web app (Next.js / Vercel / etc.)
auth.example.com Auth server (API only, Cloud Run / etc.)
api.example.com API gateway
Cookie domain: .example.comFor local development, a reverse proxy (e.g., Proxyman, mkcert + nginx) maps these domains to localhost ports, maintaining the same cookie domain structure.
Summary
+--------------------------------------------------+
| Browser |
+--------------------------------------------------+
| | |
v v v
app.example.com auth.example.com api.example.com
(Web App) (Auth Server) (API Gateway)
| |
| /api/auth/* ----> | (BFF proxy, server-side)
| |
| /login | (interaction page, rendered by web app)
| /oauth/consent | (interaction page, rendered by web app)
| |
| <-- redirects --- | (auth server delegates UI to web app)
| --- redirects --> | (web app completes interaction, hands back)
| |
+--------------------+
Shared cookie domain: .example.comThe auth server owns identity logic. The web app owns presentation. The BFF proxy bridges them without exposing the auth server directly to the browser.
Vercel deploy model
How Vercel turns code into a running URL — build time vs deploy time vs runtime, what `vercel.json` controls, and the four archetypes of "Vercel project linked to a Git repo".
Auth cookie current state
A current-state snapshot of cookie, domain, and auth relationships in today-platform-web. No target design, no migration plan.