Skip to content

OpenIddict — Self-hosted Identity Provider

Granit.OpenIddict transforms any Granit application into a self-hosted OpenID Connect authorization server with full user management. Built on OpenIddict 7, ASP.NET Core Identity, and EF Core 10.

Designed for the Granit module system with multi-tenant isolation, per-tenant configuration via Granit.Settings, and Wolverine-based background jobs.

Granit.Identity delegates authentication to external providers (Keycloak, Cognito, Entra ID). Granit.OpenIddict is the alternative: your application IS the identity provider.

External (Keycloak, etc.)Self-hosted (OpenIddict)
User storeExternal systemYour database
User managementExternal admin UIYour API (/api/admin/users)
CustomizationLimited by providerFull control
DeploymentSeparate serviceSame process
Multi-tenancyProvider-dependentBuilt-in (IMultiTenant)
Data residencyProvider’s infraYour infrastructure
ComplianceDelegatedFull ownership (GDPR, ISO 27001)
  • DirectoryGranit.OpenIddict/ Abstractions, entities, options, permissions, events
    • DirectoryGranit.OpenIddict.EntityFrameworkCore/ DbContext, OpenIddict server, seeding
    • DirectoryGranit.Identity.Local.AspNetIdentity/ IIdentityProvider bridge (AspNetIdentityProvider)
    • DirectoryGranit.OpenIddict.Endpoints/ Account self-service + admin REST API
    • DirectoryGranit.OpenIddict.BackgroundJobs/ Token cleanup + idle session enforcement
    • DirectoryGranit.Bundle.OpenIddict/ Meta-package (pulls all packages)
PackageResponsibility
Granit.OpenIddictAbstractions, entities, options, permissions, integration events, diagnostics
Granit.OpenIddict.EntityFrameworkCoreOpenIddictDbContext, server configuration, seed contributor, host builder extension
Granit.Identity.Local.AspNetIdentityAspNetIdentityProvider implementing all 7 IIdentityProvider sub-interfaces
Granit.OpenIddict.Endpoints40+ REST endpoints (account self-service + admin management)
Granit.OpenIddict.BackgroundJobs[RecurringJob] for token cleanup and idle session enforcement
Granit.Bundle.OpenIddictGranitBuilder.AddOpenIddict() convenience meta-package
graph TD
    OID[Granit.OpenIddict] --> IDEN[Granit.Identity]
    OID --> EVT[Granit.EventBus]
    OID --> SEC[Granit.Users]
    OID --> QRY[Granit.QueryEngine]
    OID --> GID[Granit.Guids]
    OID --> TIM[Granit.Timing]

    EFC[Granit.OpenIddict.EntityFrameworkCore] --> OID
    EFC --> PER[Granit.Persistence]
    EFC --> MT[Granit.MultiTenancy]

    IOID[Granit.Identity.Local.AspNetIdentity] --> IDEN
    IOID --> EFC

    EP[Granit.OpenIddict.Endpoints] --> OID
    EP --> AZ[Granit.Authorization]
    EP --> VAL[Granit.Validation]
    EP --> DOC[Granit.Http.ApiDocumentation]

    BG[Granit.OpenIddict.BackgroundJobs] --> OID
    BG --> BJ[Granit.BackgroundJobs]
<PackageReference Include="Granit.Bundle.OpenIddict" />

Or add packages individually for fine-grained control:

Terminal window
dotnet add package Granit.OpenIddict.EntityFrameworkCore
dotnet add package Granit.OpenIddict.Endpoints
dotnet add package Granit.OpenIddict.BackgroundJobs
dotnet add package Granit.Identity.OpenIddict
[DependsOn(
typeof(GranitIdentityLocalAspNetIdentityModule),
typeof(GranitOpenIddictBackgroundJobsModule),
typeof(GranitOpenIddictEndpointsModule),
typeof(GranitOpenIddictEntityFrameworkCoreModule))]
public sealed class MyAppModule : GranitModule;
Program.cs
builder.AddGranitOpenIddict(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("Identity")));
app.UseAuthentication();
app.UseOpenIddict(); // MUST be between Authentication and Authorization
app.UseAuthorization();
app.MapOpenIddictEndpoints();
Terminal window
dotnet ef migrations add Initial --context OpenIddictDbContext
dotnet ef database update --context OpenIddictDbContext
{
"OpenIddict": {
"Seeding": {
"Applications": [
{
"ClientId": "my-spa",
"ClientSecret": null,
"DisplayName": "My SPA",
"Permissions": [
"ept:authorization", "ept:token", "ept:logout",
"gt:authorization_code", "gt:refresh_token",
"scp:openid", "scp:profile", "scp:email", "scp:offline_access"
],
"RedirectUris": ["https://localhost:5173/callback"],
"PostLogoutRedirectUris": ["https://localhost:5173"]
}
]
}
}
}
SpecificationSupport
OAuth 2.0 (RFC 6749)Full
Authorization Code + PKCE (RFC 7636)Full
Client CredentialsFull
Refresh TokensFull
Device Authorization (RFC 8628)Full
Token Introspection (RFC 7662)Full
Token Revocation (RFC 7009)Full
RP-Initiated LogoutFull
Discovery (/.well-known/openid-configuration)Full
Pushed Authorization Requests (RFC 9126)Full
DPoP — Proof-of-Possession (RFC 9449)Full
JWT-Secured Authorization Requests (RFC 9101)Full
Token Exchange (RFC 8693)Opt-in
private_key_jwt (RFC 7523)Full
Authorization Response Issuer Verification (RFC 9207)Full
Back-Channel Logout (OIDC)Full
FAPI 2.0 Security ProfileFull
Reference TokensOpt-in
Passkeys / WebAuthn (FIDO2)Custom grant
TOTP Two-FactorCustom grant

GranitUser extends IdentityUser<Guid> which is managed by ASP.NET Core Identity’s UserManager<T>. The UserManager uses reflection and metadata patterns that are incompatible with the ISoftDeletable interface. Instead, GranitUser has manual IsDeleted / DeletedAt / DeletedBy fields with an explicit named query filter registered in ConfigureOpenIddictModule(). The filter follows the same bypass || real pattern as ApplyGranitConventions so that IDataFilter.Disable<ISoftDeletable>() works consistently.

ASP.NET Core Identity manages its own ConcurrencyStamp property on IdentityUser<TKey>. Adding IConcurrencyAware would create a second, conflicting concurrency mechanism.

OpenIddict’s entity cache uses ClientId as the sole cache key. In multi-tenant setups, two tenants with the same ClientId would share cached data (cross-tenant pollution). EnableEntityCaching defaults to false. Enable only for single-tenant deployments.

All Identity and OpenIddict tables use the openiddict_ prefix (configurable via GranitOpenIddictDbProperties.DbTablePrefix) to avoid collisions with other modules sharing the same database.

GranitRole is intentionally not tenant-scoped. Roles are shared globally across all tenants so that a single RBAC matrix applies platform-wide. Tenant-specific access is enforced at the permission/policy level, not at the role definition level.

Do NOT add Granit.Identity.Federated.EntityFrameworkCore

Section titled “Do NOT add Granit.Identity.Federated.EntityFrameworkCore”

When using Granit.Identity.Local.AspNetIdentity, do not add Granit.Identity.Federated.EntityFrameworkCore. GranitUser is the source of truth — UserCacheEntry and UserCacheSyncMiddleware are unnecessary and create data duplication risk. A warning is logged at startup if both packages are detected. See ADR-019 for the full rationale.

The module publishes three integration events via IDistributedEventBus (Wolverine outbox):

EventWhenConsumers
UserRegisteredEtoNew user registrationOnboarding workflows, CRM sync
AccountDeletedEtoGDPR account deletionData cleanup across modules
UserImpersonatedEtoAdmin impersonationTransparency notification, audit log