Duende Bff

作者 DuendeSoftwarefb32edc51982无许可证9 个星标收录于 2026年10月8日更新于 2026年10月8日仓库4周前更新

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.

AI 生成的概览

指导使用 Duende BFF 模式保护 SPA:会话、CSRF、令牌处理与 API 代理。

功能
该技能为在 ASP.NET Core 中实现 Duende.BFF 的 Backend-for-Frontend 安全模式提供参考指导。内容涵盖 BFF v3 与 v4 的注册方式、登录/登出管理端点、CSRF 防伪强制、带令牌类型的远程 API 代理、服务端会话存储、访问令牌管理,以及 SPA 集成(如 /bff/user 会话检查和 fetch 封装)。它产出配置片段与代码模式,而非可直接运行的产物。
适用场景
适用于构建或保护调用需认证 API 的 SPA(React、Angular、Vue、Blazor WASM),并希望令牌不进入浏览器时。也适用于配置 BFF 会话、登录/登出、CSRF 防护,或将前端请求代理到远程 API。
运行要求
不附带脚本,仅为说明性内容。应用这些指导需要 ASP.NET Core 项目及 Duende.BFF 相关包(可选 Duende.BFF.Yarp、Duende.BFF.EntityFramework、Duende.AccessTokenManagement)、一个 OpenID Connect 身份提供方,以及用于持久化会话存储的数据库。

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 configurationbuilder.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 middlewareapp.UseAuthorization();
app.Run();
csharp
// ❌ v4: Do NOT manually wire AddCookie + AddOpenIdConnect when using AddBff()// ConfigureOpenIdConnect and ConfigureCookies handle this correctlybuilder.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 wiringbuilder.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

FeatureV4V3
Auth handler setupConfigureOpenIdConnect() / ConfigureCookies()Manual AddCookie() / AddOpenIdConnect()
Management endpointsAuto-registeredMapBffManagementEndpoints() required
Remote API token type.WithAccessToken(RequiredTokenType.User).RequireAccessToken(TokenType.User)
Session cleanup.AddSessionCleanupBackgroundProcess()EnableSessionCleanup option
Token retrieverIAccessTokenRetriever (implement directly)DefaultAccessTokenRetriever (inheritable)
Multi-frontendBuilt-in AddFrontend() APINot supported
Middleware controlAutomaticallyRegisterBffMiddleware optionAlways 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 timeapp.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 orderapp.UseRouting();app.UseAuthentication();app.UseBff();           // Must be hereapp.UseAuthorization();app.MapControllers().AsBffApiEndpoint();
// ❌ Wrong: UseBff() after UseAuthorization() — anti-forgery is not appliedapp.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 headerapp.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 challengeapp.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 MapRemoteBffApiEndpointbuilder.Services.AddBff()    .AddRemoteApis();
// Maps /api/orders and all sub-paths to https://orders-service/ordersapp.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

RequiredTokenTypeBehavior
NoneNo token attached; anonymous passthrough
UserForwards the current user's access token; challenges if unauthenticated
ClientForwards a client credentials token; works even without a logged-in user
UserOrClientForwards user token if available, falls back to client token
UserOrNoneForwards 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 contextpublic 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-endpointapp.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 requirementsapp.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.EntityFrameworkbuilder.Services.AddBff()    .AddEntityFrameworkServerSideSessions(options =>    {        options.UseSqlServer(builder.Configuration.GetConnectionString("BffSessions"));    });
csharp
// ✅ Session cleanup (v4): manual registration requiredbuilder.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 SessionDbContextdotnet 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 endpointapp.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 handlerbuilder.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 loadasync 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 headerfunction bffFetch(url, options = {}) {    return fetch(url, {        ...options,        headers: {            'X-CSRF': '1',            ...options.headers,        },    });}
// Usageconst data = await bffFetch('/api/orders').then(r => r.json());
javascript
// ❌ Missing X-CSRF header — BFF will return 401const 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 wrapperasync 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 responseconst 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

ScenarioRecommended 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 contextNot 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 patternoptions.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 originsbuilder.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 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 configurationbuilder.Services.AddBff();
var proxyBuilder = builder.Services.AddReverseProxy()    .AddBffExtensions(); // Register BFF token management for YARP
// Configure routes in code using LoadFromMemoryproxyBuilder.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 MapReverseProxyapp.MapReverseProxy(proxyApp =>{    proxyApp.UseAntiforgeryCheck();});
app.Run();
csharp
// ❌ Do NOT omit UseAntiforgeryCheck() in the YARP pipeline —// anti-forgery is not automatically applied to YARP routesapp.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).

ExtensionPurpose
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 controlapp.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 modelbuilder.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.csbuilder.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 hostbuilder.Services.AddLocalApiHttpClient<WeatherClient>();

BffBlazorServerOptions

OptionDefaultPurpose
PollingInterval5 secondsHow often to check session status

BffBlazorClientOptions

OptionDefaultPurpose
RemoteApiPath/api/remoteBase path for remote API calls
BaseAddress(from host)Base address for API calls
Polling.Interval5 secondsSession status polling interval

BffOptions Reference

OptionDefaultPurpose
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
RevokeRefreshTokenOnLogouttrueRevoke refresh tokens on logout
AnonymousSessionResponse(null)Response for /bff/user when anonymous
BackchannelLogoutAllUserSessionsfalseLogout all sessions on backchannel notification
SessionCleanupInterval10 minutesInterval for expired session cleanup
AutomaticallyRegisterBffMiddlewaretrueV4: Auto-register BFF middleware; set false for multi-frontend manual control
DisableAntiForgeryCheck(null)V4: Delegate to conditionally skip anti-forgery per-request
IndexHtmlDefaultCacheDuration5 minutesV4: 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

来源与署名

来源:DuendeSoftware/duende-skills位于skills/duende-bff提交fb32edc

许可证: 无许可证

内容归原作者所有。SourceWeft 从公开仓库中收录这些内容。

举报或申请下架