The Community Edition gate is org revenue, not app audience. "It's only an internal tool" does not qualify you [2].
→Complete, supported, reference implementation. The answer is trivially Duende — take the free Community Edition licence and stop reading [11].
→Microsoft's own recipe. Free forever, ~250–450 lines of security-critical code you own forever [5].
/bff/login, /bff/logout, /bff/user, CSRF, refresh, server-side sessions, back-channel logout.4.7M NuGet downloads [14]. Anti-forgery is a single required X-CSRF: 1 header, which forces a CORS preflight for cross-origin callers — that's what actually blocks CSRF [13]. It does not need Duende IdentityServer: it drives the stock ASP.NET Core OIDC handler, so Keycloak and OpenIddict are just OIDC servers to it [50]. v4 (Dec 2025) added multi-frontend hosting and OpenTelemetry [25].
⚠ Trial mode is not a free tier: no key = a startup error and a 5-sessions-per-host limit that degrades into log noise, not an outage [11]. 4.2.0 dropped net8/net9 — the support window is short [17].
68.7M downloads [21]. YARP itself does no auth: "No authentication or authorization is performed on requests unless enabled in the route or application configuration", and by default your API receives the cookie, not a bearer token — swapping credential types "can be performed using custom request transforms" [27]. The transform is the whole trick:
transformBuilder.AddRequestTransform(async t => { var at = await t.HttpContext.GetTokenAsync("access_token"); t.ProxyRequest.Headers.Authorization = new("Bearer", at); }); — straight out of Microsoft's BlazorWebAppOidcBffAutoYarpAspire ⭐ 1.1k [29]. It is Blazor-shaped, but the BFF half transplants onto a React SPA unchanged [5].
Use GetUserAccessTokenAsync() from ATM, not raw token retrieval — the latter "doesn't automatically handle token expiration and most importantly the refresh cycle" [32]. And add RequestHeaderRemoveTransform("Cookie") [31].
IHttpForwarder / HttpClient)app.MapForwarder("/{**catch-all}", …) plus an HttpMessageInvoker and you have a proxy — no routing, no load balancing, no config discovery [34].Legitimate in exactly one shape: serve the SPA from the BFF's own wwwroot and the whole thing is same-origin, so the "proxy" collapses to a thin /api passthrough — which is what damienbod's sample ⭐ 24 does, using YARP only in dev to front the Vite server [35].
⚠ Never hand-roll token refresh. Microsoft's own 102-line CookieOidcRefresher.cs has no locking and no single-flight [36] — and its header comment points straight at dotnet/aspnetcore#8175 ⭐ 38.3k, the still-open request for built-in support [10]. Also don't use ardalis/BFF ⭐ 6 — a stale fork of the archived Duende repo [39].
Functionally it is the closest OSS thing to Duende.BFF — Redis-backed sessions, refresh, CSRF, partial back-channel logout, all from config [6]. The question is not capability, it's whether you want your admin console's session security depending on a 159-star project with an unclear maintainer.
⚠ It gives you a gate, not a BFF. The token flow is header-shaped, not BFF-shaped: pass-access-token → X-Forwarded-Access-Token; pass-authorization-header sends the ID token as the Bearer; set-authorization-header only helps in nginx auth_request mode [22]. There is no /bff/user shape, CSRF is its own cookie scheme, and back-channel logout is absent. For an internal console fronting your own .NET APIs the impedance mismatch usually costs more than the ~250 lines it saves.
Free for development, testing and personal projects — you only pay for production [1]. Licence validation is fully local, no outbound calls, and the same key may be used in dev/test/QA [11] [26].
Excludes redistribution and building identity infra for an end customer. If the console runs in your infra for your staff you are judged on your own revenue; if it ships to a client, that client needs the licence [2].
OnValidatePrincipal, re-validate the id_token, re-issue the cookie [36]SaveTokens, adds offline_access/login challenge + /logout sign-out
That is the honest exchange rate. Nestenius's series ends by migrating a from-scratch BFF to Duende and reports the collapse: the whole custom BffController becomes app.MapBffManagementEndpoints();, the proxying becomes one line, and the custom CSRF middleware disappears entirely [38].
Concurrent requests each hit the token endpoint with the same refresh token. With rotation + reuse detection (Keycloak's default posture) that can nuke the session. Microsoft's 102-line refresher has no locking or single-flight. "Real-world incidents have occurred where custom implementations accidentally exposed tokens to wrong users." [8] [36]
Cookie limit ~4 KB, header limit ~8 KB. Base64 JWTs plus Keycloak group/role claims overflow it → HTTP 431, silent truncation, intermittent logout, WAF drops. ChunkingCookieManager splits at 3500 bytes but is tactical only. [37]
ITicketStore / server-side sessions — the cookie becomes an opaque key. Duende does this with one call; on the free path you write it. Put it in from day one, not after the first HTTP 431. [12]
Cookie auth means the browser attaches credentials automatically. You need SameSite, the __Host- prefix and a required custom header. Deschryver's otherwise-solid YARP walkthrough simply doesn't cover antiforgery. [13] [31]
No built-in support anywhere in ASP.NET Core. You implement the webhook, validate the logout token, persist it to a distributed cache, and check it on every request via a cookie event handler. [20]
Strip it. The API should see only Authorization: Bearer — use RequestHeaderRemoveTransform("Cookie"). [32]
Historically the killer: an expired cookie turned an XHR into a 302 to Keycloak, and every hand-rolled BFF carried a custom OnRedirectToLogin hack to force a 401. As of .NET 10 the cookie handler returns 401/403 for endpoints carrying the new IApiEndpointMetadata — auto-added to [ApiController], JSON minimal APIs, TypedResults and SignalR. Opt out with [AllowCookieRedirect]. [9]
BlazorWebAppOidcBffAutoYarpAspire), plus a MinimalApiJwt downstream API and service-discovery destination resolution. A Microsoft-blessed skeleton to copy. [5] [29]aspnetcore.authentication.authenticate.duration, sign-in/out counters). An ops nicety. [40]dotnet new bff template. Does not exist. No BFF item appears anywhere in the .NET 10 auth roadmap; the token-handler pattern remains third-party. [42]ITicketStore implementation, antiforgery-for-BFF. Still yours to write. [20]Duende.AccessTokenManagement (Apache-2.0), which is a Duende library either way. You're just not paying for it. [4]
Gate-only. Authenticates the user and forwards identity headers — it does not mint or refresh access tokens for a downstream API. [45]
Dead. The community successor is gogatekeeper ⭐ 318, Apache-2.0, v4.10.0 (Jun 2026) — alive but tiny. [48] [49]
/bff/user shape, CSRF is its own scheme, back-channel logout is absent, and the token it forwards may be the ID token rather than a scoped access token.
It is a two-line revenue test [2]. If it passes, Duende.BFF is free, complete and supported, and the answer is trivially Duende. Stop here.
wwwroot in production, dev-proxy in dev. It makes SameSite and CSRF tractable and removes most of the proxy [35].
$5,750/yr buys ~350 lines of security code you never write, a DB-backed session store, back-channel logout, and someone to call. For a single internal console that's expensive — but the same licence covers 2 front-ends and the org's other apps [1].