Authio.AspNetCore.Bff
0.2.0
See the version list below for details.
dotnet add package Authio.AspNetCore.Bff --version 0.2.0
NuGet\Install-Package Authio.AspNetCore.Bff -Version 0.2.0
<PackageReference Include="Authio.AspNetCore.Bff" Version="0.2.0" />
<PackageVersion Include="Authio.AspNetCore.Bff" Version="0.2.0" />
<PackageReference Include="Authio.AspNetCore.Bff" />
paket add Authio.AspNetCore.Bff --version 0.2.0
#r "nuget: Authio.AspNetCore.Bff, 0.2.0"
#:package Authio.AspNetCore.Bff@0.2.0
#addin nuget:?package=Authio.AspNetCore.Bff&version=0.2.0
#tool nuget:?package=Authio.AspNetCore.Bff&version=0.2.0
Authio.AspNetCore.Bff
A Backend-for-Frontend for Authio on ASP.NET Core (.NET 8 and .NET 10). It lets a browser SPA (React, Angular, Vue, plain JavaScript...) sign in with Authio without ever holding a token, following the BFF pattern of the IETF draft OAuth 2.0 for Browser-Based Applications.
dotnet add package Authio.AspNetCore.Bff
Why a BFF
Tokens kept in the browser (local storage, session storage, memory) can be stolen by any script that runs on the page. Refresh tokens in the browser are worse: they outlive the page. With a BFF:
- the SPA and the BFF share an origin; the browser only holds an
HttpOnly,Secure,SameSite=Lax, host-only cookie (__Host-authio) that scripts cannot read; - the BFF is a confidential client: it runs the authorization code flow with PKCE, keeps the access, refresh and ID tokens server-side (the cookie only carries a random session key), refreshes them, and attaches the access token when it calls APIs on the user's behalf;
- every API call from the SPA must carry a custom header (
X-CSRF: 1), which a cross-site page cannot add without a CORS preflight, so the cookie cannot be abused by CSRF.
What it does
| Area | Behavior |
|---|---|
| Sign-in | Cookie + OpenID Connect schemes configured for Authio: code flow, PKCE S256, response_mode=query, iss of the authorization response validated (RFC 9207), code redeemed with client_secret_basic (or Post, or a public client), MapInboundClaims = false, no telemetry parameters, no PAR. |
| Authorize parameters | organization, resource (RFC 8707), prompt, login_hint, ui_locales, max_age, per login request or as defaults, plus any extra parameter. |
| Session | Server-side ticket store on IDistributedCache (in-memory by default, Redis for several instances), tickets encrypted with Data Protection. Sliding expiration (8 h by default). |
| Tokens | IAuthioBffTokenService refreshes the access token before it expires (Authio:TokenRefreshSkew), single-flight per session, persists the rotated refresh token, and ends the session on invalid_grant. Per-resource tokens via token exchange, cached per session. |
| Liveness | Authio has no back-channel logout. With SessionValidation = Introspection (default) the BFF introspects the session's refresh token every 5 minutes and signs the user out when Authio says it ended (logout elsewhere, revocation, suspension). |
| Endpoints | /bff/login, /bff/logout, /bff/user, /bff/silent-login. |
| Local APIs | .AsAuthioBffApiEndpoint() / [AuthioBffApi]: anti-forgery header required, 401/403 JSON instead of redirects. IHttpClientBuilder.AddAuthioBffUserToken() calls downstream APIs with the user's token. |
| Remote APIs | MapAuthioRemoteApi("/api/orders", "https://orders.example.com") forwards with YARP, attaching the user's token (or a machine token), stripping cookies and the anti-forgery header. |
Register the client at Authio
In the Authio console, create a client for the BFF:
- Type: confidential (it has a secret). Keep
client_secret_basic(the default) unless you setAuthio:ClientAuthenticationtoPost. - Redirect URI:
https://<your-app>/signin-oidc(Authio:Bff:CallbackPath). - Post-logout redirect URI:
https://<your-app>/signout-callback-oidc(Authio:Bff:SignedOutCallbackPath). - Scopes:
openid profile email offline_access(offline_accessgives the refresh token), plusrolesif you setRequestRoles, plus your API scopes. - Resources (optional): the API resource identifiers you will ask for with
resourceor use with remote APIs. - Allow the token exchange grant if a remote API needs a token for a specific resource or a narrower scope set.
Setup
{
"Authio": {
"Authority": "https://auth.example.com",
"Realm": "acme",
"ClientId": "acme-web",
"ClientSecret": "<secret>",
"Bff": {
"RequestRoles": true,
"SessionValidation": "Introspection",
"SessionValidationInterval": "00:05:00",
"SessionLifetime": "08:00:00"
}
}
}
using Authio.AspNetCore.Bff.Extensions;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthioBff(builder.Configuration.GetSection("Authio"));
var app = builder.Build();
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthioBff();
app.UseAuthorization();
app.MapAuthioBffEndpoints();
app.MapGet("/api/me", (HttpContext context) => new { name = context.User.FindFirst("name")?.Value })
.RequireAuthorization()
.AsAuthioBffApiEndpoint();
app.MapAuthioRemoteApi("/api/orders", "https://orders.example.com");
app.Run();
AddAuthioBff(Action<AuthioBffOptions>) is also available when the client is configured with
services.AddAuthioClient(...). Bound lists (Scopes, AllowedResources) are added to the defaults; call
options.Scopes.Clear() in code to replace them.
For more than one instance, register a shared IDistributedCache (e.g. AddStackExchangeRedisCache) and persist the
Data Protection keys (e.g. AddDataProtection().PersistKeysToStackExchangeRedis(...)) before AddAuthioBff.
Token refreshes are single-flight per process; keep a user on one instance (sticky sessions) or rely on Authio's short
refresh-token reuse leeway.
The SPA side
const api = (url, init = {}) =>
fetch(url, { ...init, credentials: "same-origin", headers: { ...init.headers, "X-CSRF": "1" } });
const response = await api("/bff/user");
if (response.status === 401) {
window.location.href = "/bff/login?returnUrl=" + encodeURIComponent(location.pathname);
} else {
const claims = await response.json();
const logoutUrl = claims.find(c => c.type === "bff:logout_url").value;
}
- Log in: navigate to
/bff/login?returnUrl=/somewhere(local URLs only). Optionalorganization,prompt(none,login,select_account,consent),login_hint,ui_locales,max_ageandresource(must be inAllowedResources). A failure comes back asreturnUrl?error=<code>(e.g.access_denied). - User:
GET /bff/userwithX-CSRF: 1returns[{ "type": "...", "value": ... }]: the session's claims, oneroleper Authio role and onepermission(resource:method) per grant (from the*_resource_accessclaims, which need therolesscope),bff:session_expires_in(seconds) andbff:logout_url. Anonymous:401, never a redirect. - Log out: navigate to the
bff:logout_urlvalue (/bff/logout?sid=...), optionally with&returnUrl=/. The BFF revokes the refresh token, deletes the session and sends the browser to Authio's end-session endpoint withid_token_hint,post_logout_redirect_uriandstate. Without the rightsid(or theX-CSRFheader), a signed-in user cannot be logged out by another site. - Silent login (optional): load
/bff/silent-loginin a hidden iframe; the page posts{ source: "authio-bff-silent-login", isLoggedIn, error }to the parent (same origin only). - APIs: always send
X-CSRF: 1. A401means the session is gone: send the user to log in again.
Calling APIs
app.MapGet("/api/report", async (HttpContext context, IAuthioBffTokenService tokens) =>
{
var token = await tokens.GetUserAccessTokenAsync(context);
return token is null ? Results.Unauthorized() : Results.Ok();
})
.RequireAuthorization()
.AsAuthioBffApiEndpoint();
builder.Services.AddHttpClient("billing", client => client.BaseAddress = new Uri("https://billing.example.com"))
.AddAuthioBffUserToken(options => options.Resource = "https://billing.example.com");
app.MapAuthioRemoteApi("/api/inventory", "https://inventory.example.com", options =>
{
options.TokenMode = AuthioBffRemoteApiTokenMode.ClientCredentials;
options.Scopes.Add("inventory:read");
});
Remote API token modes: User (default; anonymous calls get 401), UserOrNone, ClientCredentials,
UserOrClientCredentials, None. With Resource or Scopes a user token is obtained by token exchange (the result
carries act). When a remote API answers 401 to a user call, the BFF introspects the session and signs the user out
if it ended. Upstream Set-Cookie headers are dropped.
See docs/bff-api.md for the full
public API.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. 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
- Authio.Client (>= 0.2.0)
- Microsoft.AspNetCore.Authentication.OpenIdConnect (>= 10.0.10)
- Yarp.ReverseProxy (>= 2.3.0)
-
net8.0
- Authio.Client (>= 0.2.0)
- Microsoft.AspNetCore.Authentication.OpenIdConnect (>= 8.0.31)
- Yarp.ReverseProxy (>= 2.3.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.