Skip to content

BFF — Backend For Frontend Security Proxy

Single-page applications (SPAs) that authenticate directly with an identity provider face a fundamental security problem: tokens end up in JavaScript-accessible storage.

A typical SPA uses the Authorization Code flow with PKCE to obtain tokens directly from the identity provider. The tokens are then stored in localStorage, sessionStorage, or in-memory variables — all of which are accessible to JavaScript.

sequenceDiagram
    participant SPA as SPA (Browser)
    participant IdP as Identity Provider
    participant API as Backend API

    SPA->>IdP: 1. Authorization Code + PKCE
    IdP->>SPA: 2. Authorization code
    SPA->>IdP: 3. Exchange code for tokens
    IdP->>SPA: 4. Access token + refresh token
    Note over SPA: Tokens stored in localStorage<br/>or sessionStorage
    SPA->>API: 5. API call with Bearer token
    Note right of SPA: Any JS code can read<br/>the tokens from storage

Even with PKCE (which prevents authorization code interception), the fundamental problem remains: JavaScript can read the tokens.

XSS is the real threat. A cross-site scripting vulnerability — whether from a compromised npm dependency, a malicious ad script, or a user-injected payload — gives an attacker full access to every token in the browser:

  • Access tokens let the attacker call your APIs as the victim
  • Refresh tokens let the attacker maintain access indefinitely
  • ID tokens leak personal information (name, email, roles)

The attack surface is enormous. A typical SPA has hundreds of npm dependencies, each of which can inject arbitrary JavaScript. A single postMessage handler or innerHTML assignment can open the door.

The OAuth 2.0 for Browser-Based Applications draft explicitly recommends the BFF pattern:

The backend component acts as a confidential client and handles all interaction with the authorization server […]. The browser-based application never receives tokens, and the only credential visible in the browser is the session cookie.

The OAuth 2.1 specification reinforces this by mandating PKCE and removing the implicit flow — but even with these protections, tokens in the browser remain vulnerable to XSS.

The Backend For Frontend (BFF) pattern moves all token handling server-side. The browser never sees, stores, or transmits any OAuth token. Instead, it holds a single HttpOnly session cookie that references a server-side session.

sequenceDiagram
    participant SPA as SPA (Browser)
    participant BFF as BFF Server
    participant IdP as Identity Provider
    participant API as Backend API

    SPA->>BFF: 1. Click "Login" → GET /bff/login
    BFF->>IdP: 2. Authorization Code + PKCE (302 redirect)
    IdP->>BFF: 3. Callback with authorization code
    BFF->>IdP: 4. Exchange code + client_secret + code_verifier
    IdP->>BFF: 5. Access token + refresh token + ID token
    Note over BFF: Tokens stored in Redis<br/>(encrypted, server-side only)
    BFF->>SPA: 6. Set __Host-granit-bff cookie (HTTP-only)
    SPA->>BFF: 7. API call with cookie (automatic)
    BFF->>API: 8. Proxy with Bearer token injection
    API->>BFF: 9. Response
    BFF->>SPA: 10. Response (no tokens exposed)
PropertyHow the BFF achieves it
Tokens never reach the browserCode exchange + storage happen server-side
Confidential clientBFF has a client_secret — SPAs cannot have one
XSS cannot steal tokensHttpOnly cookie is invisible to JavaScript
CSRF protectionSameSite=Strict + HMAC-SHA256 double-submit token
Session revocationDelete from Redis = instant invalidation
Automatic token refreshYARP proxy refreshes silently before expiry
BFF patternDirect SPA OIDC
Token storageServer-side (Redis)Browser (localStorage / memory)
Client typeConfidential (has secret)Public (no secret)
XSS token theftImpossible (HttpOnly cookie)Possible (JS reads tokens)
CSRF riskMitigated (SameSite + CSRF token)N/A (no cookies)
Token refreshTransparent (proxy handles it)SPA must implement silent refresh
CORS complexityNone (same origin)Must configure per API
LogoutServer clears session + RP-InitiatedClient-only, session may linger
Compliance (ISO 27001)Tokens under your controlTokens in user’s browser
Setup complexityModerate (reverse proxy)Low (but insecure by default)

Granit.Bff is split into three packages following the standard Granit module anatomy:

  • DirectoryGranit.Bff/ Abstractions, token store, CSRF generator, diagnostics
    • DirectoryInternal/
      • DistributedCacheBffTokenStore.cs IDistributedCache-backed session storage
      • HmacBffCsrfTokenGenerator.cs HMAC-SHA256 CSRF token generator
    • DirectoryDiagnostics/
      • BffMetrics.cs OpenTelemetry counters (logins, logouts, refreshes, CSRF rejections)
      • BffActivitySource.cs Distributed tracing spans
    • DirectoryOptions/
      • GranitBffOptions.cs All BFF configuration (authority, session, cookies)
    • IBffTokenStore.cs Store/retrieve/remove tokens by session ID
    • IBffCsrfTokenGenerator.cs Generate/validate CSRF tokens
    • BffTokenSet.cs Token record (access, refresh, ID, expiry)
    • GranitBffModule.cs Module registration
  • DirectoryGranit.Bff.Endpoints/ Login, callback, logout, user claims, CSRF endpoints
    • DirectoryEndpoints/
      • BffLoginEndpoints.cs OIDC login + PKCE + callback token exchange
      • BffLogoutEndpoints.cs RP-Initiated Logout + session cleanup
      • BffUserEndpoints.cs User claims for the SPA (filtered from ID token)
      • BffCsrfEndpoints.cs CSRF token generation endpoint
    • DirectoryExtensions/
      • BffEndpointRouteBuilderExtensions.cs MapGranitBffEndpoints() + security headers middleware
    • GranitBffEndpointsModule.cs Module registration
  • DirectoryGranit.Bff.Yarp/ YARP reverse proxy with token injection + CSRF validation
    • DirectoryInternal/
      • BffTokenInjectionTransform.cs Bearer token injection + silent refresh
      • BffCsrfValidationTransform.cs CSRF validation on POST/PUT/DELETE/PATCH
    • DirectoryExtensions/
      • BffYarpHostApplicationBuilderExtensions.cs AddGranitBffYarp() service registration
      • BffYarpApplicationBuilderExtensions.cs UseGranitBffYarp() middleware
    • GranitBffYarpModule.cs Module registration
PackageNuGetRole
Granit.BffGranit.BffAbstractions, IBffTokenStore, IBffCsrfTokenGenerator, options, diagnostics
Granit.Bff.EndpointsGranit.Bff.EndpointsMinimal API endpoints: /bff/login, /bff/callback, /bff/logout, /bff/user, /bff/csrf-token
Granit.Bff.YarpGranit.Bff.YarpYARP reverse proxy with Bearer injection, CSRF validation, silent token refresh
graph TD
    BFFE[Granit.Bff.Endpoints] --> BFF[Granit.Bff]
    BFFE --> DOC[Granit.Http.ApiDocumentation]
    BFFY[Granit.Bff.Yarp] --> BFF
    BFFY --> YARP[Yarp.ReverseProxy]
    BFF --> CORE[Granit]
    BFF --> SEC[Granit.Users]
    BFF --> TIME[Granit.Timing]
    BFF --> CACHE[Microsoft.Extensions.Caching.Abstractions]

    style BFF fill:#4a9eff,color:#fff
    style BFFE fill:#4a9eff,color:#fff
    style BFFY fill:#4a9eff,color:#fff

All endpoints are grouped under /bff:

MethodPathPurposeAuth required
GET/bff/loginInitiate OIDC login with PKCENo
GET/bff/callbackExchange authorization code for tokensNo
GET/bff/logoutClear session + RP-Initiated LogoutNo (idempotent)
GET/bff/userReturn filtered user claims for the SPANo (returns authenticated: false)
POST/bff/csrf-tokenGenerate a CSRF token for the sessionYes (session cookie)
  • Architecture — detailed flow diagrams for login, API calls, token refresh, and logout
  • Getting Started — step-by-step setup guide with React integration
  • Security — cookie hardening, CSRF protection, clickjacking, session management
  • YARP Proxy — route configuration, token injection pipeline, multi-service routing
  • Configuration — full appsettings.json reference, metrics, and tracing

The BFF supports FAPI 2.0 security features configured per-frontend: