| 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
- Tokens Never Touch the Browser — The BFF holds all OAuth tokens server-side; the browser only ever sees an HTTP-only, Secure, SameSite cookie
- 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
- 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
- Server-Side Sessions for Production — The default in-memory cookie session is unsuitable for production; persist sessions with
Duende.BFF.EntityFramework
- 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/identityserver/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.
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");
})
.ConfigureCookies(options =>
{
options.Cookie.SameSite = SameSiteMode.Lax;
});
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseRouting();
app.UseAuthentication();
app.UseBff();
app.UseAuthorization();
app.Run();
builder.Services.AddAuthentication()
.AddCookie("cookie")
.AddOpenIdConnect("oidc", ...);
BFF v3 Registration
For projects still on v3, explicit scheme setup is required and MapBffManagementEndpoints() must be called manually:
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();
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.
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.
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
app.MapGet("/api/data", (HttpContext ctx) => Results.Ok("data"))
.RequireAuthorization()
.AsBffApiEndpoint();
[Route("api/data")]
[BffApi]
public class DataController : ControllerBase
{
[HttpGet]
public IActionResult Get() => Ok("data");
}
app.MapControllers()
.RequireAuthorization()
.AsBffApiEndpoint();
app.MapGet("/api/data", () => Results.Ok("data"))
.RequireAuthorization();
Middleware Order
UseBff() must appear after UseRouting() but before UseAuthorization(). Incorrect order silently disables anti-forgery enforcement.
app.UseRouting();
app.UseAuthentication();
app.UseBff();
app.UseAuthorization();
app.MapControllers().AsBffApiEndpoint();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseBff();
Skipping Anti-Forgery
For specific endpoints that cannot send the anti-forgery header (e.g., webhook receivers), use .SkipAntiforgery():
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:
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:
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
builder.Services.AddBff()
.AddRemoteApis();
app.MapRemoteBffApiEndpoint("/api/orders", new Uri("https://orders-service/orders"))
.WithAccessToken(RequiredTokenType.User);
app.MapRemoteBffApiEndpoint("/api/public", new Uri("https://content-service/public"))
.WithAccessToken(RequiredTokenType.None);
app.MapRemoteBffApiEndpoint("/api/internal", new Uri("https://internal-service/api"))
.WithAccessToken(RequiredTokenType.Client);
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:
public class MyTokenRetriever : IAccessTokenRetriever
{
public Task<AccessTokenResult> GetAccessToken(GetAccessTokenContext context)
{
return Task.FromResult<AccessTokenResult>(
new BearerTokenResult(context.UserToken, "Bearer"));
}
}
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:
app.MapRemoteBffApiEndpoint("/api/long-running", new Uri("https://api.example.com"))
.WithAccessToken(RequiredTokenType.User)
.WithForwarderRequestConfig(new ForwarderRequestConfig
{
ActivityTimeout = TimeSpan.FromMinutes(5),
AllowResponseBuffering = true
});
app.MapRemoteBffApiEndpoint("/api/admin", new Uri("https://admin-service/api"))
.WithAccessToken(RequiredTokenType.User)
.RequireAuthorization("AdminPolicy");
app.MapRemoteBffApiEndpoint("/", new Uri("https://backend-service"));
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.
builder.Services.AddBff()
.AddServerSideSessions();
builder.Services.AddBff()
.AddEntityFrameworkServerSideSessions(options =>
{
options.UseSqlServer(builder.Configuration.GetConnectionString("BffSessions"));
});
builder.Services.AddBff(options =>
{
options.SessionCleanupInterval = TimeSpan.FromMinutes(5);
})
.AddEntityFrameworkServerSideSessions(options =>
{
options.UseSqlServer(connectionString);
})
.AddSessionCleanupBackgroundProcess();
builder.Services.AddBff()
.AddServerSideSessions();
EF Migrations for Session Store
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.
app.MapGet("/api/data", async (HttpContext ctx, IHttpClientFactory factory) =>
{
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();
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");
return Results.Text(await (await client.GetAsync("data")).Content.ReadAsStringAsync());
})
.AsBffApiEndpoint();
builder.Services.AddHttpClient<RemoteApiClient>(client =>
{
client.BaseAddress = new Uri("https://remote-service/");
})
.AddUserAccessTokenHandler();
var token = await ctx.GetUserAccessTokenAsync();
return Results.Json(new { accessToken = token });
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.
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.
async function getUser() {
const response = await fetch('/bff/user', {
headers: { 'X-CSRF': '1' }
});
if (response.ok) {
return await response.json();
}
return null;
}
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.
function bffFetch(url, options = {}) {
return fetch(url, {
...options,
headers: {
'X-CSRF': '1',
...options.headers,
},
});
}
const data = await bffFetch('/api/orders').then(r => r.json());
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.
async function bffFetch(url, options = {}) {
const response = await fetch(url, {
...options,
headers: { 'X-CSRF': '1', ...options.headers },
});
if (response.status === 401) {
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.
window.location.href = '/bff/login';
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 |
options.Cookie.SameSite = SameSiteMode.Strict;
options.Cookie.SameSite = SameSiteMode.Lax;
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.
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});
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.
builder.Services.AddCors(options =>
{
options.AddPolicy("SpaPolicy", policy =>
{
policy.WithOrigins("https://app.example.com")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});
});
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.
builder.Services.AddDataProtection()
.PersistKeysToAzureBlobStorage()
.ProtectKeysWithAzureKeyVault();
builder.Services.AddDataProtection()
.PersistKeysToDbContext<ApplicationDbContext>();
builder.Services.AddDataProtection();
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.
dotnet add package Duende.BFF.Yarp
Setup with In-Code Configuration
builder.Services.AddBff();
var proxyBuilder = builder.Services.AddReverseProxy()
.AddBffExtensions();
proxyBuilder.LoadFromMemory(
routes:
[
new RouteConfig
{
RouteId = "api",
ClusterId = "api-cluster",
Match = new RouteMatch { Path = "/api/{**catch-all}" }
}
.WithAccessToken(TokenType.User)
.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();
app.MapReverseProxy(proxyApp =>
{
proxyApp.UseAntiforgeryCheck();
});
app.Run();
app.MapReverseProxy();
YARP Configuration via appsettings.json
When using JSON configuration instead of LoadFromMemory, set BFF behavior via route metadata:
{
"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:
builder.Services.AddBff(options =>
{
options.AutomaticallyRegisterBffMiddleware = false;
});
var app = builder.Build();
app.UseRouting();
app.UseAuthentication();
app.UseBffPreProcessing();
app.UseBffFrontendSelection();
app.UseBffPathMapping();
app.UseBffOpenIdCallbacks();
app.UseBffStaticFileProxying();
app.UseAuthorization();
Frontend Configuration (Code)
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:
public class FrontendConfigTransformer : IIndexHtmlTransformer
{
public Task<string> TransformAsync(string indexHtml, HttpContext context)
{
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):
builder.Services.AddBff(options =>
{
options.IndexHtmlDefaultCacheDuration = TimeSpan.FromMinutes(10);
});
Pattern 11: Blazor Integration
Blazor Server
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)
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();
builder.Services.AddBffBlazorClient(options =>
{
options.RemoteApiPath = "/api/remote";
options.Polling = new BffBlazorClientPollingOptions
{
Interval = TimeSpan.FromSeconds(30)
};
});
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:
public class CustomLogoutEndpoint : ILogoutEndpoint
{
private readonly ILogoutEndpoint _inner;
public CustomLogoutEndpoint(ILogoutEndpoint inner) => _inner = inner;
public async Task<IResult> ProcessRequestAsync(HttpContext context)
{
var result = await _inner.ProcessRequestAsync(context);
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:
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