Skip to content

bff

module wittgenstein_oidc_backend.bff

Backend-for-frontend OIDC actions — the /oidc token endpoint the SPA calls.

@wittgenstein/oidc-client points oidc-client-ts's token_endpoint, jwks_uri and userinfo_endpoint at ${origin}${proxyBase}/protocol/ openid-connect/... (proxyBase defaults to /oidc), so the browser only ever talks to its own origin. This router is that /oidc endpoint — and it is the OIDC client, not a pass-through:

  • The browser is redirected to Keycloak with redirect_uri = the SPA's /callback; Keycloak sends the authorization code back to the SPA.
  • The SPA posts that code here (grant_type=authorization_code); this backend exchanges it at Keycloak using the realm client's client_id + client_secret — a confidential client, so the secret never reaches the browser. The exchange returns the access/id tokens in the JSON the SPA reads, while the refresh token is set in an HttpOnly, SameSite=Lax cookie and stripped from that JSON — it never reaches JavaScript, which is what keeps a token/session from being hijacked.
  • On expiry the SPA posts grant_type=refresh_token; the refresh token comes from the cookie (never the body), this backend presents it to Keycloak with the same client_id + client_secret, rotates the cookie, and returns a fresh access token.

The client can be public (client_secret=None): Keycloak then relies on PKCE, and the code_verifier the SPA sends is forwarded. With a secret configured, the exchange is confidential and the secret stays server-side either way.

Mount it same-origin with the API (the app's nginx routes /oidc here), so no CORS is involved and the cookie is first-party. SameSite=Lax already blocks a cross-site POST from carrying the cookie; the refresh route adds an Origin check as a second fence.

Functions

wittgenstein_oidc_backend.bff.create_oidc_bff_router

create_oidc_bff_router(authority: str, client_id: str, *, client_secret: str | None = None, cookie_name: str = DEFAULT_COOKIE_NAME, cookie_path: str = '/oidc', cookie_secure: bool = True, cookie_samesite: str = 'lax', cookie_max_age: int | None = None, allowed_origins: list[str] | None = None, timeout: float = 10.0, transport: httpx.AsyncBaseTransport | None = None) → APIRouter

Build the /oidc router for one Keycloak realm.

authority is the realm URL (e.g. https://auth.example.com/realms/acme); client_id is the SPA's realm client, and client_secret its secret when the client is confidential. transport is injectable for tests only (httpx.MockTransport) — real callers omit it.

allowed_origins (when set) fences the refresh grant: a request whose Origin header is present and not listed is refused. An absent Origin is allowed (same-origin and non-browser callers do not always send one; SameSite=Lax already fences the cookie).