A general arrangement for browser auth in an internal admin console: React 19 + Vite in front, .NET 10 REST behind, Keycloak beside — and the backend-for-frontend drawn in dashed line, because it is specified but not issued for construction.
Build the SPA now. Do not build the BFF yet.
But build the SPA so that adding one is a 2–5 day change, and treat that discipline as non-negotiable rather than aspirational. The retrofit is a two-way door [24] precisely because the API never changes: a BFF forwards a bearer token to it, so AddJwtBearer stays exactly as it is [1][2].
The honest cost comparison is not “BFF now vs BFF later”. The session store and the shared Data Protection key ring [23] are paid whenever you adopt, not extra for adopting late [12]. So the real question on this sheet is “BFF ever, or never?” — and today the answer is not yet.
“It's internal, the BCP isn't about us”
This defence fails. The draft's strongest sentence names the case to its face — strongly recommended for business applications — and its threat model assumes only that attacker code runs in your origin [3]. But it prices its own advice honestly as “a different trade-off between security and simplicity”, with no RFC 2119 keyword [4]. A strong prior, not a mandate.
“A BFF stops XSS”
It doesn't. It converts silent, off-origin, outlives-the-tab token exfiltration into loud, tab-bound session riding through your own logs [5]. For an admin console that matters less than elsewhere — if the payload can grant itself a role in the first 400 ms, the tab-lifetime bound bought you nothing. “BFF shifts the security boundary. It doesn't eliminate it.” [6]
Supply chain
The one pro-BFF argument that survives all six angles. Strict CSP + Trusted Types genuinely closes injected-script XSS [7] — but CSP cannot distinguish your trojaned npm dependency from your own code. The nonced bundle executes with full privilege [37]. Against a September-2025-style compromise, “there is no token in the browser to steal” does work no CSP can do [5].
94.68% of script-limiting CSP policies measured ineffective — Google, ACM CCS [8]In-memory tokens vs cookie-backed BFF sessions under XSS
httpOnly cookies do not stop XSS — they convert silent token exfiltration into loud, tab-bound session riding. That's a real but bounded win; CSP + Trusted Types is the actual mitigation.
Does the IETF browser-based-apps BCP apply to an internal admin console?
The BCP names “business applications” in its strongest recommendation, so an internal console is in scope — but the ranking is a security/simplicity trade-off, not a normative MUST.
Refresh tokens and silent renew in a browser SPA (2026)
Iframe silent renew is dead cross-site but alive same-site; refresh-token rotation works on both stacks, DPoP only really works on Keycloak, and TanStack Query needs a single-flight mutex either way.
Greenfield BFF vs retrofitting later: where the asymmetry actually is
Adding a BFF later is a two-way door — if you build the SPA with one auth module, one fetch wrapper, relative paths and no client-side JWT parsing. The one-way door is the API, not the frontend.
What a BFF costs you every day: OpenAPI codegen and the local dev loop
A path-preserving BFF leaves OpenAPI codegen essentially untouched — the real daily cost is one extra process, a Vite proxy config, and losing Swagger UI “Try it out”.
BFF options in .NET 10: Duende.BFF, YARP, or hand-rolled
The realistic .NET 10 choice is YARP + AddOpenIdConnect + the free Apache-2.0 Duende.AccessTokenManagement (~250 LOC you own), or Duende.BFF if you qualify for its free Community Edition.
Iframe silent renew is not dead — it is dead cross-site. auth.itenium.be and admin.itenium.be are different origins but the same site, so the IdP cookie is first-party and prompt=none still works in 2026 [9][10]. Put it on a foreign host and Keycloak's own adapter tells you the session iframe and silent check-sso are unsupported [33] — the SPA's renewal story degrades into full-page redirects, which is where the BFF starts winning on ergonomics rather than on security. Either way, rotate refresh tokens with reuse detection [32] and put a single-flight mutex in the fetch wrapper — concurrent 401s otherwise fire duplicate refreshes [31].
credentials: 'include' and the CSRF header is a two-line change [28]./api and /bff locally without touching app code [26].The sharpest open question on this sheet is not whether the BFF is worth it. It is whether the discipline it depends on will hold.
The entire 2–5 day retrofit estimate rests on nobody ever calling jwtDecode() in a component, or reaching for getAccessToken() from app state.
Every team that missed that estimate missed it there.
Treat P1–P4 as a lint rule, not a code review preference.
SignalR and WebSockets are the single genuinely painful item in the whole retrofit: a WebSocket handshake cannot set the anti-forgery header the BFF requires, and teams report falling back to long-polling [13]. Everything else on this sheet is a two-way door. This is not. If itenium-ui will carry live updates, decide before you build — that is the one place where “later” is expensive.
| Part | Licence / cost | What you actually own |
|---|---|---|
|
YARP ⭐ 9.6k + AddOpenIdConnect+ Duende.AccessTokenManagement ⭐ 168 |
MIT + Apache-2.0 free, forever |
~250 lines of security-critical code you own forever — Microsoft's own documented recipe [16]. Note Microsoft's reference CookieOidcRefresher is 102 lines with no single-flight locking [22]. |
| Duende.BFF — Community Edition | $0 if org <$1M revenue |
The reference implementation, ~50 lines of Program.cs against Keycloak [21]. The gate is org revenue, not app audience — “it's just an internal tool” does not qualify you [15]. |
| Duende.BFF — Lite | $5,750/yr Standard $12,500 · Advanced $24,900 |
Production use needs a licence; trial mode caps at five sessions per host [20]. This is the number the supply-chain argument has to clear [14]. |
| .NET 10 platform — what it gives you free |
in-box | Cookie auth now returns 401 instead of a login redirect for API endpoints [17], and cookie + bearer schemes can coexist on one API [39]. But first-party OAuth refresh-token support is still unshipped and has slipped to .NET 11 planning [18] — as has DPoP [38]. |
OpenAPI codegen breaks
It doesn't. A path-preserving proxy leaves the OpenAPI document and the generated client untouched — only baseUrl and credentials: 'include' change [28]. Only a composing BFF turns codegen into a project — and Duende's default MapRemoteBffApiEndpoint shape is what quietly creates that problem [11]. NSwag users pay a small toll: WithCredentials never reaches the generated client [29].
One more process, and no “Try it out”
The genuine daily tax: a second process in the dev loop, a Vite proxy config [26], an HTTPS scheme both servers agree on — and Swagger UI can no longer exercise cookie-authenticated endpoints via “Try it out” [27]. Small, but paid every single day by every developer.
The session store
Server-side sessions plus a shared Data Protection key ring, the moment you run more than one replica [12][23]. Real operational weight — but you pay it whenever you adopt. It is not an asymmetry between now and later, which is exactly why it does not argue for building the BFF today.