Skip to main content

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.sub represents the human subject and is what the BFF stores in the session as user_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 sub is 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 stores user_id = id_token.sub (or userinfo.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, loads idps.yaml, retries introspect_token, builds EnhancedTokenClaims with normalized roles/permissions.
  • Unique identity is represented as auth:{entity_type}:{idp}:{subject} (see utils/arn.py and UniqueIdentity in utils/auth.py).
  • Unique identity is represented as canonical ARNs. The provider/idp segment prefers the IdP provider alias (falls back to entry name) 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/clears bff_session and issues CSRF token for state‑changing calls.
  • Bearer path: HTTPBearer extracts the token; we do unverified decode to read iss, 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 param csrf for 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_DOMAIN to 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; otherwise BFF_CALLBACK_URL is used. In dev, set VITE_BFF_BASE_URL and use same‑origin where possible.

Security notes

  • Claims mapping supports sources from roles, groups, Keycloak resource_access client roles, and space‑delimited scope. See claims_mapping in idps.yaml.
  • Retries/backoff for introspection use tenacity (transient errors won’t result in immediate 401s).