Granit.Identity
Granit.Identity provides a provider-agnostic identity management layer. Keycloak, Cognito, Google Cloud Identity Platform (Firebase Auth), or any OIDC provider handles authentication — Granit.Identity handles everything else: user lookup, role management, session control, password operations, and a local user cache with GDPR erasure and pseudonymization built in.
Package structure
Section titled “Package structure”DirectoryGranit.Identity/ Abstractions (7 interfaces), null defaults
- Granit.Identity.Keycloak Keycloak Admin API implementation
- Granit.Identity.Cognito AWS Cognito User Pool API implementation
- Granit.Identity.GoogleCloud Google Cloud Identity Platform (Firebase Auth) implementation
- Granit.Identity.EntityFrameworkCore User cache (cache-aside, login-time sync)
- Granit.Identity.Endpoints Minimal API endpoints (CRUD, sync, GDPR, webhook)
| Package | Role | Depends on |
|---|---|---|
Granit.Identity | Abstractions, null defaults | — |
Granit.Identity.Keycloak | Keycloak Admin API implementation | Granit.Identity |
Granit.Identity.Cognito | AWS Cognito User Pool API implementation | Granit.Identity |
Granit.Identity.GoogleCloud | Firebase Auth implementation (Firebase Admin SDK) | Granit.Identity |
Granit.Authentication.GoogleCloud | JWT Bearer for Firebase Auth + claims transformation | Granit.Authentication.JwtBearer |
Granit.Identity.EntityFrameworkCore | EF Core user cache | Granit.Identity, Granit.Persistence |
Granit.Identity.Endpoints | REST endpoints for user cache | Granit.Identity, Granit.Authorization |
Dependency graph
Section titled “Dependency graph”graph TD
I[Granit.Identity] --> C[Granit.Core]
IK[Granit.Identity.Keycloak] --> I
IC[Granit.Identity.Cognito] --> I
IGC[Granit.Identity.GoogleCloud] --> I
AGC[Granit.Authentication.GoogleCloud] --> JB[Granit.Authentication.JwtBearer]
IEF[Granit.Identity.EntityFrameworkCore] --> I
IEF --> P[Granit.Persistence]
IE[Granit.Identity.Endpoints] --> I
IE --> AZ[Granit.Authorization]
[DependsOn(typeof(GranitIdentityKeycloakModule))][DependsOn(typeof(GranitIdentityEntityFrameworkCoreModule))][DependsOn(typeof(GranitIdentityEndpointsModule))]public class AppModule : GranitModule{ public override void ConfigureServices(ServiceConfigurationContext context) { context.Services.AddGranitIdentityEntityFrameworkCore<AppDbContext>(); context.Services.AddGranitIdentityEndpoints(); }
public override void OnApplicationInitialization(ApplicationInitializationContext context) { context.App.MapIdentityUserCacheEndpoints(); }}[DependsOn(typeof(GranitIdentityCognitoModule))][DependsOn(typeof(GranitIdentityEntityFrameworkCoreModule))][DependsOn(typeof(GranitIdentityEndpointsModule))]public class AppModule : GranitModule{ public override void ConfigureServices(ServiceConfigurationContext context) { context.Services.AddGranitIdentityEntityFrameworkCore<AppDbContext>(); context.Services.AddGranitIdentityEndpoints(); }
public override void OnApplicationInitialization(ApplicationInitializationContext context) { context.App.MapIdentityUserCacheEndpoints(); }}[DependsOn(typeof(GranitIdentityGoogleCloudModule))][DependsOn(typeof(GranitAuthenticationGoogleCloudModule))][DependsOn(typeof(GranitIdentityEntityFrameworkCoreModule))][DependsOn(typeof(GranitIdentityEndpointsModule))]public class AppModule : GranitModule{ public override void ConfigureServices(ServiceConfigurationContext context) { context.Services.AddGranitIdentityEntityFrameworkCore<AppDbContext>(); context.Services.AddGranitIdentityEndpoints(); }
public override void OnApplicationInitialization(ApplicationInitializationContext context) { context.App.MapIdentityUserCacheEndpoints(); }}[DependsOn(typeof(GranitIdentityModule))]public class AppModule : GranitModule { }Registers NullIdentityProvider and NullUserLookupService — useful for development
or modules that only need the interfaces.
context.Services.AddIdentityProvider<MyCustomIdentityProvider>();Interface Segregation
Section titled “Interface Segregation”IIdentityProvider is a composite interface built from 7 fine-grained interfaces
following ISP (Interface Segregation Principle). Inject only what you need:
| Interface | Methods | Purpose |
|---|---|---|
IIdentityUserReader | GetUsersAsync, GetUserAsync | Read user profiles |
IIdentityUserWriter | CreateUserAsync, UpdateUserAsync, SetUserEnabledAsync | Mutate user profiles |
IIdentityRoleManager | GetRolesAsync, GetUserRolesAsync, AssignRoleAsync, RemoveRoleAsync, GetRoleMembersAsync | Role assignments |
IIdentityGroupManager | GetGroupsAsync, GetUserGroupsAsync, AddUserToGroupAsync, RemoveUserFromGroupAsync | Group membership |
IIdentitySessionManager | GetUserSessionsAsync, GetUserDeviceActivityAsync, TerminateSessionAsync, TerminateAllSessionsAsync | Active session control |
IIdentityPasswordManager | GetPasswordChangedAtAsync, SendPasswordResetEmailAsync, SetTemporaryPasswordAsync | Password operations |
IIdentityCredentialVerifier | VerifyUserCredentialsAsync | Credential validation |
// Inject only what you need — not the full IIdentityProviderpublic class UserProfileService(IIdentityUserReader userReader){ public async Task<IdentityUser?> GetProfileAsync( string userId, CancellationToken cancellationToken) { return await userReader.GetUserAsync(userId, cancellationToken) .ConfigureAwait(false); }}Provider capabilities
Section titled “Provider capabilities”Not all providers support every operation. Query capabilities at runtime:
public class SessionService( IIdentitySessionManager sessions, IIdentityProviderCapabilities capabilities){ public async Task TerminateSessionAsync( string userId, string sessionId, CancellationToken cancellationToken) { if (!capabilities.SupportsIndividualSessionTermination) { throw new BusinessException( "Identity:UnsupportedOperation", "Provider does not support individual session termination"); }
await sessions.TerminateSessionAsync(userId, sessionId, cancellationToken) .ConfigureAwait(false); }}User cache (cache-aside)
Section titled “User cache (cache-aside)”Granit.Identity.EntityFrameworkCore maintains a local cache of user data from the
identity provider. This avoids hitting Keycloak on every “who is this user?” query.
IUserLookupService
Section titled “IUserLookupService”public interface IUserLookupService{ Task<UserCacheEntry?> FindByIdAsync(string userId, CancellationToken cancellationToken); Task<IReadOnlyList<UserCacheEntry>> FindByIdsAsync( IReadOnlyCollection<string> userIds, CancellationToken cancellationToken); Task<PagedResult<UserCacheEntry>> SearchAsync( string searchTerm, int page, int pageSize, CancellationToken cancellationToken); Task RefreshByIdAsync(string userId, CancellationToken cancellationToken); Task RefreshAllAsync(CancellationToken cancellationToken); Task RefreshStaleAsync(CancellationToken cancellationToken); Task DeleteByIdAsync(string userId, CancellationToken cancellationToken); Task PseudonymizeByIdAsync(string userId, CancellationToken cancellationToken);}Cache-aside strategy
Section titled “Cache-aside strategy”sequenceDiagram
participant App
participant Cache as UserLookupService
participant DB as EF Core
participant IDP as Keycloak
App->>Cache: FindByIdAsync("user-123")
Cache->>DB: SELECT WHERE ExternalUserId = "user-123"
alt Cache hit (fresh)
DB-->>Cache: UserCacheEntry
Cache-->>App: UserCacheEntry
else Cache miss or stale
Cache->>IDP: GetUserAsync("user-123")
IDP-->>Cache: IdentityUser
Cache->>DB: INSERT/UPDATE UserCacheEntry
Cache-->>App: UserCacheEntry
end
Entity
Section titled “Entity”public class UserCacheEntry : AuditedEntity, IMultiTenant{ public string ExternalUserId { get; set; } = string.Empty; public string? Username { get; set; } public string? Email { get; set; } public string? FirstName { get; set; } public string? LastName { get; set; } public bool Enabled { get; set; } public DateTimeOffset? LastSyncedAt { get; set; } public Guid? TenantId { get; set; }}Login-time sync
Section titled “Login-time sync”UserCacheSyncMiddleware automatically refreshes the cache entry when a user
authenticates. This ensures the cache stays fresh without polling.
Configuration
Section titled “Configuration”{ "IdentityUserCache": { "StalenessThreshold": "1.00:00:00", "EnableLoginTimeSync": true, "IncrementalSyncBatchSize": 50 }}| Property | Default | Description |
|---|---|---|
StalenessThreshold | 24:00:00 | Age after which a cache entry is considered stale |
EnableLoginTimeSync | true | Refresh cache on login |
IncrementalSyncBatchSize | 50 | Batch size for RefreshStaleAsync |
Keycloak provider
Section titled “Keycloak provider”Granit.Identity.Keycloak implements the full IIdentityProvider against the
Keycloak Admin REST API.
Configuration
Section titled “Configuration”{ "KeycloakAdmin": { "BaseUrl": "https://keycloak.example.com", "Realm": "my-realm", "ClientId": "admin-cli", "ClientSecret": "secret", "TimeoutSeconds": 30, "UseTokenExchangeForDeviceActivity": false, "DirectAccessClientId": "direct-access-client" }}Required Keycloak service account roles:
realm-management:view-usersrealm-management:manage-users
Health check
Section titled “Health check”builder.Services.AddHealthChecks() .AddGranitKeycloakHealthCheck();Verifies Keycloak connectivity by requesting a client_credentials token. Tagged
["readiness", "startup"]. Returns Unhealthy on 401/403, Degraded on 5xx.
OpenTelemetry
Section titled “OpenTelemetry”All Keycloak Admin API calls are traced via IdentityKeycloakActivitySource.
Activity names follow the pattern Granit.Identity.Keycloak.{Operation}.
Cognito provider
Section titled “Cognito provider”Granit.Identity.Cognito implements the full IIdentityProvider against the
AWS Cognito User Pool API via AWSSDK.CognitoIdentityProvider.
Configuration
Section titled “Configuration”{ "CognitoAdmin": { "UserPoolId": "eu-west-1_XXXXXXXXX", "Region": "eu-west-1", "AccessKeyId": "", "SecretAccessKey": "", "TimeoutSeconds": 30 }}AccessKeyId and SecretAccessKey are optional — when omitted, the SDK uses the
default credential chain (IAM role, environment variables, ~/.aws/credentials).
Cognito-specific behavior
Section titled “Cognito-specific behavior”- Groups as roles — Cognito has no native “roles” concept. Groups serve as both
roles and groups.
GetRolesAsyncandGetGroupsAsyncreturn the same data. - No individual session termination — Cognito supports
AdminUserGlobalSignOut(terminate all sessions) but not individual session termination.IIdentityProviderCapabilities.SupportsIndividualSessionTerminationreturnsfalse. - No device activity —
GetUserDeviceActivityAsyncreturns an empty list.
OpenTelemetry
Section titled “OpenTelemetry”All Cognito User Pool API calls are traced via IdentityCognitoActivitySource.
Activity names follow the pattern cognito.{operation}.
Google Cloud Identity Platform provider
Section titled “Google Cloud Identity Platform provider”Granit.Identity.GoogleCloud implements the full IIdentityProvider against the
Firebase Auth API via Firebase Admin SDK v3.
Configuration
Section titled “Configuration”{ "Identity": { "GoogleCloud": { "ProjectId": "my-firebase-project", "RolesClaimKey": "roles", "TimeoutSeconds": 30 } }}For Workload Identity (recommended in GKE), omit CredentialFilePath —
Application Default Credentials are used automatically.
Google Cloud-specific behavior
Section titled “Google Cloud-specific behavior”- Roles via custom claims — Firebase Auth has no native roles. User roles are
stored as a JSON array in custom claims (configurable key, default
"roles").GetRolesAsyncreturns an empty list;GetUserRolesAsyncextracts from claims. - Groups not supported — Firebase Auth does not support groups.
AddUserToGroupAsyncandRemoveUserFromGroupAsyncthrowNotSupportedException. - No individual session termination — Firebase supports
RevokeRefreshTokens(terminate all sessions) but not individual session termination.IIdentityProviderCapabilities.SupportsIndividualSessionTerminationreturnsfalse. - Credential verification —
VerifyUserCredentialsAsyncvalidates email/password via the Firebase Auth REST API.
Health check
Section titled “Health check”builder.Services.AddHealthChecks() .AddGranitGoogleCloudIdentityHealthCheck();Verifies that ProjectId is configured. Tagged ["readiness"].
OpenTelemetry
Section titled “OpenTelemetry”All Firebase Auth API calls are traced via IdentityGoogleCloudActivitySource.
Activity names follow the pattern firebase.{operation}.
Google Cloud Authentication
Section titled “Google Cloud Authentication”Granit.Authentication.GoogleCloud configures JWT Bearer authentication for
Firebase Auth tokens and transforms custom claims into standard ClaimTypes.Role.
Configuration
Section titled “Configuration”{ "GoogleCloudAuth": { "ProjectId": "my-firebase-project", "RequireHttpsMetadata": true, "AdminRole": "admin", "RolesClaimKey": "roles" }}| Property | Default | Description |
|---|---|---|
ProjectId | — | GCP project ID (derives OIDC authority) |
RequireHttpsMetadata | true | Require HTTPS for OIDC metadata discovery |
AdminRole | "admin" | Role name for the “Admin” authorization policy |
RolesClaimKey | "roles" | Custom claims key containing roles |
The module derives the OIDC authority as https://securetoken.google.com/{ProjectId}
and configures JWT Bearer validation with Audience = ProjectId,
NameClaimType = "email".
Claims transformation
Section titled “Claims transformation”GoogleCloudClaimsTransformation maps Firebase custom claims to
ClaimTypes.Role. Supports JSON array format (["admin","editor"]) and
single string values. Existing ClaimTypes.Role claims are preserved without
duplication.
Endpoints
Section titled “Endpoints”Granit.Identity.Endpoints exposes user cache management as Minimal API endpoints.
| Method | Route | Permission | Description |
|---|---|---|---|
| GET | /identity/users/search | Identity.UserCache.Read | Search cached users |
| GET | /identity/users/{id} | Identity.UserCache.Read | Get by external ID |
| POST | /identity/users/batch | Identity.UserCache.Read | Batch resolve by IDs |
| POST | /identity/users/sync | Identity.UserCache.Sync | Sync specific user |
| POST | /identity/users/sync-all | Identity.UserCache.Sync | Full sync from provider |
| DELETE | /identity/users/{id} | Identity.UserCache.Delete | GDPR erase |
| PATCH | /identity/users/{id}/pseudonymize | Identity.UserCache.Delete | GDPR pseudonymize |
| GET | /identity/users/stats | Identity.UserCache.Read | Cache statistics |
| GET | /identity/users/capabilities | Identity.UserCache.Read | Provider capabilities |
| POST | /identity-webhook | (anonymous, signature-validated) | IdP webhook receiver |
Webhook
Section titled “Webhook”The webhook endpoint receives events from the identity provider (user created, updated, deleted) and updates the local cache accordingly. Payload authenticity is verified via HMAC signature validation.
GDPR operations
Section titled “GDPR operations”Two operations support data subject rights:
// Right to erasure (Art. 17) — hard deleteawait userLookupService.DeleteByIdAsync("user-123", cancellationToken) .ConfigureAwait(false);
// Right to restriction (Art. 18) — pseudonymizeawait userLookupService.PseudonymizeByIdAsync("user-123", cancellationToken) .ConfigureAwait(false);- Delete removes the
UserCacheEntryentirely (physical delete, not soft delete) - Pseudonymize replaces PII fields with anonymized values while preserving the record
Both operations emit Wolverine domain events (IdentityUserDeletedEvent,
IdentityUserUpdatedEvent) for downstream modules to react.
Public API summary
Section titled “Public API summary”| Category | Key types | Package |
|---|---|---|
| Module | GranitIdentityModule, GranitIdentityKeycloakModule, GranitIdentityCognitoModule, GranitIdentityGoogleCloudModule, GranitAuthenticationGoogleCloudModule, GranitIdentityEntityFrameworkCoreModule, GranitIdentityEndpointsModule | — |
| Abstractions | IIdentityProvider, IIdentityUserReader, IIdentityUserWriter, IIdentityRoleManager, IIdentityGroupManager, IIdentitySessionManager, IIdentityPasswordManager, IIdentityCredentialVerifier | Granit.Identity |
| Lookup | IUserLookupService, IUserCacheStats, IIdentityProviderCapabilities | Granit.Identity |
| Models | IdentityUser, IdentityUserCreate, IdentityUserUpdate, IdentityRole, IdentityGroup, IdentitySession, IdentityDeviceActivity | Granit.Identity |
| Keycloak | KeycloakIdentityProvider, KeycloakAdminOptions | Granit.Identity.Keycloak |
| Cognito | CognitoIdentityProvider, CognitoAdminOptions | Granit.Identity.Cognito |
| Google Cloud | GoogleCloudIdentityProvider, GoogleCloudIdentityOptions | Granit.Identity.GoogleCloud |
| Google Cloud Auth | GoogleCloudClaimsTransformation, GoogleCloudAuthenticationOptions | Granit.Authentication.GoogleCloud |
| EF Core | UserCacheEntry, IUserCacheDbContext, UserCacheOptions | Granit.Identity.EntityFrameworkCore |
| Endpoints | IdentityEndpointsOptions, IdentityWebhookOptions | Granit.Identity.Endpoints |
| Extensions | AddGranitIdentity(), AddIdentityProvider<T>(), AddGranitIdentityKeycloak(), AddGranitIdentityCognito(), AddGranitIdentityGoogleCloud(), AddGranitGoogleCloudAuthentication(), AddGranitIdentityEntityFrameworkCore<T>(), AddGranitIdentityEndpoints(), MapIdentityUserCacheEndpoints() | — |
See also
Section titled “See also”- Security module — Authentication, authorization, RBAC permissions
- Privacy module — GDPR data export/deletion, cookie consent
- Persistence module —
AuditedEntity, interceptors - API Reference (auto-generated from XML docs)