Today Platform Web — Dev Docs
Architecture

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

ComponentResponsibility
Auth ServerOAuth2/OIDC endpoints, session storage, token issuance, social provider integration. No HTML rendering.
Web AppRenders all user-facing auth pages. Communicates with the auth server via API calls.
Native ClientOpens 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 tokens

The 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-organization

These 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:

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

AspectBFF ProxyDirect (Cross-Subdomain)
CORSNot neededRequired
Cookie scopeWeb app origin onlyShared parent domain
Auth server exposureCan be internalMust be public
Client secret handlingServer-sideRequires separate token endpoint
Latency+1 hopDirect
Implementation effortProxy route handlerCORS + cookie config
Multi-platform consistencyHigh (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.com

For 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.com

The auth server owns identity logic. The web app owns presentation. The BFF proxy bridges them without exposing the auth server directly to the browser.

On this page