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.
Why self-hosted?
Section titled “Why self-hosted?”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 store | External system | Your database |
| User management | External admin UI | Your API (/api/admin/users) |
| Customization | Limited by provider | Full control |
| Deployment | Separate service | Same process |
| Multi-tenancy | Provider-dependent | Built-in (IMultiTenant) |
| Data residency | Provider’s infra | Your infrastructure |
| Compliance | Delegated | Full ownership (GDPR, ISO 27001) |
Package structure
Section titled “Package structure”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)
- …
| Package | Responsibility |
|---|---|
Granit.OpenIddict | Abstractions, entities, options, permissions, integration events, diagnostics |
Granit.OpenIddict.EntityFrameworkCore | OpenIddictDbContext, server configuration, seed contributor, host builder extension |
Granit.Identity.Local.AspNetIdentity | AspNetIdentityProvider implementing all 7 IIdentityProvider sub-interfaces |
Granit.OpenIddict.Endpoints | 40+ REST endpoints (account self-service + admin management) |
Granit.OpenIddict.BackgroundJobs | [RecurringJob] for token cleanup and idle session enforcement |
Granit.Bundle.OpenIddict | GranitBuilder.AddOpenIddict() convenience meta-package |
Dependency graph
Section titled “Dependency graph”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]
Quick start
Section titled “Quick start”1. Add the bundle
Section titled “1. Add the bundle”<PackageReference Include="Granit.Bundle.OpenIddict" />Or add packages individually for fine-grained control:
dotnet add package Granit.OpenIddict.EntityFrameworkCoredotnet add package Granit.OpenIddict.Endpointsdotnet add package Granit.OpenIddict.BackgroundJobsdotnet add package Granit.Identity.OpenIddict2. Register modules
Section titled “2. Register modules”[DependsOn( typeof(GranitIdentityLocalAspNetIdentityModule), typeof(GranitOpenIddictBackgroundJobsModule), typeof(GranitOpenIddictEndpointsModule), typeof(GranitOpenIddictEntityFrameworkCoreModule))]public sealed class MyAppModule : GranitModule;3. Configure services
Section titled “3. Configure services”builder.AddGranitOpenIddict(options => options.UseNpgsql(builder.Configuration.GetConnectionString("Identity")));4. Configure middleware (order matters)
Section titled “4. Configure middleware (order matters)”app.UseAuthentication();app.UseOpenIddict(); // MUST be between Authentication and Authorizationapp.UseAuthorization();
app.MapOpenIddictEndpoints();5. Run migrations
Section titled “5. Run migrations”dotnet ef migrations add Initial --context OpenIddictDbContextdotnet ef database update --context OpenIddictDbContext6. Seed an application
Section titled “6. Seed an application”{ "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"] } ] } }}OpenID Connect compliance
Section titled “OpenID Connect compliance”| Specification | Support |
|---|---|
| OAuth 2.0 (RFC 6749) | Full |
| Authorization Code + PKCE (RFC 7636) | Full |
| Client Credentials | Full |
| Refresh Tokens | Full |
| Device Authorization (RFC 8628) | Full |
| Token Introspection (RFC 7662) | Full |
| Token Revocation (RFC 7009) | Full |
| RP-Initiated Logout | Full |
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 Profile | Full |
| Reference Tokens | Opt-in |
| Passkeys / WebAuthn (FIDO2) | Custom grant |
| TOTP Two-Factor | Custom grant |
Key design decisions
Section titled “Key design decisions”No ISoftDeletable on GranitUser
Section titled “No ISoftDeletable on GranitUser”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.
No IConcurrencyAware
Section titled “No IConcurrencyAware”ASP.NET Core Identity manages its own ConcurrencyStamp property on IdentityUser<TKey>.
Adding IConcurrencyAware would create a second, conflicting concurrency mechanism.
Entity caching disabled by default
Section titled “Entity caching disabled by default”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.
Table prefix: openiddict_
Section titled “Table prefix: openiddict_”All Identity and OpenIddict tables use the openiddict_ prefix (configurable via
GranitOpenIddictDbProperties.DbTablePrefix) to avoid collisions with other
modules sharing the same database.
Roles are global
Section titled “Roles are global”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.
Integration events
Section titled “Integration events”The module publishes three integration events via IDistributedEventBus (Wolverine outbox):
| Event | When | Consumers |
|---|---|---|
UserRegisteredEto | New user registration | Onboarding workflows, CRM sync |
AccountDeletedEto | GDPR account deletion | Data cleanup across modules |
UserImpersonatedEto | Admin impersonation | Transparency notification, audit log |
Next steps
Section titled “Next steps”- OIDC Server Configuration — flows, endpoints, signing keys
- User Management — entities, admin API, IIdentityProvider bridge
- Account Self-service API — registration, 2FA, GDPR
- Advanced Features — multi-tenancy, impersonation, passkeys, idle sessions
- Configuration Reference — all options with JSON examples