Skip to content

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.

The server exposes 9 standard endpoints, registered automatically by AddGranitOpenIddict():

EndpointPathSpecificationPassthrough
Authorization/connect/authorizeRFC 6749 S3.1Yes
Token/connect/tokenRFC 6749 S3.2Yes
Pushed Authorization/connect/parRFC 9126No
UserInfo/connect/userinfoOIDC Core S5.3Yes
Introspection/connect/introspectRFC 7662No
Revocation/connect/revokeRFC 7009No
End Session/connect/logoutOIDC RP-Initiated LogoutYes
Device Authorization/connect/deviceRFC 8628No
End User Verification/connect/verifyRFC 8628 S3.3Yes

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.

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 code
Client --> /connect/token (code_verifier) --> Access token + Refresh token

For machine-to-machine (M2M) communication between services.

Client --> /connect/token (client_id + client_secret) --> Access token

Obtain new access tokens without re-authentication.

Client --> /connect/token (refresh_token) --> New access token + New refresh token

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 URI
User --> /connect/verify (enters code) --> Approval
Device --> /connect/token (device_code) --> Access token

Two custom grant types are registered for advanced authentication scenarios:

Grant typeUse case
urn:granit:grant_type:two_factorStateless TOTP 2FA challenge/verify flow
urn:granit:grant_type:passkeyWebAuthn/FIDO2 passkey authentication

These grants are processed by custom handlers in the host application and follow the standard OAuth 2.0 token endpoint pattern.

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.

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.

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.

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.

The following 7 standard OIDC scopes are seeded automatically on every startup (idempotent — no duplicates):

ScopeClaims includedSpec
openidsub (subject identifier)OIDC Core
profilename, given_name, family_name, preferred_usernameOIDC Core
emailemail, email_verifiedOIDC Core
phonephone_number, phone_number_verifiedOIDC Core
addressPostal addressOIDC Core
rolesrole (user roles)Custom
offline_accessEnables refresh token issuanceOAuth 2.0

Additional scopes can be registered via the admin API or declarative seeding.

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 typeDestinationCondition
subaccess_token + id_tokenAlways
name, given_name, family_nameaccess_token + id_tokenprofile scope granted
email, email_verifiedaccess_token + id_tokenemail scope granted
roleaccess_token + id_tokenroles scope granted
phone_numberaccess_token + id_tokenphone scope granted
security_stampExcludedAlways
impersonator_id, impersonator_nameaccess_token onlyAlways
All othersaccess_token onlyAlways

Override by registering a custom IClaimsDestinationProvider:

services.Replace(ServiceDescriptor.Scoped<IClaimsDestinationProvider, MyProvider>());

Apply destinations before issuing tokens:

principal.SetDestinations(claimsDestinationProvider);

By default, ephemeral keys are used. These are regenerated on every application restart, invalidating all previously issued tokens.

// Default — development only
options.AddEphemeralEncryptionKey();
options.AddEphemeralSigningKey();

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

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

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
ValidationOffline (signature check)DB lookup on every request
RevocationWait for expiryInstant
Token size~1 KB~40 bytes
PerformanceNo DB call+1 round-trip per request
Use caseMost applicationsStrict compliance

For high-traffic scenarios with reference tokens, consider using a Redis-backed token store to reduce database load.

Token lifetimes are not hardcoded. They are managed via Granit.Settings, configurable per-tenant at runtime without redeployment:

SettingDefaultKey
Access token1 hourOpenIddict.AccessTokenLifetime
Refresh token14 daysOpenIddict.RefreshTokenLifetime
Authorization code5 minutesOpenIddict.AuthCodeLifetime
Max login attempts5OpenIddict.MaxLoginAttempts
Idle session timeout0 (disabled)OpenIddict.IdleSessionTimeout

Override per-tenant via the Settings admin API at runtime.

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.

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.

The middleware order is critical and non-negotiable:

app.UseAuthentication(); // 1. Populates HttpContext.User
app.UseOpenIddict(); // 2. Handles /connect/* endpoints
app.UseAuthorization(); // 3. Enforces [Authorize] / RequireAuthorization()
  • Placing UseOpenIddict() before UseAuthentication() causes token validation to fail silently.
  • Placing it after UseAuthorization() causes OIDC endpoints to be blocked by authorization policies.

The server enables the following ASP.NET Core passthrough features:

  • EnableAuthorizationEndpointPassthrough() — custom consent screen logic
  • EnableTokenEndpointPassthrough() — custom token issuance logic
  • EnableUserInfoEndpointPassthrough() — custom claims mapping
  • EnableEndSessionEndpointPassthrough() — custom logout handling
  • EnableEndUserVerificationEndpointPassthrough() — device flow verification UI
  • EnableStatusCodePagesIntegration() — error pages for OIDC errors

The module registers an ActivitySource named Granit.OpenIddict with the following operation names:

OperationSpan name
Token issuanceopeniddict.token-issuance
Token revocationopeniddict.token-revocation
User authenticationopeniddict.user-authentication
User registrationopeniddict.user-registration
Two-factor challengeopeniddict.two-factor-challenge
Impersonationopeniddict.impersonation
Password resetopeniddict.password-reset
Account deletionopeniddict.account-deletion

Metrics are recorded via OpenIddictMetrics (meter: Granit.OpenIddict):

MetricTypeTags
granit.openiddict.tokens.issuedCountertenant_id, grant_type
granit.openiddict.tokens.revokedCountertenant_id, reason
granit.openiddict.authentication.failuresCountertenant_id, reason
granit.openiddict.registrationsCountertenant_id
granit.openiddict.token.issuance.durationHistogram (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.