Skip to content
Version v3.0.0

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.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

json
{
  "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

SettingTypeDefaultDescription
SessionTimeoutInMinutesint60Session idle timeout in minutes
PasswordChangeUrlClaimTypestring"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.

SettingTypeDefaultDescription
ForwardedHeaders:UseForwardedHeadersbooltrueConfigures forwarded header processing options for BFF host applications
ForwardedHeaders:ForwardLimitint1Maximum number of forwarded header entries processed from each header
ForwardedHeaders:KnownProxiesstring""Comma-separated trusted proxy IP addresses
ForwardedHeaders:KnownNetworksstring""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.

json
{
  "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:

json
{
  "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

SettingTypeRequiredDescription
AuthoritystringYesProAuth server URL (e.g., https://auth.example.com)
ClientIdstringYesOAuth 2.0 client identifier
ClientSecretstringFor secret-based methodsOAuth 2.0 client secret
ClientAuthenticationMethodstringNoAuto, None, ClientSecretPost, ClientSecretBasic, ClientSecretJwt, PrivateKeyJwt, TlsClientAuth, or SelfSignedTlsClientAuth. Auto uses ClientSecretBasic when a secret is configured and None otherwise.
ClientAssertion.SigningAlgorithmstringNoSigning algorithm for PrivateKeyJwt; defaults to RS256.
ClientAssertion.CertificatestringFor PrivateKeyJwt unless CertificatePath is setBase64 encoded PFX containing the assertion signing private key.
ClientAssertion.CertificatePathstringFor PrivateKeyJwt unless Certificate is setFile path to a PFX containing the assertion signing private key.
ClientAssertion.CertificatePasswordstringNoPassword for the assertion signing PFX.
ClientAssertion.KeyIdstringNoJWT header kid; should match a key in the client's ClientAssertionSigning key set.
ClientAssertion.LifetimeInSecondsintNoJWT client assertion lifetime; defaults to 60 seconds.
MutualTls.CertificatestringFor mTLS unless CertificatePath is setBase64 encoded PFX presented to ProAuth for TlsClientAuth or SelfSignedTlsClientAuth.
MutualTls.CertificatePathstringFor mTLS unless Certificate is setFile path to a PFX presented to ProAuth for mTLS.
MutualTls.CertificatePasswordstringNoPassword for the mTLS client certificate PFX.
SecurityProfilestringNoSet to Fapi2SecurityProfile to require the BFF-side FAPI 2.0 baseline.
SenderConstrainedTokenModestringNoNone, Dpop, or MutualTls.
Dpop.EnabledboolNoEnables DPoP proof generation for authorization binding, token endpoint calls, and DPoP API token forwarding.
Dpop.JsonWebKeystringNoPrivate JWK used for DPoP signing. When omitted, the BFF can generate an ephemeral process-local key.
TenantIdstringNoProAuth tenant identifier
AdditionalAcrValuesstringNoAdditional ACR values for the authentication request
PushedAuthorizationBehaviorstringNoPAR behavior: UseIfAvailable, Disable, or Require. Defaults to UseIfAvailable.
RequestObject.EnabledboolNoEnables JWT-secured authorization requests (JAR) for browser authorization flows.
RequestObject.SigningAlgorithmstringNoAsymmetric request-object signing algorithm. Defaults to RS256.
RequestObject.CertificatestringWhen JAR is enabledBase64 encoded PFX containing the request-object signing private key.
RequestObject.CertificateInput.CertificatePathstringWhen JAR is enabled unless Certificate is setFile path to the request-object signing PFX.
RequestObject.CertificatePasswordstringNoPassword for the PFX certificate.
RequestObject.KeyIdstringNoJWT header kid; should match the key registered on the ProAuth client app.
AuthorizationRequestClaimsstringNoRaw OIDC claims request parameter JSON added to authorization requests.
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,
        "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:

ModeBehavior
NoneNo access token is sent downstream.
BearerSends Authorization: Bearer <access_token>.
DpopSends Authorization: DPoP <access_token> and adds a DPoP proof for the proxied request.
MutualTlsDoes 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

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.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.

csharp
builder.Services.AddBff(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.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.

csharp
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.

csharp
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.

csharp
builder.Services.AddBff(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.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:

csharp
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

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