Skip to content

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:

RiskDescription
XSS AttacksMalicious scripts can steal tokens from browser storage
Token ExposureTokens visible in browser developer tools and network requests
Limited Token LifetimeShort-lived tokens require frequent refresh flows in the browser
Refresh Token SecurityStoring 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 integration

Basic 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

SettingTypeDefaultDescription
SessionTimeoutInMinutesint60Session idle timeout in minutes
PasswordChangeUrlClaimTypestring"pwchangeurl"Claim type for password change URL

Authentication Settings

SettingTypeRequiredDescription
AuthoritystringYesProAuth server URL (e.g., https://auth.example.com)
ClientIdstringYesOAuth 2.0 client identifier
ClientSecretstringYesOAuth 2.0 client secret
TenantIdstringNoProAuth tenant identifier
AdditionalAcrValuesstringNoAdditional ACR values for the authentication request
UserScopesstringNoSpace-separated scopes for user authentication (default: openid profile offline_access)
UserResourcesstringNoSpace-separated resource identifiers
ServiceScopesstringNoScopes for service-to-service authentication
ServiceResourcesstringNoResources 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

SettingTypeDefaultDescription
RouteIdstringRequiredUnique identifier for the route
PathstringRequiredURL pattern to match (supports {**catch-all} for wildcards)
DestinationstringRequiredBackend API base URL
InjectTokenboolfalseWhether to inject the access token as a Bearer token
AllowAnonymousboolfalseAllow unauthenticated requests
AuthorizationPolicystringnullASP.NET Core authorization policy name
RemoveRequestPrefixboolfalseRemove the matched path prefix when forwarding
PreventRedirectToLoginboolfalseReturn 401 instead of redirecting to login
CustomHeadersdict{}Custom headers to add to proxied requests
ResponsePrefixModeenumNoneHow to handle response URL prefixes
ResponseRewriteLinkHeadersboolfalseRewrite Link headers in responses
ResponseRewriteXPaginationHeaderboolfalseRewrite X-Pagination headers
RewriteManifestWithCredentialsboolfalseRewrite 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-instance

Without 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:

EndpointMethodDescription
/signinGETInitiates OIDC login flow
/signoutGET/POSTSigns out the user
/signout-callback-oidcGETOIDC sign-out callback
/account/infoGETReturns 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

The BFF uses secure, HTTP-only cookies with the following defaults:

  • HttpOnly: Prevents JavaScript access
  • Secure: Only sent over HTTPS
  • SameSite=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

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_access scope is requested
  • Verify refresh tokens are enabled for your client in ProAuth

See Also