OIDC Server Configuration
The OIDC server is configured in AddGranitOpenIddict() which
registers OpenIddict Core, Server, and Validation in a single call. This page covers
the supported flows, endpoints, token formats, and signing key management.
Endpoints
Section titled “Endpoints”The server exposes 9 standard endpoints, registered automatically by
AddGranitOpenIddict():
| Endpoint | Path | Specification | Passthrough |
|---|---|---|---|
| Authorization | /connect/authorize | RFC 6749 S3.1 | Yes |
| Token | /connect/token | RFC 6749 S3.2 | Yes |
| Pushed Authorization | /connect/par | RFC 9126 | No |
| UserInfo | /connect/userinfo | OIDC Core S5.3 | Yes |
| Introspection | /connect/introspect | RFC 7662 | No |
| Revocation | /connect/revoke | RFC 7009 | No |
| End Session | /connect/logout | OIDC RP-Initiated Logout | Yes |
| Device Authorization | /connect/device | RFC 8628 | No |
| End User Verification | /connect/verify | RFC 8628 S3.3 | Yes |
Discovery is automatically available at /.well-known/openid-configuration.
Passthrough endpoints forward the request to ASP.NET Core’s routing pipeline after OpenIddict processes it, allowing custom logic in your own controllers or handlers.
Authorization Code + PKCE (recommended)
Section titled “Authorization Code + PKCE (recommended)”The default flow for web and mobile applications. PKCE is required by default
(RequireProofKeyForCodeExchange()). This aligns with the OAuth 2.1 mandate and
is enforced by the OpenIddict.PkceRequired feature flag (default: true).
Client --> /connect/authorize (code_challenge, code_challenge_method=S256) --> User login --> Authorization codeClient --> /connect/token (code_verifier) --> Access token + Refresh tokenClient Credentials
Section titled “Client Credentials”For machine-to-machine (M2M) communication between services.
Client --> /connect/token (client_id + client_secret) --> Access tokenRefresh Token
Section titled “Refresh Token”Obtain new access tokens without re-authentication.
Client --> /connect/token (refresh_token) --> New access token + New refresh tokenDevice Authorization
Section titled “Device Authorization”For devices with limited input capability (smart TVs, CLI tools, IoT). Gated behind
the OpenIddict.DeviceFlow feature flag (default: false). Enable via Granit.Features
when needed.
Device --> /connect/device --> User code + Verification URIUser --> /connect/verify (enters code) --> ApprovalDevice --> /connect/token (device_code) --> Access tokenCustom grants
Section titled “Custom grants”Two custom grant types are registered for advanced authentication scenarios:
| Grant type | Use case |
|---|---|
urn:granit:grant_type:two_factor | Stateless TOTP 2FA challenge/verify flow |
urn:granit:grant_type:passkey | WebAuthn/FIDO2 passkey authentication |
These grants are processed by custom handlers in the host application and follow the standard OAuth 2.0 token endpoint pattern.
Pushed Authorization Requests (PAR)
Section titled “Pushed Authorization Requests (PAR)”The server supports Pushed Authorization Requests (RFC 9126)
— moving authorization parameters from the browser URL to a back-channel POST.
The /connect/par endpoint is always available. Set RequirePar: true to enforce
PAR for all authorization code flows. Required for FAPI 2.0.
DPoP (Proof-of-Possession)
Section titled “DPoP (Proof-of-Possession)”The server supports DPoP (RFC 9449)
— binding access tokens to a cryptographic key held by the client. Even if a token
leaks, it cannot be replayed without the private key. OpenIddict validates DPoP proofs
automatically. Set UseDPoP: true on a BFF frontend to enable. Required for FAPI 2.0.
FAPI 2.0 Security Profile
Section titled “FAPI 2.0 Security Profile”The server supports the FAPI 2.0 Security Profile — a comprehensive set of constraints for high-risk APIs (Open Banking, healthcare, government). Enable it with a single option:
{ "OpenIddict": { "EnableFapi2Profile": true }}When enabled, the server enforces:
- PAR required — all authorization code flows must use Pushed Authorization Requests
- Authorization code lifetime — reduced to 60 seconds (FAPI 2.0 SS5.3.2.1)
- PKCE with S256 — already enforced by default
The BFF and resource server must also be configured for DPoP, private_key_jwt, and
issuer verification. See the dedicated FAPI 2.0 page
for the full configuration guide.
Token Exchange (RFC 8693)
Section titled “Token Exchange (RFC 8693)”The server supports OAuth 2.0 Token Exchange (RFC 8693) for microservice delegation and impersonation flows. A service receiving a user’s access token can exchange it for a narrower, audience-restricted token scoped to a downstream service. Enable it with:
{ "OpenIddict": { "EnableTokenExchange": true }}When enabled, the urn:ietf:params:oauth:grant-type:token-exchange grant type is
registered on the /connect/token endpoint. Clients must have the gt:token_exchange
permission in their seeding configuration.
JWT-Secured Authorization Requests (JAR, RFC 9101)
Section titled “JWT-Secured Authorization Requests (JAR, RFC 9101)”The server supports JWT-Secured Authorization Requests
(RFC 9101), where authorization parameters
are encoded in a signed JWT request object instead of plain query parameters. This
provides integrity protection and non-repudiation for authorization requests, complementing
PAR’s confidentiality benefits.
JAR enforcement is controlled by the OpenIddict.JarRequired feature flag (default:
false). When enabled, the server rejects authorization requests that do not include a
signed request parameter.
Scopes
Section titled “Scopes”The following 7 standard OIDC scopes are seeded automatically on every startup (idempotent — no duplicates):
| Scope | Claims included | Spec |
|---|---|---|
openid | sub (subject identifier) | OIDC Core |
profile | name, given_name, family_name, preferred_username | OIDC Core |
email | email, email_verified | OIDC Core |
phone | phone_number, phone_number_verified | OIDC Core |
address | Postal address | OIDC Core |
roles | role (user roles) | Custom |
offline_access | Enables refresh token issuance | OAuth 2.0 |
Additional scopes can be registered via the admin API or declarative seeding.
Claims destination provider
Section titled “Claims destination provider”OpenIddict requires every claim on the ClaimsPrincipal to have a destination
(access_token, id_token, or both). Claims without destinations are silently excluded.
DefaultClaimsDestinationProvider implements scope-based routing:
| Claim type | Destination | Condition |
|---|---|---|
sub | access_token + id_token | Always |
name, given_name, family_name | access_token + id_token | profile scope granted |
email, email_verified | access_token + id_token | email scope granted |
role | access_token + id_token | roles scope granted |
phone_number | access_token + id_token | phone scope granted |
security_stamp | Excluded | Always |
impersonator_id, impersonator_name | access_token only | Always |
| All others | access_token only | Always |
Override by registering a custom IClaimsDestinationProvider:
services.Replace(ServiceDescriptor.Scoped<IClaimsDestinationProvider, MyProvider>());Apply destinations before issuing tokens:
principal.SetDestinations(claimsDestinationProvider);Signing and encryption
Section titled “Signing and encryption”Development (default)
Section titled “Development (default)”By default, ephemeral keys are used. These are regenerated on every application restart, invalidating all previously issued tokens.
// Default — development onlyoptions.AddEphemeralEncryptionKey();options.AddEphemeralSigningKey();Production
Section titled “Production”The host application must configure persistent signing and encryption keys after
calling AddGranitOpenIddict():
builder.Services.AddOpenIddict() .AddServer(options => { options.AddSigningCertificate( new X509Certificate2("signing.pfx", signingPassword)); options.AddEncryptionCertificate( new X509Certificate2("encryption.pfx", encryptionPassword)); });// Load certificates from Granit.Vault at startupbuilder.Services.AddOpenIddict() .AddServer(options => { X509Certificate2 signingCert = vaultService .GetCertificateAsync("oidc-signing").Result; X509Certificate2 encryptionCert = vaultService .GetCertificateAsync("oidc-encryption").Result; options.AddSigningCertificate(signingCert); options.AddEncryptionCertificate(encryptionCert); });builder.Services.AddOpenIddict() .AddServer(options => { options.AddSigningKey(new RsaSecurityKey(rsaParameters)); options.AddEncryptionKey(new RsaSecurityKey(encryptionRsaParameters)); });Access token encryption
Section titled “Access token encryption”Access token encryption is disabled by default (DisableAccessTokenEncryption())
for compatibility with standard JWT validation libraries. Enable it if your
resource servers can handle encrypted JWTs:
builder.Services.AddOpenIddict() .AddServer(options => { // Remove DisableAccessTokenEncryption() to enable encryption });Reference tokens
Section titled “Reference tokens”By default, the server issues self-contained JWTs. For regulated environments (banking, healthcare, SOC2 / ISO 27001 strict), enable opaque reference tokens:
{ "OpenIddict": { "UseReferenceTokens": true }}When enabled, both access tokens and refresh tokens are stored as opaque references:
| JWT (default) | Reference token | |
|---|---|---|
| Validation | Offline (signature check) | DB lookup on every request |
| Revocation | Wait for expiry | Instant |
| Token size | ~1 KB | ~40 bytes |
| Performance | No DB call | +1 round-trip per request |
| Use case | Most applications | Strict compliance |
For high-traffic scenarios with reference tokens, consider using a Redis-backed token store to reduce database load.
Token lifetimes
Section titled “Token lifetimes”Token lifetimes are not hardcoded. They are managed via Granit.Settings,
configurable per-tenant at runtime without redeployment:
| Setting | Default | Key |
|---|---|---|
| Access token | 1 hour | OpenIddict.AccessTokenLifetime |
| Refresh token | 14 days | OpenIddict.RefreshTokenLifetime |
| Authorization code | 5 minutes | OpenIddict.AuthCodeLifetime |
| Max login attempts | 5 | OpenIddict.MaxLoginAttempts |
| Idle session timeout | 0 (disabled) | OpenIddict.IdleSessionTimeout |
Override per-tenant via the Settings admin API at runtime.
Token validation
Section titled “Token validation”The validation component is configured with UseLocalServer() for co-located
resource servers (same process as the authorization server):
openIddict.AddValidation(options =>{ options.UseLocalServer(); options.UseAspNetCore();});For distributed resource servers, configure remote introspection or JWT validation with the issuer’s public keys via the discovery document.
Issuer configuration
Section titled “Issuer configuration”When running behind a load balancer or reverse proxy, set the issuer explicitly:
{ "OpenIddict": { "Issuer": "https://auth.example.com" }}When null (default), the issuer is inferred from the incoming request URL. This
works for single-node development but fails when the internal URL differs from the
public-facing URL.
Middleware order
Section titled “Middleware order”The middleware order is critical and non-negotiable:
app.UseAuthentication(); // 1. Populates HttpContext.Userapp.UseOpenIddict(); // 2. Handles /connect/* endpointsapp.UseAuthorization(); // 3. Enforces [Authorize] / RequireAuthorization()- Placing
UseOpenIddict()beforeUseAuthentication()causes token validation to fail silently. - Placing it after
UseAuthorization()causes OIDC endpoints to be blocked by authorization policies.
ASP.NET Core integration
Section titled “ASP.NET Core integration”The server enables the following ASP.NET Core passthrough features:
EnableAuthorizationEndpointPassthrough()— custom consent screen logicEnableTokenEndpointPassthrough()— custom token issuance logicEnableUserInfoEndpointPassthrough()— custom claims mappingEnableEndSessionEndpointPassthrough()— custom logout handlingEnableEndUserVerificationEndpointPassthrough()— device flow verification UIEnableStatusCodePagesIntegration()— error pages for OIDC errors
Observability
Section titled “Observability”ActivitySource
Section titled “ActivitySource”The module registers an ActivitySource named Granit.OpenIddict with the following
operation names:
| Operation | Span name |
|---|---|
| Token issuance | openiddict.token-issuance |
| Token revocation | openiddict.token-revocation |
| User authentication | openiddict.user-authentication |
| User registration | openiddict.user-registration |
| Two-factor challenge | openiddict.two-factor-challenge |
| Impersonation | openiddict.impersonation |
| Password reset | openiddict.password-reset |
| Account deletion | openiddict.account-deletion |
Metrics
Section titled “Metrics”Metrics are recorded via OpenIddictMetrics (meter: Granit.OpenIddict):
| Metric | Type | Tags |
|---|---|---|
granit.openiddict.tokens.issued | Counter | tenant_id, grant_type |
granit.openiddict.tokens.revoked | Counter | tenant_id, reason |
granit.openiddict.authentication.failures | Counter | tenant_id, reason |
granit.openiddict.registrations | Counter | tenant_id |
granit.openiddict.token.issuance.duration | Histogram (seconds) | tenant_id, grant_type |
All metrics follow the granit.{module}.{entity}.{action} naming convention and
include tenant_id (coalesced to "global" when null) via TagList.