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 authorizationcodeback to the SPA. - The SPA posts that code here (
grant_type=authorization_code); this backend exchanges it at Keycloak using the realm client'sclient_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 sameclient_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
-
create_oidc_bff_router — Build the
/oidcrouter for one Keycloak realm.
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).