Skip to content

Configuration Reference

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"
}
}

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.

PropertyTypeDefaultDescription
AuthorityUri(required)OIDC authority URL. Must expose /.well-known/openid-configuration.
ClientIdstring""Client ID for the BFF confidential client.
ClientSecretstring""Client secret. Use Vault in production.
Scopesstring[]["openid", "profile", "email", "roles", "offline_access"]Scopes requested during authorization.
SessionCookieNamestring"__Host-granit-bff"Session cookie name. Must start with __Host- for security.
SessionDurationTimeSpan08:00:00 (8 hours)Absolute session lifetime. Controls both cookie Max-Age and cache TTL.
RefreshGracePeriodTimeSpan00:01:00 (1 minute)Access token is refreshed this long before expiry.
PostLoginRedirectPathstring"/"Path to redirect to after successful login.
PostLogoutRedirectPathstring"/"Path to redirect to after logout.
UsePushedAuthorizationRequestsboolfalseUse 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.
UseDPoPboolfalseEnable 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.
ClientAuthenticationMethodBffClientAuthenticationMethodClientSecretPostClient 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.
ClientSigningKeyJwkstring?nullClient’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.
UseSessionSlidingExpirationbooltrueEnable 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).
SessionAbsoluteMaxDurationTimeSpan08:00:00 (8 hours)Absolute maximum session duration even with sliding expiration. After this, the user must re-authenticate regardless of activity.
RequireIssuerValidationbooltrueValidate 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.

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);
});

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" }
]
}
}
}
FieldRequiredDescription
ClusterIdYesTarget cluster (backend service)
Match.PathYesURL pattern. {**catch-all} captures all remaining segments.
Match.HostsNoRestrict route to specific host headers
Match.MethodsNoRestrict to specific HTTP methods
MetadataNoKey-value pairs. Use Granit.Bff.RequireAuth for BFF auth.
TransformsNoPath/header transformations before forwarding

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"
}
}
}
}
}
FieldRequiredDescription
Destinations.{id}.AddressYesBackend service URL
LoadBalancingPolicyNoFirstAlphabetical, RoundRobin, Random, LeastRequests, PowerOfTwoChoices
HttpClient.MaxConnectionsPerServerNoConnection pool limit (default: unlimited)
HealthCheck.Active.EnabledNoEnable active health probing
HealthCheck.Active.PathNoHealth check endpoint path
HealthCheck.Active.IntervalNoCheck interval
KeyValuesDefaultDescription
Granit.Bff.RequireAuth"true" / "false"Not set (= no auth)Enables token injection + CSRF validation
appsettings.Development.json
{
"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"
}
}

The BffMetrics class (meter name: Granit.Bff) exposes 6 counters. All counters include the tenant_id tag (coalesced to "global" when null).

Metric nameTypeTagsDescription
granit.bff.loginsCountertenant_idSuccessful BFF logins (callback completed)
granit.bff.logoutsCountertenant_idBFF logouts (session cleared)
granit.bff.token.refreshesCountertenant_idSilent token refreshes by the YARP proxy
granit.bff.proxy.requestsCountertenant_idTotal requests proxied through BFF
granit.bff.proxy.errorsCountertenant_id, reasonProxy errors (see reason values below)
granit.bff.csrf.rejectionsCountertenant_idRequests rejected due to invalid CSRF token

The granit.bff.proxy.errors counter includes a reason tag:

ReasonDescription
missing_sessionNo session cookie in the request
expired_sessionSession ID not found in Redis (expired or revoked)
refresh_failedToken refresh request to the IdP failed
# Login rate per minute
rate(granit_bff_logins_total[5m])
# CSRF rejection rate (security alert if this spikes)
rate(granit_bff_csrf_rejections_total[5m])
# Token refresh success rate
rate(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 reason
sum by (reason) (rate(granit_bff_proxy_errors_total[5m]))

The BffActivitySource (name: Granit.Bff) provides distributed tracing spans for all BFF operations. Registered automatically via GranitActivitySourceRegistry.

OperationTriggered byDescription
bff.loginGET /bff/loginOIDC authorization redirect
bff.callbackGET /bff/callbackCode exchange + session creation
bff.logoutGET /bff/logoutSession cleanup + IdP logout
bff.userGET /bff/userUser claims extraction
bff.token-refreshYARP token injectionSilent token refresh
bff.proxyYARP token injectionProxied API request

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 12ms
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
ServiceLifetimeImplementationPackage
IBffTokenStoreScopedDistributedCacheBffTokenStore (default) or EfCoreBffTokenStoreGranit.Bff / Granit.Bff.EntityFrameworkCore
IBffCsrfTokenGeneratorSingletonHmacBffCsrfTokenGeneratorGranit.Bff
BffMetricsSingletonBffMetricsGranit.Bff
BffTokenInjectionTransformScopedBffTokenInjectionTransformGranit.Bff.Yarp
BffCsrfValidationTransformScopedBffCsrfValidationTransformGranit.Bff.Yarp

All registrations use TryAdd* — you can replace any service with a custom implementation by registering your own before calling AddGranit<AppModule>().

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
},
});

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:";
});