← Duende SkillsCONTENT HISTORY

Update to Duende Skills

Snapshot Sep 30, 2026 · 23:14 UTC · version 0.3.0

Collection source: not recorded for this historical snapshot.

WHAT CHANGED · RULE-BASED ANALYSIS

First saved snapshot

No earlier snapshot is available to establish a change.

Compare saved observations

Download comparison JSON
Full technical diff · 0 changed fields
Full snapshot data
{
  "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.",
  "included_files": [],
  "skill_md_contents": "---\nname: duende-bff\ndescription: 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.\ninvocable: false\n---\n\n# Duende BFF (Backend for Frontend)\n\n## When to Use This Skill\n\n- Building or securing a SPA (React, Angular, Vue, Blazor WASM) that calls APIs requiring authentication\n- Implementing the Backend-for-Frontend security pattern to keep access tokens out of the browser\n- Configuring BFF session management, login/logout endpoints, and server-side sessions\n- Proxying requests from a frontend to remote APIs while automatically attaching access tokens\n- Adding CSRF/anti-forgery protection to APIs consumed by browser-based applications\n- Integrating `Duende.BFF` with `Duende.AccessTokenManagement` for automatic token refresh\n- Deploying a BFF behind a reverse proxy or configuring same-site cookie behavior\n\n## Core Principles\n\n1. **Tokens Never Touch the Browser** — The BFF holds all OAuth tokens server-side; the browser only ever sees an HTTP-only, Secure, SameSite cookie\n2. **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\n3. **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\n4. **Server-Side Sessions for Production** — The default in-memory cookie session is unsuitable for production; persist sessions with `Duende.BFF.EntityFramework`\n5. **Token Management Is Automatic** — BFF integrates with `Duende.AccessTokenManagement`; never manually refresh tokens or pass raw access tokens to the frontend\n\nDocs: https://docs.duendesoftware.com/bff/\n\n---\n\n## Pattern 1: Setup and Registration (BFF v4)\n\nBFF v4 uses a streamlined registration API that auto-configures OpenID Connect and cookie authentication with recommended defaults.\n\n```csharp\n// ✅ v4: AddBff() with fluent OIDC and cookie configuration\nbuilder.Services.AddBff()\n    .ConfigureOpenIdConnect(options =>\n    {\n        options.Authority = \"https://your-idp.example.com\";\n        options.ClientId = \"my-bff-client\";\n        options.ClientSecret = \"secret\";\n        options.ResponseType = \"code\";\n        options.ResponseMode = \"query\";\n\n        options.GetClaimsFromUserInfoEndpoint = true;\n        options.SaveTokens = true;\n        options.MapInboundClaims = false;\n\n        options.Scope.Clear();\n        options.Scope.Add(\"openid\");\n        options.Scope.Add(\"profile\");\n        options.Scope.Add(\"offline_access\"); // Required for refresh tokens\n    })\n    .ConfigureCookies(options =>\n    {\n        // Use Strict when your IDP is on the same site as the BFF.\n        // Use Lax when a cross-site redirect is required (e.g., IDP on a different domain).\n        options.Cookie.SameSite = SameSiteMode.Lax;\n    });\n\nbuilder.Services.AddAuthorization();\n\nvar app = builder.Build();\n\napp.UseRouting();\napp.UseAuthentication();\napp.UseBff();          // Adds CSRF anti-forgery enforcement middleware\napp.UseAuthorization();\n\napp.Run();\n```\n\n```csharp\n// ❌ v4: Do NOT manually wire AddCookie + AddOpenIdConnect when using AddBff()\n// ConfigureOpenIdConnect and ConfigureCookies handle this correctly\nbuilder.Services.AddAuthentication()\n    .AddCookie(\"cookie\")\n    .AddOpenIdConnect(\"oidc\", ...); // Bypasses BFF's recommended defaults\n```\n\n### BFF v3 Registration\n\nFor projects still on v3, explicit scheme setup is required and `MapBffManagementEndpoints()` must be called manually:\n\n```csharp\n// ✅ v3: explicit authentication scheme wiring\nbuilder.Services.AddBff();\n\nbuilder.Services\n    .AddAuthentication(options =>\n    {\n        options.DefaultScheme = \"cookie\";\n        options.DefaultChallengeScheme = \"oidc\";\n        options.DefaultSignOutScheme = \"oidc\";\n    })\n    .AddCookie(\"cookie\", options =>\n    {\n        options.Cookie.Name = \"__Host-bff\";\n        options.Cookie.SameSite = SameSiteMode.Strict;\n    })\n    .AddOpenIdConnect(\"oidc\", options =>\n    {\n        options.Authority = \"https://your-idp.example.com\";\n        options.ClientId = \"my-bff-client\";\n        options.ClientSecret = \"secret\";\n        options.ResponseType = \"code\";\n        options.SaveTokens = true;\n        options.Scope.Add(\"offline_access\");\n    });\n\n// ...\n\napp.MapBffManagementEndpoints(); // ✅ Required in v3\n```\n\n### Key Differences: V4 vs V3\n\n| Feature               | V4                                                | V3                                          |\n| --------------------- | ------------------------------------------------- | ------------------------------------------- |\n| Auth handler setup    | `ConfigureOpenIdConnect()` / `ConfigureCookies()` | Manual `AddCookie()` / `AddOpenIdConnect()` |\n| Management endpoints  | Auto-registered                                   | `MapBffManagementEndpoints()` required      |\n| Remote API token type | `.WithAccessToken(RequiredTokenType.User)`        | `.RequireAccessToken(TokenType.User)`       |\n| Session cleanup       | `.AddSessionCleanupBackgroundProcess()`           | `EnableSessionCleanup` option               |\n| Token retriever       | `IAccessTokenRetriever` (implement directly)      | `DefaultAccessTokenRetriever` (inheritable) |\n| Multi-frontend        | Built-in `AddFrontend()` API                      | Not supported                               |\n| Middleware control     | `AutomaticallyRegisterBffMiddleware` option       | Always automatic                            |\n\n---\n\n## Pattern 2: Login and Logout Endpoints\n\nIn 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()`.\n\n**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.\n\n```csharp\n// ✅ Trigger login from the SPA (browser navigation, not fetch)\n// React example:\n// window.location.href = '/bff/login?returnUrl=/dashboard';\n\n// ✅ Optional: supply a returnUrl to redirect after login\n// GET /bff/login?returnUrl=/dashboard\n// The returnUrl must be a local path; absolute URLs are rejected.\n```\n\n**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.\n\n```csharp\n// ✅ The sid claim from /bff/user must be passed as a query parameter\n// GET /bff/logout?sid=<session-id>\n// This is required to prevent CSRF attacks on the logout endpoint.\n```\n\n```csharp\n// ❌ Do NOT call /bff/logout via fetch() without the sid parameter.\n// The logout endpoint validates the sid to prevent cross-site logout attacks.\n```\n\n---\n\n## Pattern 3: CSRF / Anti-Forgery Protection\n\nThe 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.\n\n### Local (Embedded) API Endpoints\n\n```csharp\n// ✅ Minimal API: decorate with AsBffApiEndpoint()\napp.MapGet(\"/api/data\", (HttpContext ctx) => Results.Ok(\"data\"))\n    .RequireAuthorization()\n    .AsBffApiEndpoint();\n\n// ✅ MVC Controllers: apply to the entire controller via attribute\n[Route(\"api/data\")]\n[BffApi]\npublic class DataController : ControllerBase\n{\n    [HttpGet]\n    public IActionResult Get() => Ok(\"data\");\n}\n\n// ✅ MVC Controllers: apply at mapping time\napp.MapControllers()\n    .RequireAuthorization()\n    .AsBffApiEndpoint();\n```\n\n```csharp\n// ❌ Do NOT expose BFF API endpoints without AsBffApiEndpoint() or BffApi attribute.\n// Without it, the x-csrf header is not enforced and the endpoint is CSRF-vulnerable.\napp.MapGet(\"/api/data\", () => Results.Ok(\"data\"))\n    .RequireAuthorization(); // Missing .AsBffApiEndpoint()\n```\n\n### Middleware Order\n\n`UseBff()` must appear **after** `UseRouting()` but **before** `UseAuthorization()`. Incorrect order silently disables anti-forgery enforcement.\n\n```csharp\n// ✅ Correct middleware order\napp.UseRouting();\napp.UseAuthentication();\napp.UseBff();           // Must be here\napp.UseAuthorization();\napp.MapControllers().AsBffApiEndpoint();\n\n// ❌ Wrong: UseBff() after UseAuthorization() — anti-forgery is not applied\napp.UseRouting();\napp.UseAuthentication();\napp.UseAuthorization();\napp.UseBff();           // Too late\n```\n\n### Skipping Anti-Forgery\n\nFor specific endpoints that cannot send the anti-forgery header (e.g., webhook receivers), use `.SkipAntiforgery()`:\n\n```csharp\n// ✅ Webhook receiver: skip anti-forgery for endpoints that cannot send the header\napp.MapPost(\"/api/webhook\", (WebhookPayload payload) => Results.Ok())\n    .AsBffApiEndpoint()\n    .SkipAntiforgery();\n```\n\n### Skipping Response Handling (V4)\n\nBy 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:\n\n```csharp\n// ✅ Skip BFF's automatic 401/403 conversion — triggers actual OIDC redirect on challenge\napp.MapGet(\"/api/interactive\", () => Results.Ok(\"data\"))\n    .RequireAuthorization()\n    .AsBffApiEndpoint()\n    .SkipResponseHandling();\n```\n\n### Conditional Anti-Forgery (V4)\n\nIn v4, `DisableAntiForgeryCheck` is a delegate that allows conditionally skipping anti-forgery per-request:\n\n```csharp\nbuilder.Services.AddBff(options =>\n{\n    options.DisableAntiForgeryCheck = context =>\n        context.Request.Path.StartsWithSegments(\"/api/webhook\");\n});\n```\n\n---\n\n## Pattern 4: Remote API Proxying\n\nThe 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.\n\nInstall the YARP integration package:\n\n```\ndotnet add package Duende.BFF.Yarp\n```\n\n```csharp\n// ✅ Direct forwarding via MapRemoteBffApiEndpoint\nbuilder.Services.AddBff()\n    .AddRemoteApis();\n\n// Maps /api/orders and all sub-paths to https://orders-service/orders\napp.MapRemoteBffApiEndpoint(\"/api/orders\", new Uri(\"https://orders-service/orders\"))\n    .WithAccessToken(RequiredTokenType.User);    // Attach the user's access token\n\napp.MapRemoteBffApiEndpoint(\"/api/public\", new Uri(\"https://content-service/public\"))\n    .WithAccessToken(RequiredTokenType.None);    // Anonymous remote API\n\napp.MapRemoteBffApiEndpoint(\"/api/internal\", new Uri(\"https://internal-service/api\"))\n    .WithAccessToken(RequiredTokenType.Client);  // Client credentials token (machine-to-machine)\n```\n\n### Token Type Options\n\n| `RequiredTokenType` | Behavior |\n|---|---|\n| `None` | No token attached; anonymous passthrough |\n| `User` | Forwards the current user's access token; challenges if unauthenticated |\n| `Client` | Forwards a client credentials token; works even without a logged-in user |\n| `UserOrClient` | Forwards user token if available, falls back to client token |\n| `UserOrNone` | Forwards user token if logged in, no token if anonymous (no challenge). Replaces v3's `OptionalUserToken` |\n\n### Custom Access Token Retriever (V4)\n\nImplement `IAccessTokenRetriever` to customize per-route token retrieval. In v4, `DefaultAccessTokenRetriever` is internal — implement the interface directly:\n\n```csharp\n// ✅ Custom token retriever: select token based on route or request context\npublic class MyTokenRetriever : IAccessTokenRetriever\n{\n    public Task<AccessTokenResult> GetAccessToken(GetAccessTokenContext context)\n    {\n        // Custom logic — e.g., choose token based on route or header\n        return Task.FromResult<AccessTokenResult>(\n            new BearerTokenResult(context.UserToken, \"Bearer\"));\n    }\n}\n\n// Register per-endpoint\napp.MapRemoteBffApiEndpoint(\"/api/custom\", new Uri(\"https://api.example.com\"))\n    .WithAccessToken(RequiredTokenType.User)\n    .WithAccessTokenRetriever<MyTokenRetriever>();\n```\n\n### ForwarderRequestConfig (V4)\n\nConfigure per-endpoint activity timeout and response buffering for remote API proxying:\n\n```csharp\napp.MapRemoteBffApiEndpoint(\"/api/long-running\", new Uri(\"https://api.example.com\"))\n    .WithAccessToken(RequiredTokenType.User)\n    .WithForwarderRequestConfig(new ForwarderRequestConfig\n    {\n        ActivityTimeout = TimeSpan.FromMinutes(5),\n        AllowResponseBuffering = true\n    });\n```\n\n```csharp\n// ✅ Restrict access in addition to token requirements\napp.MapRemoteBffApiEndpoint(\"/api/admin\", new Uri(\"https://admin-service/api\"))\n    .WithAccessToken(RequiredTokenType.User)\n    .RequireAuthorization(\"AdminPolicy\");\n```\n\n```csharp\n// ❌ MapRemoteBffApiEndpoint opens the entire sub-path namespace.\n// Do NOT use broad paths like \"/\" or \"/api\" unless all sub-routes should be exposed.\napp.MapRemoteBffApiEndpoint(\"/\", new Uri(\"https://backend-service\")); // Exposes everything\n```\n\n---\n\n## Pattern 5: Session Management\n\n### Server-Side Sessions\n\nDefault 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.\n\n> **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.\n>\n> **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`).\n\n```csharp\n// ✅ In-memory server-side sessions (development/testing only)\nbuilder.Services.AddBff()\n    .AddServerSideSessions();\n\n// ✅ Production: persist with Entity Framework\n// dotnet add package Duende.BFF.EntityFramework\nbuilder.Services.AddBff()\n    .AddEntityFrameworkServerSideSessions(options =>\n    {\n        options.UseSqlServer(builder.Configuration.GetConnectionString(\"BffSessions\"));\n    });\n```\n\n```csharp\n// ✅ Session cleanup (v4): manual registration required\nbuilder.Services.AddBff(options =>\n{\n    options.SessionCleanupInterval = TimeSpan.FromMinutes(5);\n})\n.AddEntityFrameworkServerSideSessions(options =>\n{\n    options.UseSqlServer(connectionString);\n})\n.AddSessionCleanupBackgroundProcess();\n```\n\n```csharp\n// ❌ In-memory sessions are NOT suitable for production.\n// Sessions are lost on restart; BFF horizontal scaling requires a shared store.\nbuilder.Services.AddBff()\n    .AddServerSideSessions(); // No EF store — data lives only in process memory\n```\n\n### EF Migrations for Session Store\n\n```bash\ndotnet ef migrations add UserSessions -o Migrations -c SessionDbContext\ndotnet ef database update\n```\n\n---\n\n## Pattern 6: Token Management Integration\n\nBFF 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.\n\n```csharp\n// ✅ Retrieve the current user access token in a local API endpoint\napp.MapGet(\"/api/data\", async (HttpContext ctx, IHttpClientFactory factory) =>\n{\n    // ATM handles refresh automatically if the token is expired\n    var token = await ctx.GetUserAccessTokenAsync();\n\n    var client = factory.CreateClient();\n    client.SetBearerToken(token);\n\n    var response = await client.GetAsync(\"https://remote-service/data\");\n    return Results.Text(await response.Content.ReadAsStringAsync());\n})\n.AsBffApiEndpoint();\n```\n\n```csharp\n// ✅ Named HttpClient with automatic token management (preferred pattern)\nbuilder.Services.AddUserAccessTokenHttpClient(\"apiClient\", configureClient: client =>\n{\n    client.BaseAddress = new Uri(\"https://remote-service/\");\n});\n\napp.MapGet(\"/api/proxy\", async (IHttpClientFactory factory) =>\n{\n    var client = factory.CreateClient(\"apiClient\"); // Token attached automatically\n    return Results.Text(await (await client.GetAsync(\"data\")).Content.ReadAsStringAsync());\n})\n.AsBffApiEndpoint();\n```\n\n```csharp\n// ✅ Typed HttpClient with token handler\nbuilder.Services.AddHttpClient<RemoteApiClient>(client =>\n{\n    client.BaseAddress = new Uri(\"https://remote-service/\");\n})\n.AddUserAccessTokenHandler();\n```\n\n```csharp\n// ❌ Do NOT manually read tokens from the session and store them in JavaScript.\n// This defeats the entire purpose of BFF. Tokens must stay server-side.\nvar token = await ctx.GetUserAccessTokenAsync();\nreturn Results.Json(new { accessToken = token }); // ❌ Exposes token to browser\n```\n\n### Refresh Token Revocation\n\nBFF 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.\n\n```csharp\n// ✅ Manually revoke if needed (e.g., on account compromise)\nawait HttpContext.RevokeUserRefreshTokenAsync();\n```\n\n---\n\n## Pattern 7: SPA Integration\n\n### Session Check Endpoint (`/bff/user`)\n\nThe `/bff/user` endpoint returns the current user's claims or `401`. Use it on SPA startup to determine authentication state.\n\n```javascript\n// ✅ React: check session on app load\nasync function getUser() {\n    const response = await fetch('/bff/user', {\n        headers: { 'X-CSRF': '1' }  // Required anti-forgery header\n    });\n    if (response.ok) {\n        return await response.json();\n    }\n    return null; // 401 = not authenticated\n}\n```\n\n### Fetch Wrapper for CSRF Header\n\nEvery `fetch()` call to a BFF API endpoint must include `X-CSRF: 1`. Wrap `fetch` globally rather than adding it to every call site.\n\n```javascript\n// ✅ Fetch wrapper that automatically appends the required CSRF header\nfunction bffFetch(url, options = {}) {\n    return fetch(url, {\n        ...options,\n        headers: {\n            'X-CSRF': '1',\n            ...options.headers,\n        },\n    });\n}\n\n// Usage\nconst data = await bffFetch('/api/orders').then(r => r.json());\n```\n\n```javascript\n// ❌ Missing X-CSRF header — BFF will return 401\nconst data = await fetch('/api/orders').then(r => r.json());\n```\n\n### Handling 401 and Session Expiry\n\nBFF API endpoints return `401` (not a redirect) when the session has expired. The SPA must detect this and redirect to `/bff/login`.\n\n```javascript\n// ✅ Centralized 401 handling in fetch wrapper\nasync function bffFetch(url, options = {}) {\n    const response = await fetch(url, {\n        ...options,\n        headers: { 'X-CSRF': '1', ...options.headers },\n    });\n\n    if (response.status === 401) {\n        // Session expired — redirect to BFF login endpoint\n        window.location.href = `/bff/login?returnUrl=${encodeURIComponent(window.location.pathname)}`;\n        return;\n    }\n\n    return response;\n}\n```\n\n### Login and Logout Links\n\nLogin and logout are browser navigations, not `fetch` calls. Do not use `fetch` or `XMLHttpRequest` for these flows.\n\n```javascript\n// ✅ Navigate to login (triggers OIDC redirect)\nwindow.location.href = '/bff/login';\n\n// ✅ Navigate to logout — must include sid from /bff/user response\nconst user = await bffFetch('/bff/user').then(r => r.json());\nconst sid = user.find(c => c.type === 'sid')?.value;\nwindow.location.href = `/bff/logout?sid=${sid}`;\n```\n\n---\n\n## Pattern 8: Deployment Considerations\n\n### SameSite Cookie Configuration\n\n| Scenario | Recommended `SameSite` |\n|---|---|\n| IDP on same site as BFF (e.g., `auth.example.com` and `app.example.com`) | `Strict` |\n| IDP on a different domain (e.g., Duende demo, Auth0, Azure AD) | `Lax` |\n| Embedded in iframe or third-party context | Not supported — BFF requires first-party cookie |\n\n```csharp\n// ✅ Strict (preferred when IDP is same-site)\noptions.Cookie.SameSite = SameSiteMode.Strict;\n\n// ✅ Lax (required when IDP is on a different domain)\noptions.Cookie.SameSite = SameSiteMode.Lax;\n\n// ❌ None requires Secure=true and is only appropriate for third-party contexts\n// which are fundamentally incompatible with the BFF pattern\noptions.Cookie.SameSite = SameSiteMode.None;\n```\n\n### Reverse Proxy / Path Base\n\nWhen 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.\n\n```csharp\n// ✅ Trust forwarded headers from proxy (add before UseAuthentication)\napp.UseForwardedHeaders(new ForwardedHeadersOptions\n{\n    ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto\n});\n\n// ✅ If the BFF is mounted at a sub-path (e.g., /app)\napp.UsePathBase(\"/app\");\n```\n\n### CORS Policy\n\nThe 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.\n\n```csharp\n// ✅ Restrict CORS to known origins if the BFF and SPA are on different origins\nbuilder.Services.AddCors(options =>\n{\n    options.AddPolicy(\"SpaPolicy\", policy =>\n    {\n        policy.WithOrigins(\"https://app.example.com\")\n              .AllowAnyHeader()\n              .AllowAnyMethod()\n              .AllowCredentials(); // Required for cookie-based auth across origins\n    });\n});\n\napp.UseCors(\"SpaPolicy\");\n```\n\n### Data Protection in Clustered Deployments\n\nWhen 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.\n\n```csharp\n// ✅ Shared key ring (e.g., Azure Blob Storage + Key Vault)\nbuilder.Services.AddDataProtection()\n    .PersistKeysToAzureBlobStorage(/* ... */)\n    .ProtectKeysWithAzureKeyVault(/* ... */);\n\n// ✅ Shared key ring via database (e.g., Entity Framework)\nbuilder.Services.AddDataProtection()\n    .PersistKeysToDbContext<ApplicationDbContext>();\n```\n\n```csharp\n// ❌ Default in-memory key ring in multi-instance deployments\n// Each instance generates its own keys; cookies from one instance\n// cannot be decrypted by another.\nbuilder.Services.AddDataProtection(); // No persistence — broken in clusters\n```\n\n---\n\n## Pattern 9: YARP Reverse Proxy Integration\n\nFor 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.\n\n```bash\ndotnet add package Duende.BFF.Yarp\n```\n\n### Setup with In-Code Configuration\n\n```csharp\n// ✅ YARP with BFF extensions — in-code route/cluster configuration\nbuilder.Services.AddBff();\n\nvar proxyBuilder = builder.Services.AddReverseProxy()\n    .AddBffExtensions(); // Register BFF token management for YARP\n\n// Configure routes in code using LoadFromMemory\nproxyBuilder.LoadFromMemory(\n    routes:\n    [\n        new RouteConfig\n        {\n            RouteId = \"api\",\n            ClusterId = \"api-cluster\",\n            Match = new RouteMatch { Path = \"/api/{**catch-all}\" }\n        }\n        .WithAccessToken(TokenType.User)      // Note: YARP uses TokenType, not RequiredTokenType\n        .WithAntiforgeryCheck()\n    ],\n    clusters:\n    [\n        new ClusterConfig\n        {\n            ClusterId = \"api-cluster\",\n            Destinations = new Dictionary<string, DestinationConfig>\n            {\n                [\"default\"] = new DestinationConfig\n                {\n                    Address = \"https://upstream-api.example.com\"\n                }\n            }\n        }\n    ]\n);\n\nvar app = builder.Build();\n\napp.UseRouting();\napp.UseAuthentication();\napp.UseBff();\napp.UseAuthorization();\n\n// ✅ UseAntiforgeryCheck() must be explicitly added inside MapReverseProxy\napp.MapReverseProxy(proxyApp =>\n{\n    proxyApp.UseAntiforgeryCheck();\n});\n\napp.Run();\n```\n\n```csharp\n// ❌ Do NOT omit UseAntiforgeryCheck() in the YARP pipeline —\n// anti-forgery is not automatically applied to YARP routes\napp.MapReverseProxy(); // Missing UseAntiforgeryCheck()\n```\n\n### YARP Configuration via appsettings.json\n\nWhen using JSON configuration instead of `LoadFromMemory`, set BFF behavior via route metadata:\n\n```json\n{\n  \"ReverseProxy\": {\n    \"Routes\": {\n      \"api-route\": {\n        \"ClusterId\": \"api-cluster\",\n        \"Match\": { \"Path\": \"/api/{**catch-all}\" },\n        \"Metadata\": {\n          \"Duende.Bff.Yarp.TokenType\": \"User\",\n          \"Duende.Bff.Yarp.AntiforgeryCheck\": \"true\"\n        }\n      }\n    },\n    \"Clusters\": {\n      \"api-cluster\": {\n        \"Destinations\": {\n          \"default\": { \"Address\": \"https://upstream-api.example.com\" }\n        }\n      }\n    }\n  }\n}\n```\n\n> **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.\n\n### YARP Code Configuration Extensions\n\nNote: YARP routes use `TokenType` (not `RequiredTokenType` which is used by `MapRemoteBffApiEndpoint`).\n\n| Extension                         | Purpose                        |\n| --------------------------------- | ------------------------------ |\n| `WithAccessToken(TokenType.User)` | Attach user access token       |\n| `WithAntiforgeryCheck()`          | Enable anti-forgery validation |\n| `WithOptionalUserAccessToken()`   | Attach user token if available |\n\n---\n\n## Pattern 10: Multi-Frontend (V4)\n\nBFF 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.\n\n### AutomaticallyRegisterBffMiddleware\n\nBy default, BFF middleware is auto-registered. In multi-frontend scenarios, disable this for manual control:\n\n```csharp\nbuilder.Services.AddBff(options =>\n{\n    options.AutomaticallyRegisterBffMiddleware = false;\n});\n\nvar app = builder.Build();\n\napp.UseRouting();\napp.UseAuthentication();\n\n// ✅ Register BFF middleware components individually for multi-frontend control\napp.UseBffPreProcessing();\napp.UseBffFrontendSelection();\napp.UseBffPathMapping();\napp.UseBffOpenIdCallbacks();\napp.UseBffStaticFileProxying();\n\napp.UseAuthorization();\n```\n\n### Frontend Configuration (Code)\n\n```csharp\nbuilder.Services.AddBff()\n    .AddFrontend(\"admin\", frontend =>\n    {\n        frontend.MatchingPath = \"/admin\";\n        frontend.CdnIndexHtmlUrl = new Uri(\"https://cdn.example.com/admin/index.html\");\n\n        frontend.ConfigureOpenIdConnect(options =>\n        {\n            options.Authority = \"https://idp.example.com\";\n            options.ClientId = \"admin-client\";\n            options.ClientSecret = \"secret\";\n        });\n\n        frontend.AddRemoteApi(\"api\", remote =>\n        {\n            remote.PathMatch = \"/api/admin\";\n            remote.TargetUri = new Uri(\"https://admin-api.example.com\");\n            remote.RequiredTokenType = RequiredTokenType.User;\n        });\n    });\n```\n\n### IIndexHtmlTransformer\n\nImplement `IIndexHtmlTransformer` to inject frontend-specific configuration into the `index.html` before serving:\n\n```csharp\npublic class FrontendConfigTransformer : IIndexHtmlTransformer\n{\n    public Task<string> TransformAsync(string indexHtml, HttpContext context)\n    {\n        // Inject runtime configuration into the SPA's index.html\n        var config = $\"<script>window.__CONFIG__ = {{ api: '/api' }};</script>\";\n        return Task.FromResult(indexHtml.Replace(\"</head>\", $\"{config}</head>\"));\n    }\n}\n```\n\n### IndexHtmlDefaultCacheDuration\n\nControl CDN index.html cache duration (default 5 minutes):\n\n```csharp\nbuilder.Services.AddBff(options =>\n{\n    options.IndexHtmlDefaultCacheDuration = TimeSpan.FromMinutes(10);\n});\n```\n\n---\n\n## Pattern 11: Blazor Integration\n\n### Blazor Server\n\n```csharp\n// ✅ Blazor Server: AddBlazorServer() integrates BFF session management with the circuit model\nbuilder.Services.AddBff()\n    .ConfigureOpenIdConnect(options =>\n    {\n        options.Authority = \"https://identity.example.com\";\n        options.ClientId = \"blazor-server\";\n        options.ClientSecret = \"secret\";\n        options.ResponseType = \"code\";\n        options.Scope.Add(\"api1\");\n        options.Scope.Add(\"offline_access\");\n        options.SaveTokens = true;\n    })\n    .AddBlazorServer();\n```\n\n`AddBlazorServer()` integrates BFF session management with Blazor Server's circuit model. Long-lived circuits may encounter expired sessions — configure appropriate polling intervals via `BffBlazorServerOptions`.\n\n### Blazor WASM (Client)\n\n```csharp\n// ✅ Server-side Program.cs\nbuilder.Services.AddBff()\n    .ConfigureOpenIdConnect(options =>\n    {\n        options.Authority = \"https://identity.example.com\";\n        options.ClientId = \"blazor-wasm\";\n        options.ClientSecret = \"secret\";\n        options.ResponseType = \"code\";\n        options.Scope.Add(\"api1\");\n        options.Scope.Add(\"offline_access\");\n        options.SaveTokens = true;\n    })\n    .AddBffBlazorClient();\n```\n\n```csharp\n// ✅ Client-side Program.cs (WASM project)\nbuilder.Services.AddBffBlazorClient(options =>\n{\n    options.RemoteApiPath = \"/api/remote\";\n    options.Polling = new BffBlazorClientPollingOptions\n    {\n        Interval = TimeSpan.FromSeconds(30) // Default is 5 seconds\n    };\n});\n\n// AddLocalApiHttpClient<T>() creates a typed HTTP client that routes through the BFF host\nbuilder.Services.AddLocalApiHttpClient<WeatherClient>();\n```\n\n### BffBlazorServerOptions\n\n| Option            | Default   | Purpose                           |\n| ----------------- | --------- | --------------------------------- |\n| `PollingInterval` | 5 seconds | How often to check session status |\n\n### BffBlazorClientOptions\n\n| Option             | Default       | Purpose                         |\n| ------------------ | ------------- | ------------------------------- |\n| `RemoteApiPath`    | `/api/remote` | Base path for remote API calls  |\n| `BaseAddress`      | (from host)   | Base address for API calls      |\n| `Polling.Interval` | 5 seconds     | Session status polling interval |\n\n---\n\n## BffOptions Reference\n\n| Option                              | Default     | Purpose                                              |\n| ----------------------------------- | ----------- | ---------------------------------------------------- |\n| `AntiForgeryHeaderName`             | `\"X-CSRF\"`  | Name of the anti-forgery header                      |\n| `AntiForgeryHeaderValue`            | `\"1\"`       | Expected value of the anti-forgery header            |\n| `ManagementBasePath`                | `\"/bff\"`    | Base path for management endpoints                   |\n| `RevokeRefreshTokenOnLogout`        | `true`      | Revoke refresh tokens on logout                      |\n| `AnonymousSessionResponse`          | (null)      | Response for `/bff/user` when anonymous              |\n| `BackchannelLogoutAllUserSessions`  | `false`     | Logout all sessions on backchannel notification      |\n| `SessionCleanupInterval`            | 10 minutes  | Interval for expired session cleanup                 |\n| `AutomaticallyRegisterBffMiddleware`| `true`      | V4: Auto-register BFF middleware; set `false` for multi-frontend manual control |\n| `DisableAntiForgeryCheck`           | (null)      | V4: Delegate to conditionally skip anti-forgery per-request |\n| `IndexHtmlDefaultCacheDuration`     | 5 minutes   | V4: CDN index.html cache duration                    |\n| `Diagnostics.LogFrequency`          | (default)   | V4: How often BFF logs diagnostic information        |\n| `Diagnostics.ChunkSize`             | (default)   | V4: Size of diagnostic log chunks                    |\n\n> **V4 Breaking Change:** `EnableSessionCleanup` has been removed. Use `.AddSessionCleanupBackgroundProcess()` on the BFF builder instead.\n\n---\n\n## Extensibility: Logout Endpoint (V4)\n\nCustomize the logout endpoint by implementing `ILogoutEndpoint`:\n\n```csharp\npublic class CustomLogoutEndpoint : ILogoutEndpoint\n{\n    private readonly ILogoutEndpoint _inner;\n\n    public CustomLogoutEndpoint(ILogoutEndpoint inner) => _inner = inner;\n\n    public async Task<IResult> ProcessRequestAsync(HttpContext context)\n    {\n        // Pre-processing: audit log, cleanup, etc.\n        var result = await _inner.ProcessRequestAsync(context);\n        // Post-processing\n        return result;\n    }\n}\n```\n\nValidate return URLs with `IReturnUrlValidator` to prevent open redirector attacks.\n\n---\n\n## Extensibility: Session Store (V4)\n\nV4 uses `UserSessionKey` and `PartitionKey` types instead of raw strings. The `IUserSessionStore` interface:\n\n```csharp\npublic interface IUserSessionStore\n{\n    Task<UserSession?> GetUserSessionAsync(UserSessionKey key, CancellationToken ct);\n    Task CreateUserSessionAsync(UserSession session, CancellationToken ct);\n    Task UpdateUserSessionAsync(UserSessionKey key, UserSessionUpdate session, CancellationToken ct);\n    Task DeleteUserSessionAsync(UserSessionKey key, CancellationToken ct);\n    Task<IReadOnlyCollection<UserSession>> GetUserSessionsAsync(\n        PartitionKey partitionKey, UserSessionsFilter filter, CancellationToken ct);\n    Task DeleteUserSessionsAsync(\n        PartitionKey partitionKey, UserSessionsFilter filter, CancellationToken ct);\n}\n```\n\nRegister a custom store: `.AddServerSideSessions<YourCustomStore>()`\n\nSession cleanup is a separate concern — implement `IUserSessionStoreCleanup` and register with `.AddSessionCleanupBackgroundProcess()`.\n\n---\n\n## Common Pitfalls\n\n- **Calling `/bff/login` or `/bff/logout` via `fetch()`** — These endpoints trigger OIDC redirects and must be browser navigations (`window.location.href`), not AJAX calls.\n\n- **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.\n\n- **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.\n\n- **Forgetting `SaveTokens = true`** — Without this, OIDC tokens are not stored in the session, and `GetUserAccessTokenAsync()` returns nothing. Token management silently fails.\n\n- **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.\n\n- **Incorrect middleware order** — `UseBff()` must come after `UseRouting()` and before `UseAuthorization()`. Any deviation silently breaks anti-forgery enforcement without a clear error.\n\n- **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.\n\n- **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.\n\n- **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.\n\n- **Not configuring Data Protection in multi-instance deployments** — Cookie decryption failures manifest as users being perpetually logged out in load-balanced environments.\n\n- **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.\n\n- **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.\n\n---\n\n## Resources\n\n- [Duende BFF Overview](https://docs.duendesoftware.com/bff/)\n- [Getting Started: Single Frontend](https://docs.duendesoftware.com/bff/getting-started/single-frontend/)\n- [Embedded (Local) APIs](https://docs.duendesoftware.com/bff/fundamentals/apis/local/)\n- [Proxying Remote APIs](https://docs.duendesoftware.com/bff/fundamentals/apis/remote/)\n- [Multi-Frontend](https://docs.duendesoftware.com/bff/fundamentals/multi-frontend/)\n- [Server-Side Sessions](https://docs.duendesoftware.com/bff/fundamentals/session/server-side-sessions/)\n- [Token Management](https://docs.duendesoftware.com/bff/fundamentals/tokens/)\n- [Extensibility: Tokens](https://docs.duendesoftware.com/bff/extensibility/tokens/)\n- [Extensibility: HTTP Forwarder](https://docs.duendesoftware.com/bff/extensibility/http-forwarder/)\n- [Session Management Endpoints](https://docs.duendesoftware.com/bff/fundamentals/session/management/)\n- [BFF Options Reference](https://docs.duendesoftware.com/bff/fundamentals/options/)\n- [ASP.NET Core Data Protection](https://docs.duendesoftware.com/general/data-protection/)\n- [BFF v3 → v4 Upgrade Guide](https://docs.duendesoftware.com/bff/upgrading/bff-v3-to-v4/)\n- [NuGet: Duende.BFF](https://www.nuget.org/packages/Duende.BFF)\n- [NuGet: Duende.BFF.Yarp](https://www.nuget.org/packages/Duende.BFF.Yarp)\n- [NuGet: Duende.BFF.EntityFramework](https://www.nuget.org/packages/Duende.BFF.EntityFramework)\n- Related skills: `aspnetcore-authentication`, `token-management`, `identityserver-configuration`\n"
}

SHA-256: 8b0ed42049d5486fa5dc94226c4f457f03971d9bc7d5269495316868f3e18449