Skip to content

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.

  • 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)
PackageRoleDepends on
Granit.IdentityAbstractions, null defaults
Granit.Identity.KeycloakKeycloak Admin API implementationGranit.Identity
Granit.Identity.CognitoAWS Cognito User Pool API implementationGranit.Identity
Granit.Identity.GoogleCloudFirebase Auth implementation (Firebase Admin SDK)Granit.Identity
Granit.Authentication.GoogleCloudJWT Bearer for Firebase Auth + claims transformationGranit.Authentication.JwtBearer
Granit.Identity.EntityFrameworkCoreEF Core user cacheGranit.Identity, Granit.Persistence
Granit.Identity.EndpointsREST endpoints for user cacheGranit.Identity, Granit.Authorization
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();
}
}

IIdentityProvider is a composite interface built from 7 fine-grained interfaces following ISP (Interface Segregation Principle). Inject only what you need:

InterfaceMethodsPurpose
IIdentityUserReaderGetUsersAsync, GetUserAsyncRead user profiles
IIdentityUserWriterCreateUserAsync, UpdateUserAsync, SetUserEnabledAsyncMutate user profiles
IIdentityRoleManagerGetRolesAsync, GetUserRolesAsync, AssignRoleAsync, RemoveRoleAsync, GetRoleMembersAsyncRole assignments
IIdentityGroupManagerGetGroupsAsync, GetUserGroupsAsync, AddUserToGroupAsync, RemoveUserFromGroupAsyncGroup membership
IIdentitySessionManagerGetUserSessionsAsync, GetUserDeviceActivityAsync, TerminateSessionAsync, TerminateAllSessionsAsyncActive session control
IIdentityPasswordManagerGetPasswordChangedAtAsync, SendPasswordResetEmailAsync, SetTemporaryPasswordAsyncPassword operations
IIdentityCredentialVerifierVerifyUserCredentialsAsyncCredential validation
// Inject only what you need — not the full IIdentityProvider
public class UserProfileService(IIdentityUserReader userReader)
{
public async Task<IdentityUser?> GetProfileAsync(
string userId, CancellationToken cancellationToken)
{
return await userReader.GetUserAsync(userId, cancellationToken)
.ConfigureAwait(false);
}
}

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

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.

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

UserCacheSyncMiddleware automatically refreshes the cache entry when a user authenticates. This ensures the cache stays fresh without polling.

{
"IdentityUserCache": {
"StalenessThreshold": "1.00:00:00",
"EnableLoginTimeSync": true,
"IncrementalSyncBatchSize": 50
}
}
PropertyDefaultDescription
StalenessThreshold24:00:00Age after which a cache entry is considered stale
EnableLoginTimeSynctrueRefresh cache on login
IncrementalSyncBatchSize50Batch size for RefreshStaleAsync

Granit.Identity.Keycloak implements the full IIdentityProvider against the Keycloak Admin REST API.

{
"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-users
  • realm-management:manage-users
builder.Services.AddHealthChecks()
.AddGranitKeycloakHealthCheck();

Verifies Keycloak connectivity by requesting a client_credentials token. Tagged ["readiness", "startup"]. Returns Unhealthy on 401/403, Degraded on 5xx.

All Keycloak Admin API calls are traced via IdentityKeycloakActivitySource. Activity names follow the pattern Granit.Identity.Keycloak.{Operation}.

Granit.Identity.Cognito implements the full IIdentityProvider against the AWS Cognito User Pool API via AWSSDK.CognitoIdentityProvider.

{
"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).

  • Groups as roles — Cognito has no native “roles” concept. Groups serve as both roles and groups. GetRolesAsync and GetGroupsAsync return the same data.
  • No individual session termination — Cognito supports AdminUserGlobalSignOut (terminate all sessions) but not individual session termination. IIdentityProviderCapabilities.SupportsIndividualSessionTermination returns false.
  • No device activityGetUserDeviceActivityAsync returns an empty list.

All Cognito User Pool API calls are traced via IdentityCognitoActivitySource. Activity names follow the pattern cognito.{operation}.

Granit.Identity.GoogleCloud implements the full IIdentityProvider against the Firebase Auth API via Firebase Admin SDK v3.

{
"Identity": {
"GoogleCloud": {
"ProjectId": "my-firebase-project",
"RolesClaimKey": "roles",
"TimeoutSeconds": 30
}
}
}

For Workload Identity (recommended in GKE), omit CredentialFilePath — Application Default Credentials are used automatically.

  • 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"). GetRolesAsync returns an empty list; GetUserRolesAsync extracts from claims.
  • Groups not supported — Firebase Auth does not support groups. AddUserToGroupAsync and RemoveUserFromGroupAsync throw NotSupportedException.
  • No individual session termination — Firebase supports RevokeRefreshTokens (terminate all sessions) but not individual session termination. IIdentityProviderCapabilities.SupportsIndividualSessionTermination returns false.
  • Credential verificationVerifyUserCredentialsAsync validates email/password via the Firebase Auth REST API.
builder.Services.AddHealthChecks()
.AddGranitGoogleCloudIdentityHealthCheck();

Verifies that ProjectId is configured. Tagged ["readiness"].

All Firebase Auth API calls are traced via IdentityGoogleCloudActivitySource. Activity names follow the pattern firebase.{operation}.

Granit.Authentication.GoogleCloud configures JWT Bearer authentication for Firebase Auth tokens and transforms custom claims into standard ClaimTypes.Role.

{
"GoogleCloudAuth": {
"ProjectId": "my-firebase-project",
"RequireHttpsMetadata": true,
"AdminRole": "admin",
"RolesClaimKey": "roles"
}
}
PropertyDefaultDescription
ProjectIdGCP project ID (derives OIDC authority)
RequireHttpsMetadatatrueRequire 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".

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.

Granit.Identity.Endpoints exposes user cache management as Minimal API endpoints.

MethodRoutePermissionDescription
GET/identity/users/searchIdentity.UserCache.ReadSearch cached users
GET/identity/users/{id}Identity.UserCache.ReadGet by external ID
POST/identity/users/batchIdentity.UserCache.ReadBatch resolve by IDs
POST/identity/users/syncIdentity.UserCache.SyncSync specific user
POST/identity/users/sync-allIdentity.UserCache.SyncFull sync from provider
DELETE/identity/users/{id}Identity.UserCache.DeleteGDPR erase
PATCH/identity/users/{id}/pseudonymizeIdentity.UserCache.DeleteGDPR pseudonymize
GET/identity/users/statsIdentity.UserCache.ReadCache statistics
GET/identity/users/capabilitiesIdentity.UserCache.ReadProvider capabilities
POST/identity-webhook(anonymous, signature-validated)IdP webhook receiver

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.

Two operations support data subject rights:

// Right to erasure (Art. 17) — hard delete
await userLookupService.DeleteByIdAsync("user-123", cancellationToken)
.ConfigureAwait(false);
// Right to restriction (Art. 18) — pseudonymize
await userLookupService.PseudonymizeByIdAsync("user-123", cancellationToken)
.ConfigureAwait(false);
  • Delete removes the UserCacheEntry entirely (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.

CategoryKey typesPackage
ModuleGranitIdentityModule, GranitIdentityKeycloakModule, GranitIdentityCognitoModule, GranitIdentityGoogleCloudModule, GranitAuthenticationGoogleCloudModule, GranitIdentityEntityFrameworkCoreModule, GranitIdentityEndpointsModule
AbstractionsIIdentityProvider, IIdentityUserReader, IIdentityUserWriter, IIdentityRoleManager, IIdentityGroupManager, IIdentitySessionManager, IIdentityPasswordManager, IIdentityCredentialVerifierGranit.Identity
LookupIUserLookupService, IUserCacheStats, IIdentityProviderCapabilitiesGranit.Identity
ModelsIdentityUser, IdentityUserCreate, IdentityUserUpdate, IdentityRole, IdentityGroup, IdentitySession, IdentityDeviceActivityGranit.Identity
KeycloakKeycloakIdentityProvider, KeycloakAdminOptionsGranit.Identity.Keycloak
CognitoCognitoIdentityProvider, CognitoAdminOptionsGranit.Identity.Cognito
Google CloudGoogleCloudIdentityProvider, GoogleCloudIdentityOptionsGranit.Identity.GoogleCloud
Google Cloud AuthGoogleCloudClaimsTransformation, GoogleCloudAuthenticationOptionsGranit.Authentication.GoogleCloud
EF CoreUserCacheEntry, IUserCacheDbContext, UserCacheOptionsGranit.Identity.EntityFrameworkCore
EndpointsIdentityEndpointsOptions, IdentityWebhookOptionsGranit.Identity.Endpoints
ExtensionsAddGranitIdentity(), AddIdentityProvider<T>(), AddGranitIdentityKeycloak(), AddGranitIdentityCognito(), AddGranitIdentityGoogleCloud(), AddGranitGoogleCloudAuthentication(), AddGranitIdentityEntityFrameworkCore<T>(), AddGranitIdentityEndpoints(), MapIdentityUserCacheEndpoints()