Decision. Duende.BFF is the only option that ships the whole pattern (login/logout/user endpoints, CSRF, token refresh, server-side sessions, back-channel logout) — but production use needs a licence, and the cheapest listed tier is $5,750/yr [1], free only under the Community Edition (<$1M revenue, <$3M capital) [2].
- Pick Duende.BFF if you qualify for Community Edition, or the licence is expensable. It is the reference implementation and works against any OIDC provider, Keycloak included [3].
- Pick YARP +
AddOpenIdConnect+Duende.AccessTokenManagement(Apache-2.0, free [4]) if the licence is a blocker. This is Microsoft’s own BFF recipe [5] — ~250 lines of security-critical code you own and upgrade forever.- Pick OidcProxy.Net (⭐ 159, LGPL-3.0) if you want a config-only OSS BFF and accept a small bus factor [6].
- Pick
oauth2-proxy(⭐ 15k) if the console already lives behind a reverse proxy and a .NET BFF is a project you don’t want [7].- Never hand-roll token refresh. Every serious write-up says the same thing: refresh races, rotation reuse-detection and multi-instance token stores are where custom BFFs break [8].
⚠ .NET 10 shrank the gap but did not close it: the cookie handler finally returns 401 instead of a login redirect for API endpoints [9], but built-in OAuth refresh-token support is still unshipped and now sits in .NET 11 Planning [10] ⭐ 38.3k.
The comparison
| Option | Licence / cost | LOC you own | Keycloak | OpenIddict | Server-side sessions | Token refresh | CSRF built in | Back-channel logout | Maturity | Upgrade burden | Maintainer |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Duende.BFF v4 | Commercial; free Community Edition under thresholds; trial = 5 sessions/host [11] | ~50 (config only) [3] | ✓ [3] | ✓ [50] | ✓ AddServerSideSessions() + EF store [12] |
✓ (bundled ATM) | ✓ X-CSRF: 1 header [13] |
✓ [12] | 4.7M NuGet downloads; repo ⭐ 1.6k [14] [15] | ⚠ High. v4 renamed interfaces + a breaking DB schema change [16]; 4.2.0 dropped net8/net9 [17] | Duende Software |
| YARP + OIDC + ATM | MIT (YARP) + Apache-2.0 (ATM) → $0 [18] [4] | ~200–400 (see audit) | ✓ | ✓ | ✗ (write an ITicketStore) |
✓ via ATM [19] | ✗ (write it) | ✗ (write it) [20] | ⭐ 9.6k, 68.7M downloads [18] [21] | Low for the libs; you own the glue | Microsoft (.NET Foundation) |
Hand-rolled (IHttpForwarder / HttpClient) |
$0 | ~250–500 | ✓ | ✓ | ✗ | ⚠ you write it — don’t | ✗ | ✗ | n/a | Yours, forever | You |
| OidcProxy.Net | LGPL-3.0 [6] | ~20 (config) | ✓ | ✓ | ✓ (Redis) | ✓ | ✓ | partial | ⭐ 159 — small | Unknown; low bus factor | Community |
| oauth2-proxy sidecar | MIT [7] | 0 (YAML) | ✓ | ✓ (generic OIDC) | ✓ Redis session store [22] | ✓ cookie-refresh [22] |
⚠ own CSRF cookie, not a BFF header | ✗ | ⭐ 15k, v7.15.3 (Jun 2026) [7] | Low (container tag) | oauth2-proxy org |
Stars: dotnet/yarp ⭐ 9.6k · DuendeSoftware/products ⭐ 1.6k · oauth2-proxy ⭐ 15k · OidcProxy.Net ⭐ 159 (Jul 2026).
1. Duende.BFF v4 — the reference implementation
What the package actually gives you. A management endpoint surface under /bff (ManagementBasePath, default /bff) — /bff/login, /bff/logout, /bff/user [23]; anti-forgery enforced on API routes by requiring an X-CSRF: 1 header (the header forces a CORS preflight for cross-origin callers, which is what actually blocks CSRF) [13]; remote-API proxying via MapRemoteBffApiEndpoint("/api/users", new Uri("https://remote/users")).WithAccessToken(RequiredTokenType.User) [24]; optional server-side sessions with an EF store, which both shrinks the cookie to a session id and makes back-channel logout revocation possible [12]. Tokens never reach the browser [23].
Note what the proxy is: Duende.BFF’s forwarder is YARP — “It uses Microsoft YARP internally, but is much simpler to configure than YARP”, shipped as the Duende.BFF.Yarp package [24]. You are not choosing between Duende and YARP; you are choosing whether to buy the auth layer that sits on top of YARP.
v4 (Dec 2, 2025) added multi-frontend hosting (one BFF host serving several SPAs), OpenTelemetry, and auto-wiring of the middleware/endpoints [25]. Current is 4.2.0 (2026-06-10) [14].
Does it need Duende IdentityServer? No. BFF drives the stock ASP.NET Core OIDC handler, which “can be configured to use any OpenID Connect provider” [50]. The Wrapt walkthrough runs a standalone Duende BFF for a React SPA against Keycloak in Docker, ~50 lines in Program.cs (AddBff().AddRemoteApis(), cookie __Host- prefix + SameSite=Strict, AddOpenIdConnect pointed at the Keycloak realm, one MapRemoteBffApiEndpoint) [3]. OpenIddict is just another OIDC server to it.
Licensing — the numbers, from the source
- Production use requires a licence. “Duende products, except for our open source tools, require a license for production use.” BFF is licensed as its own product, in two editions (Starter, Enterprise) [11].
- Trial mode is not a free tier. BFF v4 without a key logs an error at startup and is “limited to a maximum of five (5) sessions per host”; excess sessions log an error per session. The limit is “not technically enforced” and not shared across nodes — it degrades to log noise, not an outage [11].
- Price. The 2026 pricing page lists Lite $5,750, Standard $12,500, Advanced $24,900 per year (billed annually), each bundling a BFF front-end quota (2 / 10 / 30 “BFF Front-Ends”); add-ons priced separately (SAML $1,500, Automatic Key Management $4,000, FAPI conformance $7,500). No EUR list price is published [1].
- Free path: Community Edition. Eligibility is revenue-based and explicit: “For-profit organizations with less than $1M USD projected annual gross revenue and access to less than $3M USD in capital facilities, or non-profit organizations and registered charities with a published annual budget under $1M USD” [2]. The BFF licensing docs point small orgs at the same Community Edition [11].
- “It’s only an internal tool” does not make it free. Nothing in the terms exempts internal/non-customer-facing production use — the gate is org revenue, not app audience. Community Edition also explicitly excludes redistribution and building identity infra for an end customer [2]. ⚠ For a consultancy: if the console runs in your infra for your staff, you’re judged on your own revenue; if it ships to a client, that client needs the licence.
- Licence validation is fully local — “no outbound network calls related to license validation” — and the same key may be used in dev/test/QA (“We do not consider the key a secret”) [11] [26].
Upgrade burden
⚠ The v3→v4 hop is real work: TokenType → RequiredTokenType (new namespace), RequireAccessToken() → WithAccessToken(), IUserService → IUserEndpoint, default implementations went from virtual to internal, and a breaking database schema change — UserSessions.ApplicationName renamed to PartitionKey, requiring all BFF instances sharing that table to move together [16]. Duende also tracks the .NET train aggressively: 4.1.2 (Mar 2026) targeted net8/net9/net10; 4.2.0 (Jun 2026) targets net10.0 only [17]. On .NET 10 that’s a non-issue — but it tells you the support window is short.
2. YARP — a proxy, not an auth solution
YARP is Yarp.ReverseProxy ⭐ 9.6k, MIT, 68.7M downloads, latest 2.3.0 (Feb 2025) — no 2.4/3.0 as of Jul 2026 [18] [21]. Stable, not stagnant: in .NET 10 its docs moved into the ASP.NET Core doc set under fundamentals/servers/yarp [27].
What it does not do: “No authentication or authorization is performed on requests unless enabled in the route or application configuration.” YARP consumes ASP.NET Core’s auth primitives — you configure AddAuthentication/AddOpenIdConnect, YARP just lets you attach an AuthorizationPolicy per route. On flowing identity onward, the docs are blunt: OIDC “authentication process can be configured in the proxy application and will result in an authentication cookie. That cookie will flow to the destination server as a normal request header” — i.e. by default your API gets the cookie, not a bearer token, and swapping credential types “can be performed using custom request transforms” [27].
Token injection is a transform. AddTransforms(ctx => ctx.AddRequestTransform(async t => { … })) [28]. Microsoft’s own .NET 10 BFF sample does exactly this:
app.MapForwarder("/weather-forecast", "https://weatherapi", transformBuilder =>
{
transformBuilder.AddRequestTransform(async transformContext =>
{
var accessToken = await transformContext.HttpContext.GetTokenAsync("access_token");
transformContext.ProxyRequest.Headers.Authorization = new("Bearer", accessToken);
});
}).RequireAuthorization();
— BlazorWebAppOidcBffAutoYarpAspire/BlazorWebAppOidc/Program.cs [29] ⭐ 1.1k.
Is there an official YARP-as-BFF sample? Yes — BlazorWebAppOidcBffAutoYarpAspire in dotnet/blazor-samples ⭐ 1.1k (Jul 2026), documented on Learn as “OIDC with the Backend for Frontend (BFF) pattern with YARP and Aspire” [5]. It is Blazor-shaped, but the BFF half (cookie + OIDC + MapForwarder + transform + MapLoginAndLogout) is framework-agnostic and transplants onto a React SPA unchanged.
The credible free stack is YARP for proxying + Microsoft.AspNetCore.Authentication.OpenIdConnect for the protocol + Duende.AccessTokenManagement.OpenIdConnect for the refresh cycle — the last is Apache-2.0, free, no licence key, 4.2.0 (Mar 2026), net8/9/10 [4] [19] [30]. This is precisely what Tim Deschryver’s YARP-BFF series lands on: hand-write /bff/login (challenge), /bff/logout (sign-out both schemes) and /bff/user (claims), then a RequestTransform calling GetUserAccessTokenAsync(), “because manually retrieving the token doesn’t automatically handle token expiration and most importantly the refresh cycle” [31] [32]. Also remember RequestHeaderRemoveTransform("Cookie") — strip the session cookie before forwarding [32]. With Aspire, AddServiceDiscoveryDestinationResolver() removes hard-coded destination URLs [33].
3. Minimal custom — how many lines do you actually own?
You don’t even need full YARP routing: IHttpForwarder/MapForwarder is YARP’s low-level direct-forwarding API — “these applications do not need … configuration discovery, routing, load balancing” — one app.MapForwarder("/{**catch-all}", …) plus a HttpMessageInvoker and you have a proxy [34]. And if the SPA is served from the BFF’s own wwwroot (damienbod’s samples do this; YARP is used only in dev to front the Vite server), the whole thing is same-origin and the “proxy” is a thin /api passthrough [35] ⭐ 24.
LOC audit — counted from Microsoft’s own .NET 10 BFF sample:
| File you’d own | LOC | What it does |
|---|---|---|
CookieOidcRefresher.cs |
102 | refresh-token exchange at OnValidatePrincipal, re-validate the id_token, re-issue the cookie [36] |
CookieOidcServiceCollectionExtensions.cs |
26 | wires the refresher, sets SaveTokens, adds offline_access |
LoginLogoutEndpointRouteBuilderExtensions.cs |
46 | /login challenge + /logout sign-out |
auth wiring inside Program.cs |
~40 | AddOpenIdConnect + AddCookie + MapForwarder + transform [29] |
| Microsoft’s baseline | ~215 | and this is the happy path |
+ /bff/user endpoint, X-CSRF enforcement middleware |
~35 (est.) | Duende gives these free [13] |
+ ITicketStore (server-side sessions, distributed cache) |
~80 (est.) | needed once cookies get fat [37] |
| + back-channel logout endpoint + revocation | ~60–100 (est.) | ASP.NET Core has none built in [20] |
| Realistic total | ~250–450 | security-critical, yours forever |
The 215-line baseline is not a strawman: CookieOidcRefresher.cs carries a comment pointing straight at dotnet/aspnetcore#8175 ⭐ 38.3k — the still-open request for built-in refresh support. Microsoft ships it as sample code you copy, not as a package [36].
What’s easy to get wrong
| Pitfall | Why it bites | Evidence |
|---|---|---|
| Refresh races | 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 [36] | “Race conditions when multiple requests need renewal simultaneously… Token storage conflicts in multi-instance deployments… Real-world incidents have occurred where custom implementations accidentally exposed tokens to wrong users” [8] |
SaveTokens = true blows up the cookie |
Cookie limit ~4 KB, header limit ~8 KB; base64 JWTs + Keycloak group/role claims overflow it → HTTP 431, silent truncation, intermittent logout, WAF drops. ChunkingCookieManager splits at 3500 bytes but is “tactical only” |
[37] |
| The real fix is a session store | ITicketStore / server-side sessions — cookie becomes an opaque key. Duende does this with one call; you write it |
[37] [12] |
| CSRF | Cookie auth means the browser attaches credentials automatically. You need SameSite, __Host- prefix and a required custom header; Deschryver’s otherwise-solid walkthrough simply doesn’t cover antiforgery |
[13] [31] |
| Back-channel logout | No built-in support. You implement the webhook, validate the logout token, persist to a distributed cache, and check it on every request via a cookie event handler | [20] |
| 401 vs 302 on proxied calls | Historically the killer: an expired cookie turned an XHR into a 302 to Keycloak. Fixed in .NET 10 (see below) | [9] |
| Forwarding the cookie downstream | Strip it — the API should see only Authorization: Bearer |
[32] |
Nestenius’s series ends by migrating the 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 (app.CheckForCsrfHeader()) disappears entirely [38]. That is the honest LOC-vs-cost exchange rate: ~250–450 lines ↔ $5,750/yr (or $0 if Community Edition applies).
⚠ Don’t use ardalis/BFF ⭐ 6 — it’s a stale fork of the archived Duende BFF repo, not a maintained alternative [39].
4. What .NET 10 itself now ships
| Change | Shrinks the BFF gap? | Status |
|---|---|---|
Cookie handler returns 401/403 instead of redirecting for endpoints carrying the new IApiEndpointMetadata (auto-added to [ApiController], JSON minimal APIs, TypedResults, SignalR). Opt out with [AllowCookieRedirect] or an AppContext switch |
✓✓ Removes the single most annoying hand-rolled-BFF hack (custom OnRedirectToLogin to force 401) |
Shipped, breaking [9] [40] |
Official BFF-with-YARP + Aspire sample (BlazorWebAppOidcBffAutoYarpAspire), plus a MinimalApiJwt downstream API and Microsoft.Extensions.ServiceDiscovery.Yarp for destination resolution |
✓ Gives you a Microsoft-blessed skeleton to copy | Shipped (docs/sample, not a package) [5] [29] |
Auth/authz metrics (aspnetcore.authentication.authenticate.duration, sign-in/out counters) |
~ Ops nicety | Shipped [40] |
| Passkeys / WebAuthn in ASP.NET Core Identity | ✗ Irrelevant if Keycloak/OpenIddict is the IdP | Shipped [40] [41] |
PAR (RFC 9126) used automatically when the IdP advertises it (PushedAuthorizationBehavior.UseIfAvailable, since .NET 9) |
✓ Free hardening for the BFF’s OIDC leg; Keycloak supports PAR (Entra does not) | Shipped in .NET 9, default in .NET 10 samples [29] |
| OAuth 2 refresh-token support (#8175, open since 2019) | ✗✗ The core gap. Was on the .NET 10 aspirational list; still open, now milestoned .NET 11 Planning | Not shipped [10] [42] |
| DPoP (#58016) — “The current option in .NET is to either use Duende libraries, or write your own” | ✗ | Not shipped, .NET 11 Planning [43] |
A first-party BFF package or dotnet new bff template |
✗ | Does not exist. No BFF item appears anywhere in the .NET 10 auth roadmap [42]; the “token handler pattern” remains third-party (Duende, OidcProxy.Net) [6] |
Back-channel logout, ITicketStore implementation, antiforgery-for-BFF |
✗ | Still yours to write [20] |
Read: .NET 10 makes the hand-rolled BFF pleasanter (401s, an official sample, better metrics), not smaller. The two things that would actually shrink the code you own — refresh-token management and DPoP — both slipped to .NET 11 planning. Until then, “free token management” means Duende.AccessTokenManagement (Apache-2.0), which is a Duende library either way — you’re just not paying for it [4].
5. The option the .NET framing misses: a sidecar OIDC proxy
If the console already sits behind an ingress, the BFF can be a container instead of a codebase.
| Tool | Stars / licence | Fit as a BFF |
|---|---|---|
| oauth2-proxy | ⭐ 15k, MIT, v7.15.3 (Jun 2026) [7] | Closest thing to a drop-in BFF. Generic OIDC (Keycloak listed), Redis session store, cookie-refresh. ⚠ Token flow is header-shaped, not BFF-shaped: pass-access-token → X-Forwarded-Access-Token; pass-authorization-header sends the ID token as Bearer; set-authorization-header only helps in nginx auth_request mode. Your .NET API must be told which header to trust [22] |
| Pomerium | ⭐ 4.9k, Apache-2.0, v0.32.9 (Jun 2026) [44] | Identity-aware proxy / zero-trust gateway. Heavier: policy engine, its own control plane. Overkill for one admin console |
| traefik-forward-auth | ⭐ 2.4k, MIT, v2.3.0 [45] | Gate-only. Authenticates the user, forwards identity headers — does not mint/refresh access tokens for a downstream API |
Traefik ForwardAuth / nginx auth_request |
built into the proxy | Primitives, not solutions: ForwardAuth “delegates authentication to an external service” [46]; nginx auth_request “implements client authorization based on the result of a subrequest” [47]. Traefik’s own OIDC middleware is a commercial Hub/Enterprise feature |
| Keycloak Gatekeeper / louketo-proxy | ⭐ 948 — archived Dec 2020, EOL [48] | Dead. Community successor: gogatekeeper ⭐ 318, Apache-2.0, v4.10.0 (Jun 2026) [49] — alive but tiny |
When the sidecar wins: you already run the reverse proxy, you want zero .NET security code, and the admin console’s APIs can accept a header you configure. When it loses: the sidecar gives you a gate, not a BFF — no /bff/user shape, CSRF is its own scheme, back-channel logout is absent, and the token it forwards may be the ID token, not a scoped access token. For an internal admin console fronting your own .NET APIs, the impedance mismatch usually costs more than the ~250 lines it saves.
Verdict for itenium-ui
Greenfield, internal, Keycloak-or-OpenIddict, one frontend:
- Check Community Edition eligibility first — it is a two-line revenue test [2], and if it passes, Duende.BFF is free, complete, and supported, and the answer is trivially Duende.
- If not eligible: YARP +
AddOpenIdConnect+Duende.AccessTokenManagement, copied from Microsoft’sBlazorWebAppOidcBffAutoYarpAspiresample [5]. Budget ~250–450 LOC, and put server-side sessions (ITicketStore) in from day one rather than after the first HTTP 431 [37]. - Serve the SPA from the BFF’s own origin (wwwroot in prod, dev-proxy in dev) — it makes SameSite/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; the same licence covers 2 front-ends and the org’s other apps, so evaluate it at portfolio level, not per app [1].