Skip to content

Backends For Frontends (BFF) Pattern — Secure Token Proxy for SPAs

The Backend For Frontend (BFF) pattern inserts a server-side proxy between a Single Page Application (SPA) and the identity provider. The proxy performs the OIDC authentication flow, stores tokens server-side, and attaches them to upstream API calls. The browser only sees an HTTP-only session cookie — tokens never reach JavaScript.

Recommended by the OAuth 2.0 for Browser-Based Apps specification. Implemented by Duende BFF, Azure AD BFF, and Granit.Bff.

flowchart TD
    SPA[SPA React] -->|cookie| BFF[BFF Server]
    BFF -->|Bearer token| API[Backend API]
    BFF -->|OIDC flows| IdP[Identity Provider]
    BFF -->|session| Redis[(Redis Cache)]

    subgraph Browser
        SPA
    end

    subgraph Server
        BFF
        Redis
    end

SPAs using authorization_code + PKCE store tokens in localStorage. Any XSS vulnerability gives an attacker full access to the user’s tokens.

Even a compromised npm dependency (supply-chain attack) can read localStorage.getItem('access_token') and exfiltrate it.

sequenceDiagram
    participant B as Browser
    participant BFF as BFF Server
    participant IdP as Identity Provider
    participant API as Backend API

    B->>BFF: GET /bff/login
    BFF->>IdP: OIDC redirect (PKCE)
    IdP->>B: Login page
    B->>IdP: Credentials
    IdP->>BFF: Callback with code
    BFF->>IdP: Exchange code → tokens (server-side)
    BFF->>BFF: Store tokens in Redis
    BFF->>B: Set-Cookie: __Host-granit-bff

    Note over B,BFF: Subsequent API calls

    B->>BFF: GET /api/data (cookie auto-sent)
    BFF->>BFF: Load tokens from Redis
    BFF->>API: GET /api/data + Authorization: Bearer
    API->>BFF: 200 OK
    BFF->>B: 200 OK

Three packages following the standard Granit module anatomy:

PackagePurpose
Granit.BffToken store (Redis), CSRF (HMAC-SHA256), session cookies, diagnostics
Granit.Bff.Endpoints/bff/login, /bff/logout, /bff/user, /bff/csrf-token
Granit.Bff.YarpYARP reverse proxy with automatic Bearer token injection
  • Multi-frontend — serve multiple SPAs (Admin, Patient, Médecin) from one backend, each with isolated cookies, sessions, and OIDC clients
  • Automatic silent refresh — tokens refreshed server-side before expiry, transparent to the SPA
  • CSRF protection — HMAC-SHA256 double-submit cookie pattern, validated before proxying mutations
  • YARP hybrid routingGranit.Bff.RequireAuth metadata controls which routes get token injection; others proxied as-is
  • Provider agnostic — works with OpenIddict, Keycloak, Entra ID, any OIDC
PropertyImplementation
XSS protectionTokens never in browser, cookies HttpOnly
CSRF protectionX-CSRF-Token header required on mutations
Cookie security__Host- prefix, Secure, SameSite=Strict
Token storageRedis, encrypted via ICacheValueEncryptor
ClickjackingX-Frame-Options: DENY on login routes
ScenarioUse BFF?
React/Vue/Angular SPAYes
Next.js (SSR)No — tokens server-side natively
Mobile app (iOS/Android)No — secure Keychain, no XSS
API-to-API (M2M)No — use client_credentials
Blazor ServerNo — tokens server-side natively
{
"Bff": {
"Authority": "https://auth.example.com",
"Frontends": [
{
"Name": "admin",
"ClientId": "my-admin-bff",
"ClientSecret": "...",
"PathPrefix": "/admin",
"StaticFilesPath": "wwwroot/admin"
}
]
}
}
FilePurpose
src/Granit.Bff/Options/GranitBffOptions.csOptions + BffFrontendOptions
src/Granit.Bff/IBffTokenStore.csToken storage interface
src/Granit.Bff/IBffCsrfTokenGenerator.csCSRF token interface
src/Granit.Bff.Endpoints/Extensions/BffEndpointRouteBuilderExtensions.csEndpoint registration
src/Granit.Bff.Yarp/Internal/BffTokenInjectionTransform.csYARP token injection