Skip to content

Authentication — Keycloak, EntraID & JWT

Every identity provider (Keycloak, Entra ID, Cognito) uses a different JWT structure — different claim names for roles, different group mappings, different logout protocols. Without an abstraction layer, switching providers means rewriting authentication code across your entire application. In regulated environments (healthcare, finance), this also means a new security audit every time.

Granit.Authentication normalizes all of this behind a single module interface: swap GranitAuthenticationJwtBearerKeycloakModule for GranitAuthenticationJwtBearerEntraIdModule and your endpoints, authorization policies, and audit logs continue to work unchanged. Back-channel logout is built in — when a session is revoked at the identity provider, your API rejects the token immediately instead of waiting for expiry.

  • DirectoryGranit.Authentication.JwtBearer/ Generic JWT Bearer, back-channel logout
    • Granit.Authentication.JwtBearer.Keycloak Keycloak claims transformation
    • Granit.Authentication.JwtBearer.EntraId Entra ID roles parsing
    • Granit.Authentication.JwtBearer.Cognito Cognito groups → roles
    • Granit.Authentication.JwtBearer.GoogleCloud Google Cloud Identity Platform claims
  • DirectoryGranit.Authentication.OpenIddict/ Built-in token validation (OpenIddict as IdP)
  • DirectoryGranit.Oidc/ OIDC client primitives (discovery, requests, DPoP, PKCE)
  • DirectoryGranit.Oidc.TokenManagement/ Token lifecycle (client credentials, caching, HttpClient)
  • DirectoryGranit.Authentication.DPoP/ DPoP proof validation (any IdP)
PackageRoleDepends on
Granit.Authentication.JwtBearerJWT Bearer middleware, back-channel logoutGranit.Users
Granit.Authentication.JwtBearer.KeycloakKeycloak claims transformationGranit.Authentication.JwtBearer
Granit.Authentication.JwtBearer.EntraIdEntra ID roles parsingGranit.Authentication.JwtBearer
Granit.Authentication.JwtBearer.CognitoCognito groups → rolesGranit.Authentication.JwtBearer
Granit.Authentication.JwtBearer.GoogleCloudGoogle Cloud Identity Platform claimsGranit.Authentication.JwtBearer
Granit.Authentication.OpenIddictBuilt-in token validation for OpenIddict IdPGranit.Users
Granit.OidcOIDC client primitives — discovery, typed requests, DPoP proof generation, PKCEGranit, Granit.Timing
Granit.Oidc.TokenManagementToken lifecycle — client credentials caching, DelegatingHandler, revocationGranit.Oidc
Granit.Authentication.DPoPIdP-agnostic DPoP proof validation (RFC 9449)Granit, Granit.Timing
[DependsOn(typeof(GranitAuthenticationJwtBearerKeycloakModule))]
public class AppModule : GranitModule { }
{
"Authentication": {
"Authority": "https://keycloak.example.com/realms/my-realm",
"Audience": "my-client"
},
"Keycloak": {
"ClientId": "my-client",
"AdminRole": "admin",
"RoleClaimsSource": "realm_access"
}
}

GranitAuthenticationJwtBearerModule registers:

  • ASP.NET Core JWT Bearer authentication
  • CurrentUserServiceICurrentUserService implementation extracting claims from HttpContext
  • IRevokedSessionStore — distributed cache-backed session revocation
{
"Authentication": {
"Authority": "https://idp.example.com/realms/my-realm",
"Audience": "my-client",
"RequireHttpsMetadata": true,
"NameClaimType": "sub",
"BackChannelLogout": {
"Enabled": true,
"EndpointPath": "/auth/back-channel-logout",
"SessionRevocationTtl": "01:00:00"
}
}
}
PropertyDefaultDescription
AuthorityOIDC issuer URL (required)
AudienceExpected aud claim (required)
RequireHttpsMetadatatrueEnforce HTTPS for metadata endpoint
NameClaimType"sub"Claim used as user identifier
BackChannelLogout.EnabledfalseEnable OIDC back-channel logout
BackChannelLogout.EndpointPath"/auth/back-channel-logout"Endpoint path
BackChannelLogout.SessionRevocationTtl"01:00:00"How long revoked sessions are remembered

Provider-agnostic implementation of the OIDC Back-Channel Logout specification. When the IdP sends a logout token, the session is revoked in distributed cache.

// In OnApplicationInitialization
app.MapGranitBackChannelLogout(); // POST /auth/back-channel-logout (anonymous)

The endpoint validates the logout token signature against the IdP’s JWKS, extracts the sid claim, and stores it in IDistributedCache with key granit:revoked-session:{sid}.

Subsequent requests with a revoked sid are rejected by the JWT Bearer events handler.

GranitAuthenticationJwtBearerKeycloakModule post-configures JWT Bearer with Keycloak-specific behavior:

  • Extracts roles from realm_access.roles or resource_access.{clientId}.roles
  • Maps them to standard ClaimTypes.Role claims
  • Registers an "Admin" authorization policy
// Keycloak JWT payload (simplified)
{
"realm_access": {
"roles": ["admin", "doctor"]
},
"resource_access": {
"my-client": {
"roles": ["manage-patients"]
}
}
}
// After transformation → ClaimTypes.Role: "admin", "doctor", "manage-patients"

GranitAuthenticationJwtBearerEntraIdModule post-configures JWT Bearer with Entra ID-specific behavior:

  • Extracts roles from the v1.0 roles claim and the v2.0 wids claim
  • Maps them to standard ClaimTypes.Role claims

GranitAuthenticationJwtBearerCognitoModule post-configures JWT Bearer with Cognito-specific behavior:

  • Extracts groups from the cognito:groups claim (multiple claims with same type)
  • Maps them to standard ClaimTypes.Role claims
// Cognito JWT payload — groups appear as repeated claims
// "cognito:groups": "admin"
// "cognito:groups": "doctors"
// After transformation → ClaimTypes.Role: "admin", "doctors"

When using DPoP (RFC 9449) for sender-constrained access tokens, the authorization server binds tokens to a cryptographic key. However, resource servers must also enforce that clients present valid DPoP proofs — otherwise an attacker could still use a stolen token with a plain Authorization: Bearer header.

Two packages provide DPoP enforcement, depending on your identity provider:

Granit.Authentication.DPoP provides IdP-agnostic DPoP proof validation. It works with any OIDC provider that issues cnf.jkt-bound access tokens (Keycloak 25+, Entra ID, Duende).

[DependsOn(
typeof(GranitAuthenticationJwtBearerKeycloakModule),
typeof(GranitAuthenticationDPoPModule))]
public class MyServiceModule : GranitModule { }
app.UseAuthentication();
app.UseGranitDPoPValidation(); // ← validates DPoP proofs
app.UseAuthorization();
{
"Authentication": {
"DPoP": {
"RequireDPoP": true,
"AllowedAlgorithms": ["ES256", "PS256"]
}
}
}

See DPoP Resource Server Validation for the full setup guide.

CategoryKey typesPackage
ModulesGranitAuthenticationJwtBearerModule, GranitAuthenticationJwtBearerKeycloakModule, GranitAuthenticationJwtBearerEntraIdModule, GranitAuthenticationJwtBearerCognitoModule, GranitAuthenticationJwtBearerGoogleCloudModule, GranitAuthenticationOpenIddictModule
AbstractionsCurrentUserService, IRevokedSessionStore, BackChannelLogoutTokenValidatorGranit.Authentication.JwtBearer
ClaimsKeycloakClaimsTransformationGranit.Authentication.JwtBearer.Keycloak
ClaimsEntraIdClaimsTransformationGranit.Authentication.JwtBearer.EntraId
ClaimsCognitoClaimsTransformationGranit.Authentication.JwtBearer.Cognito
ClaimsGoogleCloudClaimsTransformationGranit.Authentication.JwtBearer.GoogleCloud
OptionsJwtBearerAuthOptions, KeycloakOptions, CognitoOptions, GoogleCloudOptions
DPoPIDPoPProofValidator, JwkThumbprintCalculator, DPoPValidationMiddlewareGranit.Authentication.DPoP
ExtensionsAddGranitJwtBearer(), AddGranitKeycloak(), AddGranitCognito(), MapGranitBackChannelLogout(), UseGranitDPoPValidation()

For long-lived, non-interactive callers — cron jobs, partner integrations, internal CLIs, lab equipment — JWT bearer is the wrong primitive. Use Granit.Authentication.ApiKeys instead: opaque secrets hashed at rest with typed prefixes, CIDR allow-listing, rotation, and a daily scanner that emails administrators ahead of expiry. See the API Keys page for the full lifecycle, endpoints, permissions, configuration, and the four lifecycle notifications (apikeys.new_key_issued, apikeys.rotation_completed, apikeys.revoked, apikeys.expiring_soon).