---
name: duende-bff
description: Duende BFF (Backend for Frontend) security framework for securing SPAs. Covers session management, API endpoint proxying, token management, anti-forgery protection, and integration with React/Angular/Blazor frontends.
invocable: false
---

# Duende BFF (Backend for Frontend)

## When to Use This Skill

- Building or securing a SPA (React, Angular, Vue, Blazor WASM) that calls APIs requiring authentication
- Implementing the Backend-for-Frontend security pattern to keep access tokens out of the browser
- Configuring BFF session management, login/logout endpoints, and server-side sessions
- Proxying requests from a frontend to remote APIs while automatically attaching access tokens
- Adding CSRF/anti-forgery protection to APIs consumed by browser-based applications
- Integrating `Duende.BFF` with `Duende.AccessTokenManagement` for automatic token refresh
- Deploying a BFF behind a reverse proxy or configuring same-site cookie behavior

## Core Principles

1. **Tokens Never Touch the Browser** — The BFF holds all OAuth tokens server-side; the browser only ever sees an HTTP-only, Secure, SameSite cookie
2. **CSRF Protection Is Mandatory** — Every BFF API endpoint must require the `X-CSRF: 1` header; use `.AsBffApiEndpoint()` or `MapRemoteBffApiEndpoint` — never skip it without an explicit alternative
3. **Cookie Configuration Determines Security Posture** — `SameSite=Strict` is preferred when the IDP is on the same site; `Lax` is acceptable when cross-site redirects are required after login
4. **Server-Side Sessions for Production** — The default in-memory cookie session is unsuitable for production; persist sessions with `Duende.BFF.EntityFramework`
5. **Token Management Is Automatic** — BFF integrates with `Duende.AccessTokenManagement`; never manually refresh tokens or pass raw access tokens to the frontend

Docs: https://docs.duendesoftware.com/bff/

---

## Pattern 1: Setup and Registration (BFF v4)

BFF v4 uses a streamlined registration API that auto-configures OpenID Connect and cookie authentication with recommended defaults.

```csharp
// ✅ v4: AddBff() with fluent OIDC and cookie configuration
builder.Services.AddBff()
    .ConfigureOpenIdConnect(options =>
    {
        options.Authority = "https://your-idp.example.com";
        options.ClientId = "my-bff-client";
        options.ClientSecret = "secret";
        options.ResponseType = "code";
        options.ResponseMode = "query";

        options.GetClaimsFromUserInfoEndpoint = true;
        options.SaveTokens = true;
        options.MapInboundClaims = false;

        options.Scope.Clear();
        options.Scope.Add("openid");
        options.Scope.Add("profile");
        options.Scope.Add("offline_access"); // Required for refresh tokens
    })
    .ConfigureCookies(options =>
    {
        // Use Strict when your IDP is on the same site as the BFF.
        // Use Lax when a cross-site redirect is required (e.g., IDP on a different domain).
        options.Cookie.SameSite = SameSiteMode.Lax;
    });

builder.Services.AddAuthorization();

var app = builder.Build();

app.UseRouting();
app.UseAuthentication();
app.UseBff();          // Adds CSRF anti-forgery enforcement middleware
app.UseAuthorization();

app.Run();
```

```csharp
// ❌ v4: Do NOT manually wire AddCookie + AddOpenIdConnect when using AddBff()
// ConfigureOpenIdConnect and ConfigureCookies handle this correctly
builder.Services.AddAuthentication()
    .AddCookie("cookie")
    .AddOpenIdConnect("oidc", ...); // Bypasses BFF's recommended defaults
```

### BFF v3 Registration

For projects still on v3, explicit scheme setup is required and `MapBffManagementEndpoints()` must be called manually:

```csharp
// ✅ v3: explicit authentication scheme wiring
builder.Services.AddBff();

builder.Services
    .AddAuthentication(options =>
    {
        options.DefaultScheme = "cookie";
        options.DefaultChallengeScheme = "oidc";
        options.DefaultSignOutScheme = "oidc";
    })
    .AddCookie("cookie", options =>
    {
        options.Cookie.Name = "__Host-bff";
        options.Cookie.SameSite = SameSiteMode.Strict;
    })
    .AddOpenIdConnect("oidc", options =>
    {
        options.Authority = "https://your-idp.example.com";
        options.ClientId = "my-bff-client";
        options.ClientSecret = "secret";
        options.ResponseType = "code";
        options.SaveTokens = true;
        options.Scope.Add("offline_access");
    });

// ...

app.MapBffManagementEndpoints(); // ✅ Required in v3
```

### Key Differences: V4 vs V3

| Feature               | V4                                                | V3                                          |
| --------------------- | ------------------------------------------------- | ------------------------------------------- |
| Auth handler setup    | `ConfigureOpenIdConnect()` / `ConfigureCookies()` | Manual `AddCookie()` / `AddOpenIdConnect()` |
| Management endpoints  | Auto-registered                                   | `MapBffManagementEndpoints()` required      |
| Remote API token type | `.WithAccessToken(RequiredTokenType.User)`        | `.RequireAccessToken(TokenType.User)`       |
| Session cleanup       | `.AddSessionCleanupBackgroundProcess()`           | `EnableSessionCleanup` option               |
| Token retriever       | `IAccessTokenRetriever` (implement directly)      | `DefaultAccessTokenRetriever` (inheritable) |
| Multi-frontend        | Built-in `AddFrontend()` API                      | Not supported                               |
| Middleware control     | `AutomaticallyRegisterBffMiddleware` option       | Always automatic                            |

---

## Pattern 2: Login and Logout Endpoints

In BFF v4, management endpoints (`/bff/login`, `/bff/logout`, `/bff/user`, `/bff/backchannel-logout`) are registered automatically by `AddBff()` with the implicit default frontend. In v3, they require an explicit call to `MapBffManagementEndpoints()`.

**Login** — A browser navigation to `/bff/login` initiates an OIDC Authorization Code flow. After the IDP redirects back, the BFF sets an HTTP-only authentication cookie.

```csharp
// ✅ Trigger login from the SPA (browser navigation, not fetch)
// React example:
// window.location.href = '/bff/login?returnUrl=/dashboard';

// ✅ Optional: supply a returnUrl to redirect after login
// GET /bff/login?returnUrl=/dashboard
// The returnUrl must be a local path; absolute URLs are rejected.
```

**Logout** — A browser navigation to `/bff/logout` signs the user out locally and initiates an OIDC end_session flow. It also revokes the refresh token automatically.

```csharp
// ✅ The sid claim from /bff/user must be passed as a query parameter
// GET /bff/logout?sid=<session-id>
// This is required to prevent CSRF attacks on the logout endpoint.
```

```csharp
// ❌ Do NOT call /bff/logout via fetch() without the sid parameter.
// The logout endpoint validates the sid to prevent cross-site logout attacks.
```

---

## Pattern 3: CSRF / Anti-Forgery Protection

The BFF enforces a custom `X-CSRF` header on every protected endpoint. This triggers a CORS preflight for cross-origin requests, effectively preventing CSRF attacks. The header value is irrelevant — its presence is sufficient.

### Local (Embedded) API Endpoints

```csharp
// ✅ Minimal API: decorate with AsBffApiEndpoint()
app.MapGet("/api/data", (HttpContext ctx) => Results.Ok("data"))
    .RequireAuthorization()
    .AsBffApiEndpoint();

// ✅ MVC Controllers: apply to the entire controller via attribute
[Route("api/data")]
[BffApi]
public class DataController : ControllerBase
{
    [HttpGet]
    public IActionResult Get() => Ok("data");
}

// ✅ MVC Controllers: apply at mapping time
app.MapControllers()
    .RequireAuthorization()
    .AsBffApiEndpoint();
```

```csharp
// ❌ Do NOT expose BFF API endpoints without AsBffApiEndpoint() or BffApi attribute.
// Without it, the x-csrf header is not enforced and the endpoint is CSRF-vulnerable.
app.MapGet("/api/data", () => Results.Ok("data"))
    .RequireAuthorization(); // Missing .AsBffApiEndpoint()
```

### Middleware Order

`UseBff()` must appear **after** `UseRouting()` but **before** `UseAuthorization()`. Incorrect order silently disables anti-forgery enforcement.

```csharp
// ✅ Correct middleware order
app.UseRouting();
app.UseAuthentication();
app.UseBff();           // Must be here
app.UseAuthorization();
app.MapControllers().AsBffApiEndpoint();

// ❌ Wrong: UseBff() after UseAuthorization() — anti-forgery is not applied
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseBff();           // Too late
```

### Skipping Anti-Forgery

For specific endpoints that cannot send the anti-forgery header (e.g., webhook receivers), use `.SkipAntiforgery()`:

```csharp
// ✅ Webhook receiver: skip anti-forgery for endpoints that cannot send the header
app.MapPost("/api/webhook", (WebhookPayload payload) => Results.Ok())
    .AsBffApiEndpoint()
    .SkipAntiforgery();
```

### Skipping Response Handling (V4)

By default, BFF converts 401/403 responses from local API endpoints into JSON-friendly responses (no redirect). Use `.SkipResponseHandling()` to bypass this and trigger normal ASP.NET Core authentication redirects:

```csharp
// ✅ Skip BFF's automatic 401/403 conversion — triggers actual OIDC redirect on challenge
app.MapGet("/api/interactive", () => Results.Ok("data"))
    .RequireAuthorization()
    .AsBffApiEndpoint()
    .SkipResponseHandling();
```

### Conditional Anti-Forgery (V4)

In v4, `DisableAntiForgeryCheck` is a delegate that allows conditionally skipping anti-forgery per-request:

```csharp
builder.Services.AddBff(options =>
{
    options.DisableAntiForgeryCheck = context =>
        context.Request.Path.StartsWithSegments("/api/webhook");
});
```

---

## Pattern 4: Remote API Proxying

The BFF can act as a reverse proxy to APIs deployed on separate hosts. Requests carry only the session cookie; the BFF exchanges it for an access token before forwarding.

Install the YARP integration package:

```
dotnet add package Duende.BFF.Yarp
```

```csharp
// ✅ Direct forwarding via MapRemoteBffApiEndpoint
builder.Services.AddBff()
    .AddRemoteApis();

// Maps /api/orders and all sub-paths to https://orders-service/orders
app.MapRemoteBffApiEndpoint("/api/orders", new Uri("https://orders-service/orders"))
    .WithAccessToken(RequiredTokenType.User);    // Attach the user's access token

app.MapRemoteBffApiEndpoint("/api/public", new Uri("https://content-service/public"))
    .WithAccessToken(RequiredTokenType.None);    // Anonymous remote API

app.MapRemoteBffApiEndpoint("/api/internal", new Uri("https://internal-service/api"))
    .WithAccessToken(RequiredTokenType.Client);  // Client credentials token (machine-to-machine)
```

### Token Type Options

| `RequiredTokenType` | Behavior |
|---|---|
| `None` | No token attached; anonymous passthrough |
| `User` | Forwards the current user's access token; challenges if unauthenticated |
| `Client` | Forwards a client credentials token; works even without a logged-in user |
| `UserOrClient` | Forwards user token if available, falls back to client token |
| `UserOrNone` | Forwards user token if logged in, no token if anonymous (no challenge). Replaces v3's `OptionalUserToken` |

### Custom Access Token Retriever (V4)

Implement `IAccessTokenRetriever` to customize per-route token retrieval. In v4, `DefaultAccessTokenRetriever` is internal — implement the interface directly:

```csharp
// ✅ Custom token retriever: select token based on route or request context
public class MyTokenRetriever : IAccessTokenRetriever
{
    public Task<AccessTokenResult> GetAccessToken(GetAccessTokenContext context)
    {
        // Custom logic — e.g., choose token based on route or header
        return Task.FromResult<AccessTokenResult>(
            new BearerTokenResult(context.UserToken, "Bearer"));
    }
}

// Register per-endpoint
app.MapRemoteBffApiEndpoint("/api/custom", new Uri("https://api.example.com"))
    .WithAccessToken(RequiredTokenType.User)
    .WithAccessTokenRetriever<MyTokenRetriever>();
```

### ForwarderRequestConfig (V4)

Configure per-endpoint activity timeout and response buffering for remote API proxying:

```csharp
app.MapRemoteBffApiEndpoint("/api/long-running", new Uri("https://api.example.com"))
    .WithAccessToken(RequiredTokenType.User)
    .WithForwarderRequestConfig(new ForwarderRequestConfig
    {
        ActivityTimeout = TimeSpan.FromMinutes(5),
        AllowResponseBuffering = true
    });
```

```csharp
// ✅ Restrict access in addition to token requirements
app.MapRemoteBffApiEndpoint("/api/admin", new Uri("https://admin-service/api"))
    .WithAccessToken(RequiredTokenType.User)
    .RequireAuthorization("AdminPolicy");
```

```csharp
// ❌ MapRemoteBffApiEndpoint opens the entire sub-path namespace.
// Do NOT use broad paths like "/" or "/api" unless all sub-routes should be exposed.
app.MapRemoteBffApiEndpoint("/", new Uri("https://backend-service")); // Exposes everything
```

---

## Pattern 5: Session Management

### Server-Side Sessions

Default cookie-based sessions embed claims and tokens in the cookie. For production, move session data server-side: the cookie only carries a session ID, keeping cookie size small and enabling server-initiated revocation.

> **Tokens never touch the cookie with server-side sessions.** All tokens — including **refresh tokens** — live in the server-side session store; the cookie holds only the session id. This is why the store choice is a security/availability decision, not just a size optimization.
>
> **The in-memory store is not durable and not shared:** sessions are lost on process restart, and in a load-balanced deployment a request routed to a *different* instance won't find the session (the user appears logged out). For any multi-node BFF, use a **persistent, shared** store — the EF store from `Duende.BFF.EntityFramework` (`AddEntityFrameworkServerSideSessions`).

```csharp
// ✅ In-memory server-side sessions (development/testing only)
builder.Services.AddBff()
    .AddServerSideSessions();

// ✅ Production: persist with Entity Framework
// dotnet add package Duende.BFF.EntityFramework
builder.Services.AddBff()
    .AddEntityFrameworkServerSideSessions(options =>
    {
        options.UseSqlServer(builder.Configuration.GetConnectionString("BffSessions"));
    });
```

```csharp
// ✅ Session cleanup (v4): manual registration required
builder.Services.AddBff(options =>
{
    options.SessionCleanupInterval = TimeSpan.FromMinutes(5);
})
.AddEntityFrameworkServerSideSessions(options =>
{
    options.UseSqlServer(connectionString);
})
.AddSessionCleanupBackgroundProcess();
```

```csharp
// ❌ In-memory sessions are NOT suitable for production.
// Sessions are lost on restart; BFF horizontal scaling requires a shared store.
builder.Services.AddBff()
    .AddServerSideSessions(); // No EF store — data lives only in process memory
```

### EF Migrations for Session Store

```bash
dotnet ef migrations add UserSessions -o Migrations -c SessionDbContext
dotnet ef database update
```

---

## Pattern 6: Token Management Integration

BFF integrates with `Duende.AccessTokenManagement` (ATM) automatically when `SaveTokens = true` is set on the OIDC handler. Tokens are stored in the server-side session and refreshed transparently.

```csharp
// ✅ Retrieve the current user access token in a local API endpoint
app.MapGet("/api/data", async (HttpContext ctx, IHttpClientFactory factory) =>
{
    // ATM handles refresh automatically if the token is expired
    var token = await ctx.GetUserAccessTokenAsync();

    var client = factory.CreateClient();
    client.SetBearerToken(token);

    var response = await client.GetAsync("https://remote-service/data");
    return Results.Text(await response.Content.ReadAsStringAsync());
})
.AsBffApiEndpoint();
```

```csharp
// ✅ Named HttpClient with automatic token management (preferred pattern)
builder.Services.AddUserAccessTokenHttpClient("apiClient", configureClient: client =>
{
    client.BaseAddress = new Uri("https://remote-service/");
});

app.MapGet("/api/proxy", async (IHttpClientFactory factory) =>
{
    var client = factory.CreateClient("apiClient"); // Token attached automatically
    return Results.Text(await (await client.GetAsync("data")).Content.ReadAsStringAsync());
})
.AsBffApiEndpoint();
```

```csharp
// ✅ Typed HttpClient with token handler
builder.Services.AddHttpClient<RemoteApiClient>(client =>
{
    client.BaseAddress = new Uri("https://remote-service/");
})
.AddUserAccessTokenHandler();
```

```csharp
// ❌ Do NOT manually read tokens from the session and store them in JavaScript.
// This defeats the entire purpose of BFF. Tokens must stay server-side.
var token = await ctx.GetUserAccessTokenAsync();
return Results.Json(new { accessToken = token }); // ❌ Exposes token to browser
```

### Refresh Token Revocation

BFF revokes refresh tokens automatically at logout. Configure rotation behavior on IdentityServer — BFF clients are confidential clients and do **not** need rotating (one-time-use) refresh tokens.

```csharp
// ✅ Manually revoke if needed (e.g., on account compromise)
await HttpContext.RevokeUserRefreshTokenAsync();
```

---

## Pattern 7: SPA Integration

### Session Check Endpoint (`/bff/user`)

The `/bff/user` endpoint returns the current user's claims or `401`. Use it on SPA startup to determine authentication state.

```javascript
// ✅ React: check session on app load
async function getUser() {
    const response = await fetch('/bff/user', {
        headers: { 'X-CSRF': '1' }  // Required anti-forgery header
    });
    if (response.ok) {
        return await response.json();
    }
    return null; // 401 = not authenticated
}
```

### Fetch Wrapper for CSRF Header

Every `fetch()` call to a BFF API endpoint must include `X-CSRF: 1`. Wrap `fetch` globally rather than adding it to every call site.

```javascript
// ✅ Fetch wrapper that automatically appends the required CSRF header
function bffFetch(url, options = {}) {
    return fetch(url, {
        ...options,
        headers: {
            'X-CSRF': '1',
            ...options.headers,
        },
    });
}

// Usage
const data = await bffFetch('/api/orders').then(r => r.json());
```

```javascript
// ❌ Missing X-CSRF header — BFF will return 401
const data = await fetch('/api/orders').then(r => r.json());
```

### Handling 401 and Session Expiry

BFF API endpoints return `401` (not a redirect) when the session has expired. The SPA must detect this and redirect to `/bff/login`.

```javascript
// ✅ Centralized 401 handling in fetch wrapper
async function bffFetch(url, options = {}) {
    const response = await fetch(url, {
        ...options,
        headers: { 'X-CSRF': '1', ...options.headers },
    });

    if (response.status === 401) {
        // Session expired — redirect to BFF login endpoint
        window.location.href = `/bff/login?returnUrl=${encodeURIComponent(window.location.pathname)}`;
        return;
    }

    return response;
}
```

### Login and Logout Links

Login and logout are browser navigations, not `fetch` calls. Do not use `fetch` or `XMLHttpRequest` for these flows.

```javascript
// ✅ Navigate to login (triggers OIDC redirect)
window.location.href = '/bff/login';

// ✅ Navigate to logout — must include sid from /bff/user response
const user = await bffFetch('/bff/user').then(r => r.json());
const sid = user.find(c => c.type === 'sid')?.value;
window.location.href = `/bff/logout?sid=${sid}`;
```

---

## Pattern 8: Deployment Considerations

### SameSite Cookie Configuration

| Scenario | Recommended `SameSite` |
|---|---|
| IDP on same site as BFF (e.g., `auth.example.com` and `app.example.com`) | `Strict` |
| IDP on a different domain (e.g., Duende demo, Auth0, Azure AD) | `Lax` |
| Embedded in iframe or third-party context | Not supported — BFF requires first-party cookie |

```csharp
// ✅ Strict (preferred when IDP is same-site)
options.Cookie.SameSite = SameSiteMode.Strict;

// ✅ Lax (required when IDP is on a different domain)
options.Cookie.SameSite = SameSiteMode.Lax;

// ❌ None requires Secure=true and is only appropriate for third-party contexts
// which are fundamentally incompatible with the BFF pattern
options.Cookie.SameSite = SameSiteMode.None;
```

### Reverse Proxy / Path Base

When the BFF is hosted behind a reverse proxy (e.g., nginx, Azure Application Gateway), configure forwarded headers and path base so authentication callbacks resolve correctly.

```csharp
// ✅ Trust forwarded headers from proxy (add before UseAuthentication)
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
    ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});

// ✅ If the BFF is mounted at a sub-path (e.g., /app)
app.UsePathBase("/app");
```

### CORS Policy

The BFF serves the SPA from the same origin, so CORS is typically not needed between the SPA and BFF. CORS should be configured only for cross-origin scenarios.

```csharp
// ✅ Restrict CORS to known origins if the BFF and SPA are on different origins
builder.Services.AddCors(options =>
{
    options.AddPolicy("SpaPolicy", policy =>
    {
        policy.WithOrigins("https://app.example.com")
              .AllowAnyHeader()
              .AllowAnyMethod()
              .AllowCredentials(); // Required for cookie-based auth across origins
    });
});

app.UseCors("SpaPolicy");
```

### Data Protection in Clustered Deployments

When running multiple BFF instances, cookies and anti-forgery tokens must be decryptable by all nodes. Configure a shared Data Protection key store. See [ASP.NET Core Data Protection](https://docs.duendesoftware.com/general/data-protection/) for comprehensive configuration guidance — BFF depends on Data Protection equally to IdentityServer.

```csharp
// ✅ Shared key ring (e.g., Azure Blob Storage + Key Vault)
builder.Services.AddDataProtection()
    .PersistKeysToAzureBlobStorage(/* ... */)
    .ProtectKeysWithAzureKeyVault(/* ... */);

// ✅ Shared key ring via database (e.g., Entity Framework)
builder.Services.AddDataProtection()
    .PersistKeysToDbContext<ApplicationDbContext>();
```

```csharp
// ❌ Default in-memory key ring in multi-instance deployments
// Each instance generates its own keys; cookies from one instance
// cannot be decrypted by another.
builder.Services.AddDataProtection(); // No persistence — broken in clusters
```

---

## Pattern 9: YARP Reverse Proxy Integration

For complex proxying scenarios, BFF integrates with YARP (Yet Another Reverse Proxy) via the `Duende.BFF.Yarp` package, which provides full BFF token management and anti-forgery enforcement inside the YARP pipeline.

```bash
dotnet add package Duende.BFF.Yarp
```

### Setup with In-Code Configuration

```csharp
// ✅ YARP with BFF extensions — in-code route/cluster configuration
builder.Services.AddBff();

var proxyBuilder = builder.Services.AddReverseProxy()
    .AddBffExtensions(); // Register BFF token management for YARP

// Configure routes in code using LoadFromMemory
proxyBuilder.LoadFromMemory(
    routes:
    [
        new RouteConfig
        {
            RouteId = "api",
            ClusterId = "api-cluster",
            Match = new RouteMatch { Path = "/api/{**catch-all}" }
        }
        .WithAccessToken(TokenType.User)      // Note: YARP uses TokenType, not RequiredTokenType
        .WithAntiforgeryCheck()
    ],
    clusters:
    [
        new ClusterConfig
        {
            ClusterId = "api-cluster",
            Destinations = new Dictionary<string, DestinationConfig>
            {
                ["default"] = new DestinationConfig
                {
                    Address = "https://upstream-api.example.com"
                }
            }
        }
    ]
);

var app = builder.Build();

app.UseRouting();
app.UseAuthentication();
app.UseBff();
app.UseAuthorization();

// ✅ UseAntiforgeryCheck() must be explicitly added inside MapReverseProxy
app.MapReverseProxy(proxyApp =>
{
    proxyApp.UseAntiforgeryCheck();
});

app.Run();
```

```csharp
// ❌ Do NOT omit UseAntiforgeryCheck() in the YARP pipeline —
// anti-forgery is not automatically applied to YARP routes
app.MapReverseProxy(); // Missing UseAntiforgeryCheck()
```

### YARP Configuration via appsettings.json

When using JSON configuration instead of `LoadFromMemory`, set BFF behavior via route metadata:

```json
{
  "ReverseProxy": {
    "Routes": {
      "api-route": {
        "ClusterId": "api-cluster",
        "Match": { "Path": "/api/{**catch-all}" },
        "Metadata": {
          "Duende.Bff.Yarp.TokenType": "User",
          "Duende.Bff.Yarp.AntiforgeryCheck": "true"
        }
      }
    },
    "Clusters": {
      "api-cluster": {
        "Destinations": {
          "default": { "Address": "https://upstream-api.example.com" }
        }
      }
    }
  }
}
```

> **Warning:** Metadata keys (`Duende.Bff.Yarp.TokenType`, `Duende.Bff.Yarp.AntiforgeryCheck`) are case-sensitive strings. Typos fail silently — no token is attached and no anti-forgery check is performed.

### YARP Code Configuration Extensions

Note: YARP routes use `TokenType` (not `RequiredTokenType` which is used by `MapRemoteBffApiEndpoint`).

| Extension                         | Purpose                        |
| --------------------------------- | ------------------------------ |
| `WithAccessToken(TokenType.User)` | Attach user access token       |
| `WithAntiforgeryCheck()`          | Enable anti-forgery validation |
| `WithOptionalUserAccessToken()`   | Attach user token if available |

---

## Pattern 10: Multi-Frontend (V4)

BFF v4 supports serving multiple frontends from a single BFF host. Each frontend gets its own OIDC, cookie, and API configuration. The default single-frontend behavior is an implicit multi-frontend setup with one frontend.

### AutomaticallyRegisterBffMiddleware

By default, BFF middleware is auto-registered. In multi-frontend scenarios, disable this for manual control:

```csharp
builder.Services.AddBff(options =>
{
    options.AutomaticallyRegisterBffMiddleware = false;
});

var app = builder.Build();

app.UseRouting();
app.UseAuthentication();

// ✅ Register BFF middleware components individually for multi-frontend control
app.UseBffPreProcessing();
app.UseBffFrontendSelection();
app.UseBffPathMapping();
app.UseBffOpenIdCallbacks();
app.UseBffStaticFileProxying();

app.UseAuthorization();
```

### Frontend Configuration (Code)

```csharp
builder.Services.AddBff()
    .AddFrontend("admin", frontend =>
    {
        frontend.MatchingPath = "/admin";
        frontend.CdnIndexHtmlUrl = new Uri("https://cdn.example.com/admin/index.html");

        frontend.ConfigureOpenIdConnect(options =>
        {
            options.Authority = "https://idp.example.com";
            options.ClientId = "admin-client";
            options.ClientSecret = "secret";
        });

        frontend.AddRemoteApi("api", remote =>
        {
            remote.PathMatch = "/api/admin";
            remote.TargetUri = new Uri("https://admin-api.example.com");
            remote.RequiredTokenType = RequiredTokenType.User;
        });
    });
```

### IIndexHtmlTransformer

Implement `IIndexHtmlTransformer` to inject frontend-specific configuration into the `index.html` before serving:

```csharp
public class FrontendConfigTransformer : IIndexHtmlTransformer
{
    public Task<string> TransformAsync(string indexHtml, HttpContext context)
    {
        // Inject runtime configuration into the SPA's index.html
        var config = $"<script>window.__CONFIG__ = {{ api: '/api' }};</script>";
        return Task.FromResult(indexHtml.Replace("</head>", $"{config}</head>"));
    }
}
```

### IndexHtmlDefaultCacheDuration

Control CDN index.html cache duration (default 5 minutes):

```csharp
builder.Services.AddBff(options =>
{
    options.IndexHtmlDefaultCacheDuration = TimeSpan.FromMinutes(10);
});
```

---

## Pattern 11: Blazor Integration

### Blazor Server

```csharp
// ✅ Blazor Server: AddBlazorServer() integrates BFF session management with the circuit model
builder.Services.AddBff()
    .ConfigureOpenIdConnect(options =>
    {
        options.Authority = "https://identity.example.com";
        options.ClientId = "blazor-server";
        options.ClientSecret = "secret";
        options.ResponseType = "code";
        options.Scope.Add("api1");
        options.Scope.Add("offline_access");
        options.SaveTokens = true;
    })
    .AddBlazorServer();
```

`AddBlazorServer()` integrates BFF session management with Blazor Server's circuit model. Long-lived circuits may encounter expired sessions — configure appropriate polling intervals via `BffBlazorServerOptions`.

### Blazor WASM (Client)

```csharp
// ✅ Server-side Program.cs
builder.Services.AddBff()
    .ConfigureOpenIdConnect(options =>
    {
        options.Authority = "https://identity.example.com";
        options.ClientId = "blazor-wasm";
        options.ClientSecret = "secret";
        options.ResponseType = "code";
        options.Scope.Add("api1");
        options.Scope.Add("offline_access");
        options.SaveTokens = true;
    })
    .AddBffBlazorClient();
```

```csharp
// ✅ Client-side Program.cs (WASM project)
builder.Services.AddBffBlazorClient(options =>
{
    options.RemoteApiPath = "/api/remote";
    options.Polling = new BffBlazorClientPollingOptions
    {
        Interval = TimeSpan.FromSeconds(30) // Default is 5 seconds
    };
});

// AddLocalApiHttpClient<T>() creates a typed HTTP client that routes through the BFF host
builder.Services.AddLocalApiHttpClient<WeatherClient>();
```

### BffBlazorServerOptions

| Option            | Default   | Purpose                           |
| ----------------- | --------- | --------------------------------- |
| `PollingInterval` | 5 seconds | How often to check session status |

### BffBlazorClientOptions

| Option             | Default       | Purpose                         |
| ------------------ | ------------- | ------------------------------- |
| `RemoteApiPath`    | `/api/remote` | Base path for remote API calls  |
| `BaseAddress`      | (from host)   | Base address for API calls      |
| `Polling.Interval` | 5 seconds     | Session status polling interval |

---

## BffOptions Reference

| Option                              | Default     | Purpose                                              |
| ----------------------------------- | ----------- | ---------------------------------------------------- |
| `AntiForgeryHeaderName`             | `"X-CSRF"`  | Name of the anti-forgery header                      |
| `AntiForgeryHeaderValue`            | `"1"`       | Expected value of the anti-forgery header            |
| `ManagementBasePath`                | `"/bff"`    | Base path for management endpoints                   |
| `RevokeRefreshTokenOnLogout`        | `true`      | Revoke refresh tokens on logout                      |
| `AnonymousSessionResponse`          | (null)      | Response for `/bff/user` when anonymous              |
| `BackchannelLogoutAllUserSessions`  | `false`     | Logout all sessions on backchannel notification      |
| `SessionCleanupInterval`            | 10 minutes  | Interval for expired session cleanup                 |
| `AutomaticallyRegisterBffMiddleware`| `true`      | V4: Auto-register BFF middleware; set `false` for multi-frontend manual control |
| `DisableAntiForgeryCheck`           | (null)      | V4: Delegate to conditionally skip anti-forgery per-request |
| `IndexHtmlDefaultCacheDuration`     | 5 minutes   | V4: CDN index.html cache duration                    |
| `Diagnostics.LogFrequency`          | (default)   | V4: How often BFF logs diagnostic information        |
| `Diagnostics.ChunkSize`             | (default)   | V4: Size of diagnostic log chunks                    |

> **V4 Breaking Change:** `EnableSessionCleanup` has been removed. Use `.AddSessionCleanupBackgroundProcess()` on the BFF builder instead.

---

## Extensibility: Logout Endpoint (V4)

Customize the logout endpoint by implementing `ILogoutEndpoint`:

```csharp
public class CustomLogoutEndpoint : ILogoutEndpoint
{
    private readonly ILogoutEndpoint _inner;

    public CustomLogoutEndpoint(ILogoutEndpoint inner) => _inner = inner;

    public async Task<IResult> ProcessRequestAsync(HttpContext context)
    {
        // Pre-processing: audit log, cleanup, etc.
        var result = await _inner.ProcessRequestAsync(context);
        // Post-processing
        return result;
    }
}
```

Validate return URLs with `IReturnUrlValidator` to prevent open redirector attacks.

---

## Extensibility: Session Store (V4)

V4 uses `UserSessionKey` and `PartitionKey` types instead of raw strings. The `IUserSessionStore` interface:

```csharp
public interface IUserSessionStore
{
    Task<UserSession?> GetUserSessionAsync(UserSessionKey key, CancellationToken ct);
    Task CreateUserSessionAsync(UserSession session, CancellationToken ct);
    Task UpdateUserSessionAsync(UserSessionKey key, UserSessionUpdate session, CancellationToken ct);
    Task DeleteUserSessionAsync(UserSessionKey key, CancellationToken ct);
    Task<IReadOnlyCollection<UserSession>> GetUserSessionsAsync(
        PartitionKey partitionKey, UserSessionsFilter filter, CancellationToken ct);
    Task DeleteUserSessionsAsync(
        PartitionKey partitionKey, UserSessionsFilter filter, CancellationToken ct);
}
```

Register a custom store: `.AddServerSideSessions<YourCustomStore>()`

Session cleanup is a separate concern — implement `IUserSessionStoreCleanup` and register with `.AddSessionCleanupBackgroundProcess()`.

---

## Common Pitfalls

- **Calling `/bff/login` or `/bff/logout` via `fetch()`** — These endpoints trigger OIDC redirects and must be browser navigations (`window.location.href`), not AJAX calls.

- **Omitting `offline_access` scope** — Without a refresh token, BFF cannot automatically renew expired access tokens. The user will receive 401 errors from remote APIs when their access token expires.

- **Using in-memory sessions in production** — `AddServerSideSessions()` without EF means sessions vanish on restart and cannot be shared across instances. Always use `AddEntityFrameworkServerSideSessions()` in production.

- **Forgetting `SaveTokens = true`** — Without this, OIDC tokens are not stored in the session, and `GetUserAccessTokenAsync()` returns nothing. Token management silently fails.

- **Missing `X-CSRF: 1` header in SPA fetch calls** — BFF returns 401 for API requests without the header. Centralize header injection in a fetch wrapper rather than adding it to each call site.

- **Incorrect middleware order** — `UseBff()` must come after `UseRouting()` and before `UseAuthorization()`. Any deviation silently breaks anti-forgery enforcement without a clear error.

- **Exposing access tokens to the frontend** — Returning token values from a local API endpoint to JavaScript completely defeats the BFF pattern and its token-theft protections.

- **Using `SameSite=Strict` with a cross-site IDP** — After the OIDC redirect back from the IDP, the browser won't send the post-login session cookie on the first request because it was a cross-site navigation. Use `Lax` when the IDP is on a different site.

- **Forgetting to revoke the refresh token on logout** — BFF does this automatically, but if `RevokeRefreshTokenOnLogout = false` is set, abandoned sessions retain valid refresh tokens indefinitely.

- **Not configuring Data Protection in multi-instance deployments** — Cookie decryption failures manifest as users being perpetually logged out in load-balanced environments.

- **YARP metadata key typos** — When using appsettings.json configuration for YARP, the metadata keys (`Duende.Bff.Yarp.TokenType`, `Duende.Bff.Yarp.AntiforgeryCheck`) are case-sensitive strings. A typo causes silent failure: no token is attached and no anti-forgery check is performed.

- **Forgetting `UseAntiforgeryCheck()` in the YARP pipeline** — Unlike `MapRemoteBffApiEndpoint`, YARP's anti-forgery enforcement is not automatic. `proxyApp.UseAntiforgeryCheck()` must be explicitly added inside `MapReverseProxy`; omitting it leaves YARP routes unprotected.

---

## Resources

- [Duende BFF Overview](https://docs.duendesoftware.com/bff/)
- [Getting Started: Single Frontend](https://docs.duendesoftware.com/bff/getting-started/single-frontend/)
- [Embedded (Local) APIs](https://docs.duendesoftware.com/bff/fundamentals/apis/local/)
- [Proxying Remote APIs](https://docs.duendesoftware.com/bff/fundamentals/apis/remote/)
- [Multi-Frontend](https://docs.duendesoftware.com/bff/fundamentals/multi-frontend/)
- [Server-Side Sessions](https://docs.duendesoftware.com/bff/fundamentals/session/server-side-sessions/)
- [Token Management](https://docs.duendesoftware.com/bff/fundamentals/tokens/)
- [Extensibility: Tokens](https://docs.duendesoftware.com/bff/extensibility/tokens/)
- [Extensibility: HTTP Forwarder](https://docs.duendesoftware.com/bff/extensibility/http-forwarder/)
- [Session Management Endpoints](https://docs.duendesoftware.com/bff/fundamentals/session/management/)
- [BFF Options Reference](https://docs.duendesoftware.com/bff/fundamentals/options/)
- [ASP.NET Core Data Protection](https://docs.duendesoftware.com/general/data-protection/)
- [BFF v3 → v4 Upgrade Guide](https://docs.duendesoftware.com/bff/upgrading/bff-v3-to-v4/)
- [NuGet: Duende.BFF](https://www.nuget.org/packages/Duende.BFF)
- [NuGet: Duende.BFF.Yarp](https://www.nuget.org/packages/Duende.BFF.Yarp)
- [NuGet: Duende.BFF.EntityFramework](https://www.nuget.org/packages/Duende.BFF.EntityFramework)
- Related skills: `aspnetcore-authentication`, `token-management`, `identityserver-configuration`
