← Default view
Itenium.Forge · itenium-ui admin console

Draw the BFF.
Don't build it.

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.

DrawingGA-01 · AUTH
Sheets1 + 6 details
Governing docBCP rev −27
StatusISSUED
Date2026-07-14
Refs cited166
Reading65 min
Retrofit2–5 days
DECISION · APPROVED 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.

GA

General arrangement

Both routes terminate on the same API contract
IDENTITY PROVIDER Keycloak / OpenIddict auth.itenium.be SAME SITE (eTLD+1) → IdP cookie is first-party → prompt=none still works → iframe silent renew alive BROWSER · admin.itenium.be React 19 + Vite SPA TanStack Query · server state one auth module one fetch wrapper ACCESS TOKEN — IN MEMORY reachable by any script in this origin STRICT CSP + TRUSTED TYPES the actual mitigation — Baseline Feb 2026 BFF · SPECIFIED, NOT ISSUED .NET 10 BFF YARP forwarder AddOpenIdConnect httpOnly cookie session X-CSRF header required SESSION STORE + DP KEY RING paid whenever you adopt — not an asymmetry EST. RETROFIT: 2–5 DAYS .NET 10 REST API — UNCHANGED AddJwtBearer accepts Authorization: Bearer OpenAPI 3.1 document generated TS client THE ONE-WAY DOOR IS HERE — AND WE ARE NOT OPENING IT. the frontend is the reversible side cookie + bearer schemes can coexist ROUTE A — BUILT · Authorization: Bearer <jwt> cookie + X-CSRF Bearer <jwt> ROUTE B — DEFERRED · same terminus, same contract 1 XSS & TOKEN STORAGE httpOnly ≠ XSS-proof — it bounds the blast 2 IETF BCP −27 names “business applications” — strong prior, no MUST 3 4 THE TWO-WAY DOOR this seam is the whole 2–5 day estimate 5 CODEGEN & THE DEV LOOP path-preserving proxy → spec untouched real cost: one more process, no “Try it out” 6 WHAT THE BFF WOULD BE MADE OF Duende.BFF $5,750/yr · or YARP + ~250 LOC you own GA-01 · AUTH · REV −27 · SCALE: NTS CALLOUTS 1–6 KEY TO THE SCHEDULE BELOW
Route A — built today Route B — specified, deferred Callout leader Every drawn element corresponds to a cited claim
LP

Load path — which argument actually carries

Two of three do not
Non-load-bearing

“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.

Load-bearing · bounded

“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]

★ Carries the whole load

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]
CS

Callout schedule — the six annotated components

Keys 1–6 on the drawing above
1

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.

survey 17 refs · 8 min
2

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.

recon 8 refs · 4 min
3

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.

survey 39 refs · 12 min
4

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.

survey 25 refs · 9 min
5

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”.

survey 27 refs · 13 min
6

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.

expedition 50 refs · 16 min
SN

Site note — a DNS choice moves the needle most

Not a security choice
Setting out

Put Keycloak on a subdomain of the app's registrable domain.

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].

FD

Foundation detail — what the 2–5 day estimate stands on

Four piles. Pull one and the estimate is fiction.

Load-bearing piles

  • One auth module. A single place that knows how a request is authenticated — swapping bearer for cookie is an edit to one file, not a search across the app.
  • One fetch wrapper. Every call goes through it. Adding credentials: 'include' and the CSRF header is a two-line change [28].
  • Relative paths only. No absolute API host baked into the client — a Vite proxy then routes /api and /bff locally without touching app code [26].
  • No client-side JWT parsing. The access token is meant to be opaque to the client; decoding it to read claims is discouraged [25]. Under a BFF there is no token to decode at all.

Where it cracks

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.

NC

Non-conformance — the one unbounded retrofit

Decide this before you build, not after
NCR-01

Real-time does not pass through a BFF cleanly.

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.

BM

Bill of materials — if and when you issue Route B

Duende.BFF's forwarder is YARP — the choice is whether to buy the auth layer on top
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].
DC

Daily cost — the tax is not where people expect

Codegen is a non-issue; the dev loop is the bill
Feared

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].

Actual

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.

Mispriced

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.

RS

Revision schedule — what would reissue this drawing

Absent these, the marginal security a BFF buys is the tail of the risk, not the body
△ A
The console needs real-time.The only unbounded retrofit on the sheet. SignalR through a BFF forces long-polling [13]. Reissue before construction, not after.
△ B
It starts handling customer PII, or becomes reachable from outside the corporate perimeter.This moves the console squarely into the BCP's “sensitive applications … personal data” clause [3].
△ C
A second client appears.Mobile, native, or a partner integration — all of which want bearer tokens anyway. One API can serve both schemes, but the BFF's value proposition changes shape [39].
△ D
A supply-chain incident lands close enough.Close enough to make “nothing in the browser to steal” worth $5,750 a year [14]. This is the trigger the whole load path hangs from.
△ E
Keycloak ends up off the registrable domain.Then silent renew degrades to full-page redirects [33] and the BFF starts winning on ergonomics — a weaker argument than security, but a real one.
RF

Reference documents

39 of 166 cited on this sheet
01Duende BFF — Remote APIs: the forwarder is YARP; the API keeps its bearer contract 02Curity — Token Handler: minimal changes to existing APIs 03IETF draft-ietf-oauth-browser-based-apps-27 — “strongly recommended for business applications” 04IETF BCP — “a different trade-off between security and simplicity” 05FusionAuth — BFF's only remaining vector is request proxying; no offline exfiltration 06Auth0 — “BFF shifts the security boundary. It doesn't eliminate it.” 07URIports — Trusted Types reached Baseline when Firefox shipped, Feb 2026 08Google / ACM CCS — 94.68% of script-limiting CSP policies ineffective 09oidc-spa — same registrable domain keeps the IdP first-party 10web.dev — a “site” is scheme + eTLD+1; subdomains are same-site 11Duende — a composing BFF forces you to transform the OpenAPI spec 12Duende BFF — server-side sessions require a store 13Duende Support #972 — WebSocket handshake cannot carry X-CSRF ⭐ 21 14Duende — pricing: Lite $5,750/yr, Standard $12,500, Advanced $24,900 15Duende — Community Edition: gated on org revenue <$1M, not app audience 16DuendeSoftware/foss — AccessTokenManagement is Apache-2.0 and free ⭐ 168 17Microsoft — .NET 10: cookie auth returns 401/403 for API endpoints 18dotnet/aspnetcore #8175 — OAuth 2 refresh tokens, open since 2019, now .NET 11 ⭐ 38.3k 19dotnet/yarp — MIT reverse-proxy toolkit; README does not cover auth ⭐ 9.6k 20Duende — licensing: production needs a key; trial caps at five sessions 21Wrapt — a standalone Duende BFF for a React SPA is ~50 lines of Program.cs 22Microsoft's CookieOidcRefresher — 102 lines, no single-flight locking ⭐ 1.1k 23Microsoft — multi-instance cookie auth needs a shared Data Protection key ring 24thoughtbot — one-way vs two-way door decisions 25oidc-spa — the access token is opaque to the client; don't decode it 26Vite — server.proxy: how a relative-path SPA runs against a BFF locally 27Swagger — “Try it out” cannot exercise cookie-authenticated endpoints 28openapi-fetch — cookie auth under a BFF is a two-line change 29NSwag #5229 — WithCredentials never reaches the generated client ⭐ 7.3k 30Keycloak — DPoP officially supported from 26.4 31TanStack Query #6249 — concurrent 401s fire duplicate refreshes ⭐ 50k 32RFC 9700 (BCP 240) — refresh tokens must be sender-constrained or rotated 33Keycloak — silent check-sso unsupported when third-party cookies are blocked 34InfoQ — a non-extractable DPoP key still leaves a signing oracle reachable from XSS 35De Ryck — avoiding localStorage is the wrong fix; XSS owns the app either way 36OWASP — do not store session identifiers in Web Storage 37web.dev — strict CSP does not stop injection into an already-nonced bundle 38dotnet/aspnetcore #58016 — DPoP still open, milestoned .NET 11 ⭐ 38.3k 39Microsoft — cookie and bearer schemes can coexist on one API
GA-01 · AUTH · itenium-ui 166 references · 6 detail sheets · 65 min Scale: NTS · Rev −27 · 2026-07-14 Read the full expedition →