Backend-For-Frontend (BFF)
Introduction
The Backend-For-Frontend (BFF) pattern is a security architecture that places an intermediary server between your Single Page Application (SPA) and your backend APIs. This pattern addresses fundamental security limitations of browser-based applications when handling OAuth 2.0 / OpenID Connect tokens.
Why Use a BFF?
Traditional SPAs store access tokens in browser storage (localStorage or sessionStorage), which exposes them to several attack vectors:
| Risk | Description |
|---|---|
| XSS Attacks | Malicious scripts can steal tokens from browser storage |
| Token Exposure | Tokens visible in browser developer tools and network requests |
| Limited Token Lifetime | Short-lived tokens require frequent refresh flows in the browser |
| Refresh Token Security | Storing refresh tokens in the browser is inherently insecure |
The BFF pattern solves these problems by:
- Keeping tokens server-side: Access and refresh tokens never reach the browser
- Using HTTP-only cookies: Session authentication via secure, HTTP-only cookies that JavaScript cannot access
- Server-side token injection: The BFF injects bearer tokens into API requests on behalf of the user
- Centralized token refresh: Automatic token refresh happens transparently on the server
Architecture
Getting Started
Prerequisites
- .NET 9.0 or later
- ASP.NET Core application
- A ProAuth tenant with a configured client application
Package Installation
Add the BFF package and your chosen token store provider:
# Core BFF package
dotnet add package ProAuth.Bff
# Choose ONE token store provider:
dotnet add package ProAuth.Oidc.Client.InMemory # Development/Testing
dotnet add package ProAuth.Oidc.Client.Redis # Production (distributed)
dotnet add package ProAuth.Oidc.Client.Dapr # Dapr-based deployments
dotnet add package ProAuth.Oidc.Client.ReaFx # ReaFx framework integrationBasic Setup
Create a new ASP.NET Core application and configure the BFF in Program.cs:
using ProAuth.Bff.Extensions;
using ProAuth.Oidc.Client.InMemory;
var builder = WebApplication.CreateBuilder(args);
// Add health checks
builder.Services.AddHealthChecks();
// Add distributed cache for session support
builder.Services.AddDistributedMemoryCache();
// Add ProAuth BFF services
builder.Services.AddBff(builder.Configuration)
.WithInProcessLocking()
.WithInMemoryTokenStore()
.WithInMemoryTicketStore();
var app = builder.Build();
// Configure middleware pipeline
app.UseForwardedHeaders();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseSession();
app.MapControllers();
app.MapHealthChecks("/health");
// Map BFF reverse proxy routes
app.MapReverseProxy();
// SPA fallback
app.MapFallbackToFile("index.html");
app.Run();Configuration
The BFF is configured through the ProAuthBff section in your application settings.
Configuration Structure
{
"ProAuthBff": {
"SessionTimeoutInMinutes": 60,
"PasswordChangeUrlClaimType": "pwchangeurl",
"ForwardedHeaders": {
"UseForwardedHeaders": true,
"ForwardLimit": 1,
"KnownProxies": "",
"KnownNetworks": ""
},
"Routes": [],
"AuthenticationSettings": {
"Authority": "https://auth.example.com",
"ClientId": "my-spa-client",
"ClientSecret": "client-secret",
"ClientAuthenticationMethod": "Auto",
"ClientAssertion": {
"SigningAlgorithm": "RS256",
"Certificate": "<base64-pfx>",
"CertificatePath": null,
"CertificatePassword": "<pfx-password>",
"KeyId": "my-client-key",
"LifetimeInSeconds": 60
},
"MutualTls": {
"Certificate": "<base64-pfx>",
"CertificatePath": null,
"CertificatePassword": "<pfx-password>"
},
"TenantId": "my-tenant",
"PushedAuthorizationBehavior": "UseIfAvailable",
"RequestObject": {
"Enabled": false,
"SigningAlgorithm": "RS256",
"Certificate": "<base64-pfx>",
"CertificatePassword": "<pfx-password>",
"KeyId": "my-request-object-key"
},
"UserScopes": "openid profile offline_access",
"UserResources": "api://my-api"
},
"TlsCertificateValidation": {
"AcceptAnyServerCertificates": false,
"CustomTrustedRootCaFilePaths": [],
"CustomTrustedTlsCertificatePaths": []
},
"DataProtectionSettings": {
"ApplicationName": "MyBffApp",
"Enabled": false
},
"BackChannelLogout": {
"Enabled": false
},
"FrontChannelLogout": {
"Enabled": true
}
}
}General Settings
| Setting | Type | Default | Description |
|---|---|---|---|
SessionTimeoutInMinutes | int | 60 | Session idle timeout in minutes |
PasswordChangeUrlClaimType | string | "pwchangeurl" | Claim type for password change URL |
Forwarded Headers
The BFF package configures ASP.NET Core ForwardedHeadersOptions from ProAuthBff:ForwardedHeaders. Host applications should still call app.UseForwardedHeaders() before authentication, authorization, session, antiforgery, and BFF endpoints.
| Setting | Type | Default | Description |
|---|---|---|---|
ForwardedHeaders:UseForwardedHeaders | bool | true | Configures forwarded header processing options for BFF host applications |
ForwardedHeaders:ForwardLimit | int | 1 | Maximum number of forwarded header entries processed from each header |
ForwardedHeaders:KnownProxies | string | "" | Comma-separated trusted proxy IP addresses |
ForwardedHeaders:KnownNetworks | string | "" | Comma-separated trusted proxy CIDR ranges |
Back-Channel Logout
The BFF can receive OpenID Connect Back-Channel Logout notifications on POST /bff/backchannel-logout. This is optional and disabled by default.
{
"ProAuthBff": {
"BackChannelLogout": {
"Enabled": true
}
}
}When enabled, configure the client application in ProAuth with backchannel_logout_uri set to the public BFF endpoint, for example https://app.example.com/bff/backchannel-logout, and set backchannel_logout_session_required to true for interactive browser clients.
The endpoint expects the standard form POST shape logout_token=<jwt>, validates the token against the ProAuth issuer metadata and signing keys, rejects tokens with a nonce, rejects replayed jti values until token expiry, and removes matching local BFF authentication sessions by sid when present or by sub for subject-only logout tokens. Use a shared ticket store and distributed cache provider for multi-instance deployments so every instance can find and remove sessions created by another instance and enforce replay protection across pods.
Front-Channel Logout
The BFF can also receive OpenID Connect Front-Channel Logout notifications on GET /bff/frontchannel-logout. This endpoint is enabled by default and can be disabled with:
{
"ProAuthBff": {
"FrontChannelLogout": {
"Enabled": false
}
}
}The endpoint validates the optional iss parameter against the configured authority, removes sessions matching sid, and signs out the current browser cookie when the current session matches the logout notification.
Authentication Settings
| Setting | Type | Required | Description |
|---|---|---|---|
Authority | string | Yes | ProAuth server URL (e.g., https://auth.example.com) |
ClientId | string | Yes | OAuth 2.0 client identifier |
ClientSecret | string | For secret-based methods | OAuth 2.0 client secret |
ClientAuthenticationMethod | string | No | Auto, None, ClientSecretPost, ClientSecretBasic, ClientSecretJwt, PrivateKeyJwt, TlsClientAuth, or SelfSignedTlsClientAuth. Auto uses ClientSecretBasic when a secret is configured and None otherwise. |
ClientAssertion.SigningAlgorithm | string | No | Signing algorithm for PrivateKeyJwt; defaults to RS256. |
ClientAssertion.Certificate | string | For PrivateKeyJwt unless CertificatePath is set | Base64 encoded PFX containing the assertion signing private key. |
ClientAssertion.CertificatePath | string | For PrivateKeyJwt unless Certificate is set | File path to a PFX containing the assertion signing private key. |
ClientAssertion.CertificatePassword | string | No | Password for the assertion signing PFX. |
ClientAssertion.KeyId | string | No | JWT header kid; should match a key in the client's ClientAssertionSigning key set. |
ClientAssertion.LifetimeInSeconds | int | No | JWT client assertion lifetime; defaults to 60 seconds. |
MutualTls.Certificate | string | For mTLS unless CertificatePath is set | Base64 encoded PFX presented to ProAuth for TlsClientAuth or SelfSignedTlsClientAuth. |
MutualTls.CertificatePath | string | For mTLS unless Certificate is set | File path to a PFX presented to ProAuth for mTLS. |
MutualTls.CertificatePassword | string | No | Password for the mTLS client certificate PFX. |
SecurityProfile | string | No | Set to Fapi2SecurityProfile to require the BFF-side FAPI 2.0 baseline. |
SenderConstrainedTokenMode | string | No | None, Dpop, or MutualTls. |
Dpop.Enabled | bool | No | Enables DPoP proof generation for authorization binding, token endpoint calls, and DPoP API token forwarding. |
Dpop.JsonWebKey | string | No | Private JWK used for DPoP signing. When omitted, the BFF can generate an ephemeral process-local key. |
TenantId | string | No | ProAuth tenant identifier |
AdditionalAcrValues | string | No | Additional ACR values for the authentication request |
PushedAuthorizationBehavior | string | No | PAR behavior: UseIfAvailable, Disable, or Require. Defaults to UseIfAvailable. |
RequestObject.Enabled | bool | No | Enables JWT-secured authorization requests (JAR) for browser authorization flows. |
RequestObject.SigningAlgorithm | string | No | Asymmetric request-object signing algorithm. Defaults to RS256. |
RequestObject.Certificate | string | When JAR is enabled | Base64 encoded PFX containing the request-object signing private key. |
RequestObject.CertificateInput.CertificatePath | string | When JAR is enabled unless Certificate is set | File path to the request-object signing PFX. |
RequestObject.CertificatePassword | string | No | Password for the PFX certificate. |
RequestObject.KeyId | string | No | JWT header kid; should match the key registered on the ProAuth client app. |
AuthorizationRequestClaims | string | No | Raw OIDC claims request parameter JSON added to authorization requests. |
UserScopes | string | No | Space-separated scopes for user authentication (default: openid profile offline_access) |
UserResources | string | No | Space-separated resource identifiers |
ServiceScopes | string | No | Scopes for service-to-service authentication |
ServiceResources | string | No | Resources for service-to-service authentication |
Route Configuration
Routes define how the BFF proxies requests to your backend APIs:
{
"ProAuthBff": {
"Routes": [
{
"RouteId": "api-route",
"Path": "/api/{**catch-all}",
"Destination": "https://api.example.com",
"InjectToken": true,
"AccessTokenForwardingMode": "Bearer",
"RemoveRequestPrefix": true,
"AuthorizationPolicy": "default"
},
{
"RouteId": "public-route",
"Path": "/public/{**catch-all}",
"Destination": "https://api.example.com/public",
"AllowAnonymous": true,
"InjectToken": false
}
]
}
}InjectToken=true remains a compatibility shortcut for bearer forwarding. New routes should prefer AccessTokenForwardingMode:
| Mode | Behavior |
|---|---|
None | No access token is sent downstream. |
Bearer | Sends Authorization: Bearer <access_token>. |
Dpop | Sends Authorization: DPoP <access_token> and adds a DPoP proof for the proxied request. |
MutualTls | Does not inject a bearer token; use when the downstream API is reached through mTLS-bound infrastructure. |
When SecurityProfile is Fapi2SecurityProfile, the BFF requires PAR, PKCE, secure client authentication, and either DPoP or mTLS sender-constrained tokens. For DPoP, configure routes that call protected APIs with AccessTokenForwardingMode: "Dpop" so downstream APIs receive both the sender-constrained token and proof.
Route Options
| Setting | Type | Default | Description |
|---|---|---|---|
RouteId | string | Required | Unique identifier for the route |
Path | string | Required | URL pattern to match (supports {**catch-all} for wildcards) |
Destination | string | Required | Backend API base URL |
InjectToken | bool | false | Whether to inject the access token as a Bearer token |
AllowAnonymous | bool | false | Allow unauthenticated requests |
AuthorizationPolicy | string | null | ASP.NET Core authorization policy name |
RemoveRequestPrefix | bool | false | Remove the matched path prefix when forwarding |
PreventRedirectToLogin | bool | false | Return 401 instead of redirecting to login |
CustomHeaders | dict | {} | Custom headers to add to proxied requests |
ResponsePrefixMode | enum | None | How to handle response URL prefixes |
ResponseRewriteLinkHeaders | bool | false | Rewrite Link headers in responses |
ResponseRewriteXPaginationHeader | bool | false | Rewrite X-Pagination headers |
RewriteManifestWithCredentials | bool | false | Rewrite manifest files with credentials |
TLS Certificate Validation
For development or private CA scenarios:
{
"ProAuthBff": {
"TlsCertificateValidation": {
"AcceptAnyServerCertificates": false,
"CustomTrustedRootCaFilePaths": [
"/etc/ssl/certs/custom-ca.crt"
],
"CustomTrustedTlsCertificatePaths": [
"/etc/ssl/certs/custom-cert.crt"
],
"EnableFileSystemWatcher": true
}
}
}WARNING
Never set AcceptAnyServerCertificates to true in production environments.
Data Protection Settings
For multi-instance deployments, configure data protection to share authentication cookies:
{
"ProAuthBff": {
"DataProtectionSettings": {
"ApplicationName": "MyBffApp",
"Enabled": true,
"EncryptionKeys": {
"Mode": "X509",
"Certificate": "BASE64_ENCODED_CERTIFICATE",
"CertificatePassword": "certificate-password",
"KeyRotationDecryptionCertificates": [
{
"Certificate": "BASE64_ENCODED_OLD_CERTIFICATE",
"CertificatePassword": "old-password"
}
]
}
}
}
}Token Store Providers
The BFF requires a token store to persist user tokens between requests. Choose the appropriate provider based on your deployment scenario.
In-Memory Token Store
Package: ProAuth.Oidc.Client.InMemory
Best for development, testing, and single-instance deployments.
builder.Services.AddBff(builder.Configuration)
.WithInProcessLocking()
.WithInMemoryTokenStore()
.WithInMemoryTicketStore();WARNING
In-memory storage does not survive application restarts and cannot be shared across multiple instances. Use only for development or single-instance scenarios.
Redis Token Store
Package: ProAuth.Oidc.Client.Redis
Recommended for production multi-instance deployments.
builder.Services.AddBff(builder.Configuration)
.WithRedisLocking()
.WithRedisTokenStore()
.WithRedisTicketStore();Configuration:
{
"Redis": {
"ConnectionString": "localhost:6379,password=secret"
}
}Dapr Token Store
Package: ProAuth.Oidc.Client.Dapr
For applications using Dapr for state management.
builder.Services.AddBff(builder.Configuration)
.WithDaprLocking()
.WithDaprTokenStore()
.WithDaprTicketStore();Requires Dapr state store component configuration.
ReaFx Token Store
Package: ProAuth.Oidc.Client.ReaFx
For applications built with the ReaFx framework.
builder.Services.AddBff(builder.Configuration)
.WithReaFxLocking()
.WithReaFxTokenStore()
.WithReaFxTicketStore();INFO
ReaFx integration is available separately for licensed ReaFx customers. Contact your account representative for more information.
Auth Session Token Store
Package: ProAuth.Oidc.Client.AspNetCore
Stores tokens directly in the ASP.NET Core authentication cookie.
builder.Services.AddBff(builder.Configuration)
.WithInProcessLocking()
.WithAuthSessionTokenStore();Recommended: Combine with Ticket Store
When using AuthSessionTokenStore, we strongly recommend also configuring a ticket store. This keeps the authentication cookie small by storing the bulk of the authentication ticket (including tokens) server-side, with only a session reference in the cookie.
builder.Services.AddBff(builder.Configuration)
.WithInProcessLocking()
.WithAuthSessionTokenStore()
.WithInMemoryTicketStore(); // Or Redis/Dapr for multi-instanceWithout a ticket store, the cookie can become very large (several KB) as it contains the full authentication ticket and tokens, which may cause issues with cookie size limits.
Authentication Endpoints
The BFF package automatically registers authentication controllers:
| Endpoint | Method | Description |
|---|---|---|
/signin | GET | Initiates OIDC login flow |
/signout | GET/POST | Signs out the user |
/signout-callback-oidc | GET | OIDC sign-out callback |
/account/info | GET | Returns current user information |
Frontend Integration
Your SPA can check authentication status and user info:
// Check if user is authenticated
const response = await fetch('/account/info', {
credentials: 'include'
});
if (response.ok) {
const userInfo = await response.json();
console.log('User:', userInfo);
} else if (response.status === 401) {
// Redirect to login
window.location.href = '/signin';
}Advanced Configuration
Custom Token Processing
You can add custom processing when tokens are validated:
builder.Services.AddBff(builder.Configuration,
configureAuthenticationOptions: options =>
{
options.CustomTokenProcessing = async (context, logger) =>
{
// Add custom claims or perform additional validation
var identity = (ClaimsIdentity)context.Principal.Identity;
identity.AddClaim(new Claim("custom_claim", "custom_value"));
logger.LogInformation("Token validated for user {User}",
context.Principal.Identity.Name);
};
});Programmatic Configuration
All configuration can also be done programmatically:
builder.Services.AddBff(
builder.Configuration,
configureBffOptions: options =>
{
options.SessionTimeoutInMinutes = 120;
options.Routes.Add(new BffRouteOptions
{
RouteId = "api",
Path = "/api/{**catch-all}",
Destination = "https://api.example.com",
InjectToken = true
});
},
configureAuthenticationOptions: options =>
{
options.Authority = "https://auth.example.com";
options.ClientId = "my-client";
options.ClientSecret = "secret";
});Security Considerations
Cookie Security
The BFF uses secure, HTTP-only cookies with the following defaults:
HttpOnly: Prevents JavaScript accessSecure: Only sent over HTTPSSameSite=None: Required for cross-origin scenarios (OIDC flows)
CSRF Protection
Anti-forgery protection is enabled by default. For state-changing operations from your SPA, include the XSRF token:
// Get XSRF token from cookie
const xsrfToken = document.cookie
.split('; ')
.find(row => row.startsWith('__Host-X-XSRF-TOKEN='))
?.split('=')[1];
// Include in requests
fetch('/api/data', {
method: 'POST',
headers: {
'X-XSRF-TOKEN': xsrfToken
},
credentials: 'include'
});Preventing Open Redirects
The BFF validates redirect URLs to prevent open redirect attacks. Only relative URLs and URLs to the configured authority are allowed.
Troubleshooting
Common Issues
401 Unauthorized on API calls
- Verify the route has
InjectToken: true - Check that the user is authenticated
- Verify the access token hasn't expired
Cookie too large errors
- Use a ticket store to reduce cookie size
- See Auth Session Token Store recommendations
CORS errors
- The BFF should be served from the same origin as your SPA
- Verify your reverse proxy or hosting configuration
Token refresh failures
- Ensure
offline_accessscope is requested - Verify refresh tokens are enabled for your client in ProAuth
See Also
- OIDC Client Library - Core OIDC operations
- Client Applications - Configure clients in ProAuth