Authentication Options and Flow
The right mental model: BFF-auth with OAuth 2.1/OIDC
- Identity source: the end-user’s identity comes from the OIDC ID token (or, when configured, the UserInfo endpoint). The
id_token.subrepresents the human subject and is what the BFF stores in the session asuser_id. - Service tokens are not identity: tokens used by the BFF to call backend services (CRUD/PDP/etc.) can be client-credential or token-exchange tokens. Their
subis often the OAuth client (for example,bff-server). That is OK and expected—they are not the user identity. - No browser token exposure: these service tokens live only server-side in the BFF; the browser never sees them and must not interpret them.
- Session flow: the browser authenticates at the IdP; the BFF receives the authorization code, exchanges it server-side, obtains an
id_token(and optionally a user-bound access token), then creates a session cookie and storesuser_id = id_token.sub(oruserinfo.sub). When the SPA calls/api/..., the BFF uses its server-held service tokens to call backends.
What we support (verified)
- Browser SPAs use a secure session with a single HttpOnly cookie (
bff_session). - Service/API callers can use bearer tokens; the BFF validates via IdP introspection when configured.
- IdP configuration is externalized in
ServiceConfigs/BFF/config/idps.yaml(issuer, audience, JWKS/introspection URLs, client credentials, claims mapping).
Two flows in one system
Where it’s implemented
- Bearer validation is in
utils/auth.py(get_current_user): decodes issuer from JWT, loadsidps.yaml, retriesintrospect_token, buildsEnhancedTokenClaimswith normalized roles/permissions. - Unique identity is represented as
auth:{entity_type}:{idp}:{subject}(seeutils/arn.pyandUniqueIdentityinutils/auth.py). - Unique identity is represented as canonical ARNs. The provider/idp segment prefers the IdP
provideralias (falls back to entryname) to keep identities stable across entries with the same issuer, e.g.,auth:account:empowernow:{sub}for both admin and CRUD audiences.
Key behaviors
- Session path: tokens never reach the browser; Traefik calls
/auth/verify(alias/auth/forward) to gate requests; BFF sets/clearsbff_sessionand issues CSRF token for state‑changing calls. - Bearer path:
HTTPBearerextracts the token; we do unverified decode to readiss, then introspection with Basic auth using IdP client credentials; we normalize roles/permissions via claims mapping.
CSRF in SPAs (contract)
- Header:
X-CSRF-Token(or query paramcsrffor GET logout flow) - Cookie name:
_eid_csrf_v1(readable by JS; HttpOnly=false) - Required for: POST, PUT, DELETE, PATCH to BFF paths (safe methods like GET/HEAD/OPTIONS skip validation)
- Example fetch usage:
await apiClient.post('/api/crud/execute', body, {
headers: { 'X-CSRF-Token': getCookie('_eid_csrf_v1') }
});
Cookies
- Session cookie name:
bff_session(HttpOnly, Secure, SameSite=Lax, domain per env) - CSRF cookie name:
_eid_csrf_v1(readable by JS, SameSite=Strict/Lax per config) - Domain guidance: set
BFF_COOKIE_DOMAINto your apex (e.g.,.ocg.labs.empowernow.ai) to enable SSO across SPAs
Callback origins
- Dynamic callback can echo the request origin when
BFF_DYNAMIC_CALLBACK=true; otherwiseBFF_CALLBACK_URLis used. In dev, setVITE_BFF_BASE_URLand use same‑origin where possible.
Security notes
- Claims mapping supports sources from
roles,groups, Keycloakresource_accessclient roles, and space‑delimitedscope. Seeclaims_mappinginidps.yaml. - Retries/backoff for introspection use
tenacity(transient errors won’t result in immediate 401s).