Skip to content

Getting Started

This guide walks through setting up a complete BFF from scratch. By the end, you will have a working SPA that authenticates via an OIDC provider with all tokens stored server-side.

  • .NET 10 SDK
  • Redis instance (for session storage)
  • An OIDC-compliant identity provider (OpenIddict, Keycloak, Entra ID)
  • A SPA project (React, Vue, Angular, or any framework)
Terminal window
dotnet add package Granit.Bff
dotnet add package Granit.Bff.Endpoints
dotnet add package Granit.Bff.Yarp
dotnet add package Granit.Caching.StackExchangeRedis

The Granit.Caching.StackExchangeRedis package provides the IDistributedCache implementation backed by Redis. You can also use any other IDistributedCache provider (SQL Server, NCache, etc.), but Redis is recommended for production.

The BFF is a confidential client — it has a client_secret and uses the Authorization Code flow with PKCE. Register it in your identity provider.

Add the BFF application to the OpenIddict:Seeding:Applications configuration:

{
"OpenIddict": {
"Seeding": {
"Applications": [
{
"ClientId": "my-bff",
"ClientSecret": "bff-secret-from-vault",
"DisplayName": "My Application (BFF)",
"Permissions": [
"ept:authorization",
"ept:token",
"ept:logout",
"gt:authorization_code",
"gt:refresh_token",
"scp:openid",
"scp:profile",
"scp:email",
"scp:roles",
"scp:offline_access"
],
"RedirectUris": ["https://app.example.com/bff/callback"],
"PostLogoutRedirectUris": ["https://app.example.com"]
}
]
}
}
}
{
"Bff": {
"Authority": "https://auth.example.com",
"ClientId": "my-bff",
"ClientSecret": "bff-secret-from-vault",
"Scopes": ["openid", "profile", "email", "roles", "offline_access"],
"SessionCookieName": "__Host-granit-bff",
"SessionDuration": "08:00:00",
"RefreshGracePeriod": "00:01:00",
"PostLoginRedirectPath": "/",
"PostLogoutRedirectPath": "/"
},
"ReverseProxy": {
"Routes": {
"api": {
"ClusterId": "backend-api",
"Match": {
"Path": "/api/{**catch-all}"
},
"Metadata": {
"Granit.Bff.RequireAuth": "true"
}
}
},
"Clusters": {
"backend-api": {
"Destinations": {
"primary": {
"Address": "https://api.example.com"
}
}
}
}
},
"ConnectionStrings": {
"Redis": "localhost:6379"
}
}
using Granit.Bff;
using Granit.Bff.Endpoints;
using Granit.Bff.Yarp;
using Granit.Caching.StackExchangeRedis;
using Granit.Modularity;
[DependsOn(
typeof(GranitBffModule),
typeof(GranitBffEndpointsModule),
typeof(GranitBffYarpModule),
typeof(GranitCachingStackExchangeRedisModule))]
public sealed class AppModule : GranitModule;
var builder = WebApplication.CreateBuilder(args);
// Add Granit modules
builder.AddGranit<AppModule>();
// Add Redis distributed cache (used by IBffTokenStore)
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration.GetConnectionString("Redis");
options.InstanceName = "bff:";
});
// Add YARP with BFF transforms (token injection + CSRF validation)
builder.AddGranitBffYarp();
// Add HttpClient for token exchange with the IdP
builder.Services.AddHttpClient("Granit.Bff", client =>
{
client.Timeout = TimeSpan.FromSeconds(10);
});
var app = builder.Build();

The order of middleware matters. The BFF security headers must come before routing, and the YARP proxy must come after authentication.

// Security headers on login/callback endpoints (X-Frame-Options, CSP)
app.UseMiddleware<BffSecurityHeadersMiddleware>();
// Serve SPA static files
app.UseDefaultFiles();
app.UseStaticFiles();
// Standard ASP.NET Core middleware
app.UseRouting();
// Map BFF endpoints (/bff/login, /bff/callback, /bff/logout, /bff/user, /bff/csrf-token)
app.MapGranitBffEndpoints();
// Map YARP reverse proxy (with token injection)
app.UseGranitBffYarp();
// SPA fallback — serve index.html for all unmatched routes
app.MapFallbackToFile("index.html");
app.Run();
flowchart LR
    A[Request] --> B[SecurityHeaders]
    B --> C[StaticFiles]
    C --> D[Routing]
    D --> E{/bff/* ?}
    E -->|Yes| F[BFF Endpoints]
    E -->|No| G{YARP route?}
    G -->|Yes| H[YARP + Token Injection]
    G -->|No| I[SPA Fallback]

The SPA interacts with the BFF through standard HTTP calls. No OAuth library is needed on the frontend — the browser handles cookies automatically.

src/hooks/useAuth.ts
import { useCallback, useEffect, useState } from "react";
interface BffUser {
authenticated: boolean;
sub?: string;
name?: string;
email?: string;
roles?: string[];
tenantId?: string;
sessionExpiresAt?: string;
}
export function useAuth() {
const [user, setUser] = useState<BffUser | null>(null);
const [loading, setLoading] = useState(true);
const checkSession = useCallback(async () => {
try {
const response = await fetch("/bff/user", {
credentials: "include", // sends the session cookie
});
const data: BffUser = await response.json();
setUser(data.authenticated ? data : null);
} catch {
setUser(null);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
checkSession();
}, [checkSession]);
const login = useCallback(() => {
// Full-page redirect — BFF handles the OIDC flow
window.location.href = "/bff/login";
}, []);
const logout = useCallback(() => {
// Full-page redirect — BFF clears session + IdP logout
window.location.href = "/bff/logout";
}, []);
return { user, loading, login, logout, checkSession };
}

Start the application and verify each endpoint.

Terminal window
# Open in browser — should redirect to the IdP login page
curl -v https://localhost:5001/bff/login
# Expected: 302 redirect to {Authority}/connect/authorize?...
Terminal window
# Before login — should return { authenticated: false }
curl -s https://localhost:5001/bff/user | jq
# {
# "authenticated": false
# }
Terminal window
# After login — extract the session cookie and request a CSRF token
curl -s -X POST https://localhost:5001/bff/csrf-token \
-b "__Host-granit-bff=your-session-id" | jq
# {
# "csrfToken": "1711234567:a1b2c3d4..."
# }
Terminal window
# After login — API calls are proxied with Bearer token injection
curl -s https://localhost:5001/api/products \
-b "__Host-granit-bff=your-session-id" | jq
# Response from your backend API — no tokens visible

Open the SPA in a browser and perform the following sequence:

flowchart TD
    A[Open https://localhost:5001] --> B[Click 'Sign in']
    B --> C[Redirected to IdP login page]
    C --> D[Enter credentials]
    D --> E[Redirected back to SPA]
    E --> F[SPA shows user name + roles]
    F --> G[Make API calls — automatic cookie]
    G --> H[Click 'Sign out']
    H --> I[Redirected to IdP logout]
    I --> J[Redirected back to SPA]
    J --> K[SPA shows 'Sign in' button]

A typical BFF project looks like this:

  • DirectoryMyApp.Bff/
    • Program.cs Host configuration + middleware pipeline
    • AppModule.cs Granit module with [DependsOn]
    • appsettings.json BFF + YARP + Redis configuration
    • appsettings.Development.json Development overrides
    • Directorywwwroot/ SPA static files (React build output)
      • index.html SPA entry point
      • Directoryassets/ JS, CSS, images
    • MyApp.Bff.csproj
  • Architecture — understand the full flow diagrams
  • Security — harden cookies, enable cache encryption, configure CSRF
  • YARP Proxy — advanced routing, multi-service, WebSocket proxying
  • Configuration — all options, metrics, and tracing reference