Bff.AspNetCore 1.8.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package Bff.AspNetCore --version 1.8.0
                    
NuGet\Install-Package Bff.AspNetCore -Version 1.8.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Bff.AspNetCore" Version="1.8.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Bff.AspNetCore" Version="1.8.0" />
                    
Directory.Packages.props
<PackageReference Include="Bff.AspNetCore" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Bff.AspNetCore --version 1.8.0
                    
#r "nuget: Bff.AspNetCore, 1.8.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Bff.AspNetCore@1.8.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Bff.AspNetCore&version=1.8.0
                    
Install as a Cake Addin
#tool nuget:?package=Bff.AspNetCore&version=1.8.0
                    
Install as a Cake Tool

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:

  1. Send credentials to the BFF, not KeycloakPOST /bff/login with { username, password }. The response body is { user: { ...claims } }; it never contains a token.
  2. Call downstream APIs through /bff/api/* — the cookie is sent automatically; the BFF attaches the Bearer.
  3. Send X-BFF-Csrf: 1 on every state-changing request (POST/PUT/PATCH/DELETE) to /bff/* and /bff/api/*.
  4. Treat 401 from /bff/api/* or /bff/me as "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 SETNX refresh lock is the concurrency protection until then.
  • Request bodies are never logged — login bodies contain passwords.
  • Set Keycloak.SkipTlsValidation only for staging's self-signed cert.

License

MIT

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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
Loading failed