Authagonal.Bff
0.25.2
dotnet add package Authagonal.Bff --version 0.25.2
NuGet\Install-Package Authagonal.Bff -Version 0.25.2
<PackageReference Include="Authagonal.Bff" Version="0.25.2" />
<PackageVersion Include="Authagonal.Bff" Version="0.25.2" />
<PackageReference Include="Authagonal.Bff" />
paket add Authagonal.Bff --version 0.25.2
#r "nuget: Authagonal.Bff, 0.25.2"
#:package Authagonal.Bff@0.25.2
#addin nuget:?package=Authagonal.Bff&version=0.25.2
#tool nuget:?package=Authagonal.Bff&version=0.25.2
Authagonal.Bff
Backend-for-Frontend (BFF) for SPAs that authenticate with Authagonal.
Your React/Vue/Angular app should never hold access or refresh tokens: anything in JS-reachable storage is exposed to XSS. This package is a confidential OIDC client you host on your own backend. It runs the authorization-code + PKCE flow server-side, keeps the tokens in a server-side session, and gives the browser nothing but an httpOnly session cookie. This is the pattern the IETF OAuth 2.0 for Browser-Based Apps BCP recommends.
Install
dotnet add package Authagonal.Bff
Wire it up
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthagonalBff(o =>
{
o.Authority = "https://acme-admin.authagonal.io"; // your tenant auth host
o.ClientId = builder.Configuration["Bff:ClientId"]!;
o.ClientSecret = builder.Configuration["Bff:ClientSecret"]!;
o.Scope = ["openid", "profile", "email", "offline_access"]; // offline_access enables refresh
o.PostLogoutRedirectUri = "https://app.acme.com/";
});
var app = builder.Build();
app.UseForwardedHeaders(); // required if you run behind a reverse proxy / ingress
app.MapAuthagonalBff();
app.MapFallbackToFile("index.html"); // your SPA
app.Run();
Register a BFF client in the Authagonal portal (confidential + PKCE + offline_access) with:
- redirect URI
https://app.acme.com/bff/callback - post-logout redirect URI
https://app.acme.com/
Endpoints (mounted under /bff by default)
| Route | Purpose |
|---|---|
GET /bff/login?returnUrl=/ |
Start login; redirects to Authagonal. |
GET /bff/callback |
OIDC redirect URI (handled for you). |
GET /bff/user |
{ isAuthenticated, claims, sessionExpiresAt }. Requires the anti-forgery header. |
GET\|POST /bff/logout |
Ends the session locally + at Authagonal. |
From the browser
Every non-navigation call must carry a static anti-forgery header (defends against CSRF alongside
SameSite=Lax):
const me = await fetch("/bff/user", { headers: { "X-Authagonal-Bff": "1" } }).then(r => r.json());
if (!me.isAuthenticated) window.location.href = "/bff/login?returnUrl=" + encodeURIComponent(location.pathname);
To log in / out, navigate (don't fetch): location.href = "/bff/login" / "/bff/logout".
Sessions & scaling
Sessions are stored via IDistributedCache. In-memory is the default; register a real distributed
cache (e.g. Redis) before AddAuthagonalBff when you run more than one instance:
builder.Services.AddStackExchangeRedisCache(o => o.Configuration = "...");
A shared cache is not sufficient on its own — you also need a cross-replica refresh lock. The refresh single-flight is otherwise process-local, while the session and its rotating refresh token live in the cache every replica shares. Two replicas can read the same session, both see it needs refreshing, and both redeem the same refresh token; that is indistinguishable from a stolen-token replay, and the IdP's answer to replay is to revoke the whole grant family — so a multi-instance BFF can sign a user out everywhere as a matter of routine.
Supply the lock by either route. Register an ILeaseProvider — any backend works, because all the
coordinator needs is "at most one holder for a short time", and the Azure, AWS and SQL providers each
ship one:
builder.Services.AddAuthagonalClustering(/* … */); // supplies ILeaseProvider
Or implement IBffRefreshLockStore on the session store, which is the shorter route when you have
Redis and no clustering. It is a conditional write with a TTL, and it puts the lock in the backend the
sessions already live in:
sealed class RedisBffSessionStore : IBffSessionStore, IBffRefreshLockStore
{
// …the IBffSessionStore members…
public async Task<bool> TryAcquireRefreshLockAsync(string sessionId, TimeSpan ttl, CancellationToken ct = default) =>
await _db.StringSetAsync($"agbff:lock:{sessionId}", "1", ttl, When.NotExists);
public Task ReleaseRefreshLockAsync(string sessionId, CancellationToken ct = default) =>
_db.KeyDeleteAsync($"agbff:lock:{sessionId}");
}
The default store cannot offer this itself: IDistributedCache has no set-if-absent, so there is no
conditional write to build a lock on. That is why this is a seam rather than something the library just
does. The TypeScript package's own session store carries the same two methods.
With neither supplied, a multi-instance deployment depends on the IdP's refresh-reuse grace window
(Auth:RefreshTokenReuseGraceSeconds) to absorb the double redemption — and in Authagonal's own server
host that defaults to 0, strict. A BFF whose session store looks shared and has no lock now says so
in a warning at startup, so this is not something you find out from a support ticket.
Extension points (the hosted seam)
Swap any of these to move the BFF onto other infrastructure:
IBffSessionStore— where sessions live (default:IDistributedCache).ICookieProtector— cookie payload encryption (default: ASP.NET Data Protection).ITokenClient— talking to Authagonal's token/revocation endpoints.
See docs/bff.md in the authagonal-cloud repo for the full protocol contract.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. 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
- Authagonal.Core (>= 0.25.2)
- Microsoft.IdentityModel.JsonWebTokens (>= 8.19.2)
- Microsoft.IdentityModel.Protocols.OpenIdConnect (>= 8.19.2)
-
net9.0
- Authagonal.Core (>= 0.25.2)
- Microsoft.IdentityModel.JsonWebTokens (>= 8.19.2)
- Microsoft.IdentityModel.Protocols.OpenIdConnect (>= 8.19.2)
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 |
|---|---|---|
| 0.25.2 | 43 | 8/12/2026 |
| 0.24.0 | 88 | 8/7/2026 |
| 0.23.0 | 84 | 8/5/2026 |
| 0.22.1 | 99 | 8/4/2026 |
| 0.22.0 | 100 | 8/3/2026 |
| 0.21.0 | 99 | 7/30/2026 |
| 0.20.0 | 90 | 7/27/2026 |
| 0.19.0 | 92 | 7/27/2026 |
| 0.18.0 | 90 | 7/27/2026 |
| 0.17.5 | 100 | 7/27/2026 |
| 0.17.4 | 96 | 7/27/2026 |
| 0.17.3 | 210 | 7/27/2026 |
| 0.17.2 | 94 | 7/27/2026 |
| 0.17.1 | 95 | 7/26/2026 |
| 0.17.0 | 93 | 7/26/2026 |
| 0.16.5 | 88 | 7/26/2026 |
| 0.16.4 | 99 | 7/26/2026 |
| 0.16.3 | 96 | 7/26/2026 |
| 0.16.2 | 95 | 7/25/2026 |
| 0.16.1 | 97 | 7/25/2026 |