Bff.AspNetCore
1.6.0
See the version list below for details.
dotnet add package Bff.AspNetCore --version 1.6.0
NuGet\Install-Package Bff.AspNetCore -Version 1.6.0
<PackageReference Include="Bff.AspNetCore" Version="1.6.0" />
<PackageVersion Include="Bff.AspNetCore" Version="1.6.0" />
<PackageReference Include="Bff.AspNetCore" />
paket add Bff.AspNetCore --version 1.6.0
#r "nuget: Bff.AspNetCore, 1.6.0"
#:package Bff.AspNetCore@1.6.0
#addin nuget:?package=Bff.AspNetCore&version=1.6.0
#tool nuget:?package=Bff.AspNetCore&version=1.6.0
Bff.AspNetCore
A reusable Backend-For-Frontend engine for ASP.NET Core. It terminates
authentication server-side: the browser only ever receives an opaque,
httpOnly session cookie, while the Keycloak access / refresh / id tokens
live in a Redis-backed token vault. Downstream API calls are reverse-proxied
through YARP with a Bearer token attached server-side.
Each per-app BFF (bff-katalogos, bff-erevna, ...) is a ~20-line
Program.cs wrapping this package.
Why
A SPA that holds a refresh token in browser storage is one XSS away from a long-lived account takeover. The BFF removes the token from the browser entirely. The native branded login form is preserved — the SPA posts credentials to its own-origin BFF, which does server-to-server ROPC against Keycloak with a confidential client.
What it provides
| Component | Responsibility |
|---|---|
| Cookie auth + Redis session store | __Host-bff-{app} cookie carries only a session id; tokens live in Redis at bff:{app}:sess:{id} |
| Redis-backed DataProtection key ring | Key ring persisted to Redis at bff:{app}:dataprotection-keys so cookies survive pod restarts and work across replicas; per-app scoped via SetApplicationName("bff-{app}") |
| Server-side ROPC client | grant_type=password against KC using the confidential client |
| YARP token-forwarding transform | /bff/api/* → downstream services with Authorization: Bearer attached |
Silent refresh + SETNX lock |
Stale access tokens refreshed server-side; a per-session Redis lock prevents concurrent-tab races |
| Auth endpoints | /bff/config, /bff/login, /bff/logout, /bff/me, /bff/otp/request, /bff/otp/verify, /bff/pin/login, /bff/register, /bff/forgot-password, /bff/reset-password |
| Anti-forgery (CSRF) | Custom X-BFF-Csrf header + Origin/Referer allow-list on state-changing requests |
| Hardening | No request-body logging, TLS to KC, token/secret redaction in logs |
Quick start
A complete per-app BFF Program.cs:
using Bff.AspNetCore.Extensions;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddBffServices(builder.Configuration);
var app = builder.Build();
app.UseBff();
app.Run();
appsettings.json (secrets come from environment / K8s Secret):
{
"Bff": {
"AppName": "katalogos",
"Keycloak": {
"Authority": "https://identity.dloizides.com",
"Realm": "onlinemenu",
"ClientId": "bff-katalogos-client",
"ClientSecret": "${BFF_CLIENT_SECRET}"
},
"Redis": { "ConnectionString": "redis:6379" },
"Proxy": {
"SpaUpstream": "http://katalogos-web",
"Downstreams": [
{ "Segment": "menus", "Upstream": "http://onlinemenu-api" }
]
},
"Csrf": {
"AllowedOrigins": [ "https://katalogos.dloizides.com" ]
},
"Methods": {
"Default": [ "Password" ],
"RoleOverrides": {
"door-staff": [ "Pin" ]
}
},
"TenantProxy": {
"TenantServiceUpstream": "http://tenant-api"
}
}
}
The optional Methods section declares which login methods this BFF offers.
Default is the set every role gets; RoleOverrides restricts (or grants) a
named role a different set. When Methods is omitted the BFF falls back to
Password only. Listing Otp in Default opens the /bff/otp/* endpoints;
without it they return 501. Listing Pin in Default opens
/bff/pin/login (the event-scoped PIN login); without it it returns 501.
Public API
// Registration — bind + validate the `Bff` config section and register
// the whole engine (Redis store, ROPC client, YARP proxy, CSRF, endpoints).
IServiceCollection AddBffServices(
this IServiceCollection services,
IConfiguration configuration,
Action<BffOptions>? configure = null);
// Pipeline — anti-forgery middleware + auth endpoints + reverse proxy.
WebApplication UseBff(this WebApplication app);
The SPA contract
The SPA must:
- Send credentials to the BFF, not Keycloak —
POST /bff/loginwith{ username, password }. The response body is{ user: { ...claims } }; it never contains a token. - Call downstream APIs through
/bff/api/*— the cookie is sent automatically; the BFF attaches theBearer. - Send
X-BFF-Csrf: 1on every state-changing request (POST/PUT/PATCH/DELETE) to/bff/*and/bff/api/*. - Treat 401 from
/bff/api/*or/bff/meas "session ended" — redirect to the login form.
Endpoints
| Method + path | Purpose |
|---|---|
GET /bff/config |
Anonymous capability descriptor — enabled login methods + registrationEnabled; read by the SPA before login |
POST /bff/login |
ROPC login; creates the session, sets the cookie |
POST /bff/logout |
KC end-session, deletes the Redis session, clears the cookie |
GET /bff/me |
Current user's sanitised claims, or 401 |
POST /bff/otp/request |
Proxied to TenantService send-OTP; 501 when Otp is not an enabled method |
POST /bff/otp/verify |
Email-OTP login (KC direct-grant); creates the session, sets the cookie; 501 when Otp is disabled |
POST /bff/pin/login |
Event-scoped PIN login (KC direct-grant with pin + eventExternalId); creates the session, sets the cookie; 501 when Pin is disabled |
POST /bff/register |
Proxied to TenantService |
POST /bff/forgot-password |
Proxied to TenantService |
POST /bff/reset-password |
Proxied to TenantService |
/bff/api/{segment}/* |
Reverse-proxied downstream with Bearer attached |
/* |
Reverse-proxied to the SPA's nginx upstream |
Security notes
- The session cookie is
__Host--prefixed: httpOnly, Secure, SameSite=Lax, Path=/. It carries only the opaque session id. - Refresh-token rotation at the realm level is deferred (Phase 6). The
per-session
SETNXrefresh lock is the concurrency protection until then. - Request bodies are never logged — login bodies contain passwords.
- Set
Keycloak.SkipTlsValidationonly for staging's self-signed cert.
License
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Fido2 (>= 4.0.1)
- Fido2.Models (>= 4.0.1)
- Konscious.Security.Cryptography.Argon2 (>= 1.3.1)
- Microsoft.AspNetCore.DataProtection.StackExchangeRedis (>= 10.0.8)
- StackExchange.Redis (>= 2.8.16)
- Yarp.ReverseProxy (>= 2.2.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.16.1 | 100 | 7/22/2026 |
| 1.16.0 | 91 | 7/22/2026 |
| 1.15.0 | 95 | 7/22/2026 |
| 1.14.0 | 91 | 7/22/2026 |
| 1.13.1 | 102 | 7/20/2026 |
| 1.13.0 | 89 | 7/20/2026 |
| 1.12.0 | 107 | 7/19/2026 |
| 1.11.1 | 90 | 7/19/2026 |
| 1.11.0 | 97 | 7/19/2026 |
| 1.10.0 | 99 | 7/17/2026 |
| 1.9.1 | 104 | 7/14/2026 |
| 1.9.0 | 102 | 7/14/2026 |
| 1.8.0 | 97 | 7/13/2026 |
| 1.7.0 | 112 | 7/9/2026 |
| 1.6.0 | 102 | 7/9/2026 |
| 1.5.0 | 104 | 7/6/2026 |
| 1.4.0 | 108 | 7/4/2026 |
| 1.2.5 | 109 | 5/23/2026 |
| 1.2.4 | 107 | 5/23/2026 |
| 1.2.3 | 110 | 5/23/2026 |