Appearance
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:
bash
# 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:
csharp
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.AddProAuthBff(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
json
{
"ProAuthBff": {
"SessionTimeoutInMinutes": 60,
"PasswordChangeUrlClaimType": "pwchangeurl",
"Routes": [],
"AuthenticationSettings": {
"Authority": "https://auth.example.com",
"ClientId": "my-spa-client",
"ClientSecret": "client-secret",
"TenantId": "my-tenant",
"UserScopes": "openid profile offline_access",
"UserResources": "api://my-api"
},
"TlsCertificateValidation": {
"AcceptAnyServerCertificates": false,
"CustomTrustedRootCaFilePaths": [],
"CustomTrustedTlsCertificatePaths": []
},
"DataProtectionSettings": {
"ApplicationName": "MyBffApp",
"Enabled": false
}
}
}General Settings
| Setting | Type | Default | Description |
|---|---|---|---|
SessionTimeoutInMinutes | int | 60 | Session idle timeout in minutes |
PasswordChangeUrlClaimType | string | "pwchangeurl" | Claim type for password change URL |
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 | Yes | OAuth 2.0 client secret |
TenantId | string | No | ProAuth tenant identifier |
AdditionalAcrValues | string | No | Additional ACR values for the authentication request |
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:
json
{
"ProAuthBff": {
"Routes": [
{
"RouteId": "api-route",
"Path": "/api/{**catch-all}",
"Destination": "https://api.example.com",
"InjectToken": true,
"RemoveRequestPrefix": true,
"AuthorizationPolicy": "default"
},
{
"RouteId": "public-route",
"Path": "/public/{**catch-all}",
"Destination": "https://api.example.com/public",
"AllowAnonymous": true,
"InjectToken": false
}
]
}
}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:
json
{
"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:
json
{
"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.
csharp
builder.Services.AddProAuthBff(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.
csharp
builder.Services.AddProAuthBff(builder.Configuration)
.WithRedisLocking()
.WithRedisTokenStore()
.WithRedisTicketStore();Configuration:
json
{
"Redis": {
"ConnectionString": "localhost:6379,password=secret"
}
}Dapr Token Store
Package: ProAuth.Oidc.Client.Dapr
For applications using Dapr for state management.
csharp
builder.Services.AddProAuthBff(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.
csharp
builder.Services.AddProAuthBff(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.
csharp
builder.Services.AddProAuthBff(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.
csharp
builder.Services.AddProAuthBff(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:
typescript
// 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:
csharp
builder.Services.AddProAuthBff(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:
csharp
builder.Services.AddProAuthBff(
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:
typescript
// 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