Configuration Reference
Full appsettings.json example
Section titled “Full appsettings.json example”This is a complete configuration example covering all BFF, YARP, and Redis options:
{ "Bff": { "Authority": "https://auth.example.com", "SessionDuration": "08:00:00", "RefreshGracePeriod": "00:01:00", "UseSessionSlidingExpiration": true, "SessionAbsoluteMaxDuration": "08:00:00",
"Frontends": [ { "Name": "admin", "ClientId": "my-bff", "ClientSecret": "bff-secret-from-vault", "Scopes": ["openid", "profile", "email", "roles", "offline_access"], "UsePushedAuthorizationRequests": false, "UseDPoP": false } ] },
"ReverseProxy": { "Routes": { "api-authenticated": { "ClusterId": "backend-api", "Match": { "Path": "/api/{**catch-all}" }, "Metadata": { "Granit.Bff.RequireAuth": "true" } }, "public-api": { "ClusterId": "backend-api", "Match": { "Path": "/public/{**catch-all}" } }, "health": { "ClusterId": "backend-api", "Match": { "Path": "/health" } } }, "Clusters": { "backend-api": { "Destinations": { "primary": { "Address": "https://api.example.com" } } } } },
"ConnectionStrings": { "Redis": "redis.example.com:6380,ssl=true,password=secret,abortConnect=false" }}GranitBffOptions
Section titled “GranitBffOptions”Bind from the Bff configuration section. Section name constant: GranitBffOptions.SectionName.
When using multi-frontend (Frontends[] array), the properties below apply to each BffFrontendOptions entry.
| Property | Type | Default | Description |
|---|---|---|---|
Authority | Uri | (required) | OIDC authority URL. Must expose /.well-known/openid-configuration. |
ClientId | string | "" | Client ID for the BFF confidential client. |
ClientSecret | string | "" | Client secret. Use Vault in production. |
Scopes | string[] | ["openid", "profile", "email", "roles", "offline_access"] | Scopes requested during authorization. |
SessionCookieName | string | "__Host-granit-bff" | Session cookie name. Must start with __Host- for security. |
SessionDuration | TimeSpan | 08:00:00 (8 hours) | Absolute session lifetime. Controls both cookie Max-Age and cache TTL. |
RefreshGracePeriod | TimeSpan | 00:01:00 (1 minute) | Access token is refreshed this long before expiry. |
PostLoginRedirectPath | string | "/" | Path to redirect to after successful login. |
PostLogoutRedirectPath | string | "/" | Path to redirect to after logout. |
UsePushedAuthorizationRequests | bool | false | Use Pushed Authorization Requests (RFC 9126). When true, the BFF posts authorization parameters to /connect/par and redirects with only the request_uri. Requires the OIDC server to support PAR. |
UseDPoP | bool | false | Enable DPoP (RFC 9449) token binding. When true, the BFF generates an EC P-256 key pair per session and attaches DPoP proofs to token requests and proxied API calls. Prevents token replay attacks. |
ClientAuthenticationMethod | BffClientAuthenticationMethod | ClientSecretPost | Client authentication method for token endpoint requests. Set to PrivateKeyJwt for RFC 7523 signed JWT assertions instead of shared secrets. Required for FAPI 2.0 when not using mTLS. |
ClientSigningKeyJwk | string? | null | Client’s private signing key as a JWK JSON string. Required when ClientAuthenticationMethod is PrivateKeyJwt. Supports EC (ES256) and RSA (PS256) keys. Must include private key parameters. Store in Vault in production. |
UseSessionSlidingExpiration | bool | true | Enable sliding session expiration. When true, each proxied request past the session’s halfway point extends the session TTL. Bounded by SessionAbsoluteMaxDuration to prevent indefinite sessions (ISO 27001 A.9.4.2). |
SessionAbsoluteMaxDuration | TimeSpan | 08:00:00 (8 hours) | Absolute maximum session duration even with sliding expiration. After this, the user must re-authenticate regardless of activity. |
RequireIssuerValidation | bool | true | Validate the iss parameter in authorization responses (RFC 9207). Prevents IdP mix-up attacks by verifying that the authorization response comes from the expected issuer. Required for FAPI 2.0. Enabled by default — set to false only for legacy identity providers that do not include the iss parameter. |
Configuration binding
Section titled “Configuration binding”GranitBffOptions is bound automatically by AddGranitBffYarp():
builder.Services.Configure<GranitBffOptions>( builder.Configuration.GetSection(GranitBffOptions.SectionName));You can also configure it manually:
builder.Services.Configure<GranitBffOptions>(options =>{ options.Authority = new Uri("https://auth.example.com"); options.ClientId = "my-bff"; options.SessionDuration = TimeSpan.FromHours(4);});YARP configuration
Section titled “YARP configuration”Routes
Section titled “Routes”Each route maps a URL pattern to a backend cluster:
{ "Routes": { "{route-id}": { "ClusterId": "{cluster-id}", "Match": { "Path": "/api/{**catch-all}", "Hosts": ["app.example.com"], "Methods": ["GET", "POST"] }, "Metadata": { "Granit.Bff.RequireAuth": "true" }, "Transforms": [ { "PathRemovePrefix": "/api/v2" }, { "PathPrefix": "/api" } ] } }}| Field | Required | Description |
|---|---|---|
ClusterId | Yes | Target cluster (backend service) |
Match.Path | Yes | URL pattern. {**catch-all} captures all remaining segments. |
Match.Hosts | No | Restrict route to specific host headers |
Match.Methods | No | Restrict to specific HTTP methods |
Metadata | No | Key-value pairs. Use Granit.Bff.RequireAuth for BFF auth. |
Transforms | No | Path/header transformations before forwarding |
Clusters
Section titled “Clusters”Each cluster defines one or more backend destinations:
{ "Clusters": { "{cluster-id}": { "LoadBalancingPolicy": "RoundRobin", "Destinations": { "{destination-id}": { "Address": "https://api.example.com" } }, "HttpClient": { "MaxConnectionsPerServer": 100, "RequestHeaderEncoding": "utf-8" }, "HealthCheck": { "Active": { "Enabled": true, "Interval": "00:00:30", "Timeout": "00:00:10", "Path": "/health" } } } }}| Field | Required | Description |
|---|---|---|
Destinations.{id}.Address | Yes | Backend service URL |
LoadBalancingPolicy | No | FirstAlphabetical, RoundRobin, Random, LeastRequests, PowerOfTwoChoices |
HttpClient.MaxConnectionsPerServer | No | Connection pool limit (default: unlimited) |
HealthCheck.Active.Enabled | No | Enable active health probing |
HealthCheck.Active.Path | No | Health check endpoint path |
HealthCheck.Active.Interval | No | Check interval |
BFF-specific metadata keys
Section titled “BFF-specific metadata keys”| Key | Values | Default | Description |
|---|---|---|---|
Granit.Bff.RequireAuth | "true" / "false" | Not set (= no auth) | Enables token injection + CSRF validation |
Environment-specific overrides
Section titled “Environment-specific overrides”{ "Bff": { "Authority": "https://localhost:5002", "ClientId": "my-bff-dev", "ClientSecret": "dev-secret", "SessionDuration": "24:00:00" }, "ReverseProxy": { "Clusters": { "backend-api": { "Destinations": { "primary": { "Address": "https://localhost:5001" } } } } }, "ConnectionStrings": { "Redis": "localhost:6379" }}{ "Bff": { "Authority": "https://auth.staging.example.com", "ClientId": "my-bff-staging", "SessionDuration": "08:00:00" }, "ReverseProxy": { "Clusters": { "backend-api": { "Destinations": { "primary": { "Address": "https://api.staging.example.com" } } } } }, "ConnectionStrings": { "Redis": "redis.staging.internal:6380,ssl=true" }}{ "Bff": { "Authority": "https://auth.example.com", "ClientId": "my-bff", "SessionDuration": "08:00:00", "RefreshGracePeriod": "00:02:00" }, "ReverseProxy": { "Clusters": { "backend-api": { "LoadBalancingPolicy": "RoundRobin", "Destinations": { "instance-1": { "Address": "https://api-1.internal:5001" }, "instance-2": { "Address": "https://api-2.internal:5001" } }, "HealthCheck": { "Active": { "Enabled": true, "Interval": "00:00:30", "Path": "/health" } } } } }, "ConnectionStrings": { "Redis": "redis.prod.internal:6380,ssl=true" }}Metrics reference
Section titled “Metrics reference”The BffMetrics class (meter name: Granit.Bff) exposes 6 counters.
All counters include the tenant_id tag (coalesced to "global" when null).
| Metric name | Type | Tags | Description |
|---|---|---|---|
granit.bff.logins | Counter | tenant_id | Successful BFF logins (callback completed) |
granit.bff.logouts | Counter | tenant_id | BFF logouts (session cleared) |
granit.bff.token.refreshes | Counter | tenant_id | Silent token refreshes by the YARP proxy |
granit.bff.proxy.requests | Counter | tenant_id | Total requests proxied through BFF |
granit.bff.proxy.errors | Counter | tenant_id, reason | Proxy errors (see reason values below) |
granit.bff.csrf.rejections | Counter | tenant_id | Requests rejected due to invalid CSRF token |
Proxy error reasons
Section titled “Proxy error reasons”The granit.bff.proxy.errors counter includes a reason tag:
| Reason | Description |
|---|---|
missing_session | No session cookie in the request |
expired_session | Session ID not found in Redis (expired or revoked) |
refresh_failed | Token refresh request to the IdP failed |
Grafana dashboard query examples
Section titled “Grafana dashboard query examples”# Login rate per minuterate(granit_bff_logins_total[5m])
# CSRF rejection rate (security alert if this spikes)rate(granit_bff_csrf_rejections_total[5m])
# Token refresh success raterate(granit_bff_token_refreshes_total[5m]) / (rate(granit_bff_token_refreshes_total[5m]) + rate(granit_bff_proxy_errors_total{reason="refresh_failed"}[5m]))
# Proxy error breakdown by reasonsum by (reason) (rate(granit_bff_proxy_errors_total[5m]))ActivitySource operations
Section titled “ActivitySource operations”The BffActivitySource (name: Granit.Bff) provides distributed tracing spans
for all BFF operations. Registered automatically via GranitActivitySourceRegistry.
| Operation | Triggered by | Description |
|---|---|---|
bff.login | GET /bff/login | OIDC authorization redirect |
bff.callback | GET /bff/callback | Code exchange + session creation |
bff.logout | GET /bff/logout | Session cleanup + IdP logout |
bff.user | GET /bff/user | User claims extraction |
bff.token-refresh | YARP token injection | Silent token refresh |
bff.proxy | YARP token injection | Proxied API request |
Trace example
Section titled “Trace example”A typical authenticated API call produces this trace:
[bff.proxy] 2.3ms |-- Redis GET bff:session:abc123 0.4ms |-- [bff.token-refresh] 145ms (only if token was refreshed) | |-- HTTP POST /connect/token 142ms | |-- Redis SET bff:session:abc123 0.3ms |-- HTTP GET https://api.internal/products 12msModule dependencies
Section titled “Module dependencies”graph TD
BFF["GranitBffModule"]
SEC[""]
TIME["GranitTimingModule"]
BFFE["GranitBffEndpointsModule"]
DOC["GranitHttpApiDocumentationModule"]
BFFY["GranitBffYarpModule"]
BFF --> SEC
BFF --> TIME
BFFE --> BFF
BFFE --> DOC
BFFY --> BFF
style BFF fill:#4a9eff,color:#fff
style BFFE fill:#4a9eff,color:#fff
style BFFY fill:#4a9eff,color:#fff
DI registrations
Section titled “DI registrations”| Service | Lifetime | Implementation | Package |
|---|---|---|---|
IBffTokenStore | Scoped | DistributedCacheBffTokenStore (default) or EfCoreBffTokenStore | Granit.Bff / Granit.Bff.EntityFrameworkCore |
IBffCsrfTokenGenerator | Singleton | HmacBffCsrfTokenGenerator | Granit.Bff |
BffMetrics | Singleton | BffMetrics | Granit.Bff |
BffTokenInjectionTransform | Scoped | BffTokenInjectionTransform | Granit.Bff.Yarp |
BffCsrfValidationTransform | Scoped | BffCsrfValidationTransform | Granit.Bff.Yarp |
All registrations use TryAdd* — you can replace any service with a custom
implementation by registering your own before calling AddGranit<AppModule>().
HttpClient configuration
Section titled “HttpClient configuration”The BFF uses a named HttpClient ("Granit.Bff") for token exchange and refresh
calls to the identity provider. Configure it to your needs:
builder.Services.AddHttpClient("Granit.Bff", client =>{ client.Timeout = TimeSpan.FromSeconds(10); client.DefaultRequestHeaders.Add("Accept", "application/json");}).ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler{ // Connection pooling MaxConnectionsPerServer = 10, PooledConnectionLifetime = TimeSpan.FromMinutes(5),
// TLS SslOptions = new System.Net.Security.SslClientAuthenticationOptions { // Custom certificate validation if needed },});Distributed cache configuration
Section titled “Distributed cache configuration”The IBffTokenStore uses IDistributedCache. Any provider works, but Redis
is recommended for production:
builder.Services.AddStackExchangeRedisCache(options =>{ options.Configuration = builder.Configuration.GetConnectionString("Redis"); options.InstanceName = "bff:";});builder.Services.AddDistributedMemoryCache();builder.Services.AddDistributedSqlServerCache(options =>{ options.ConnectionString = builder.Configuration.GetConnectionString("SqlServer"); options.SchemaName = "bff"; options.TableName = "Sessions";});See also
Section titled “See also”- Overview — why BFF and what problems it solves
- Architecture — component diagram and flow sequences
- Getting Started — step-by-step setup
- Security — cookie, CSRF, and session hardening
- YARP Proxy — routing and transform pipeline
- Authentication — JWT Bearer authentication (API-to-API)
- OpenIddict — self-hosted identity provider