Skip to content

Identity — User Claims & Tenant Context

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
    • DirectoryGranit.Identity.Federated/ Federated identity abstractions
      • Granit.Identity.Federated.Keycloak Keycloak Admin API implementation (+ realm-vs-client role distinction)
      • Granit.Identity.Federated.EntraId Microsoft Graph API implementation (+ Entra ID App Role distinction)
      • Granit.Identity.Federated.Cognito AWS Cognito User Pool API implementation
      • Granit.Identity.Federated.GoogleCloud Google Cloud Identity Platform (Firebase Auth) implementation
      • Granit.Identity.Federated.EntityFrameworkCore User cache (cache-aside, login-time sync)
      • Granit.Identity.Federated.Privacy Personal-data export provider for the federated user cache
    • DirectoryGranit.Identity.Local/ Shared domain types for self-hosted identity providers
      • Granit.Identity.Local.AspNetIdentity ASP.NET Core Identity provider (GranitUser + role orchestration + tenant-aware lookup)
      • Granit.Identity.Local.Endpoints Account self-service endpoints (login, register, profile, 2FA, passkey) + role CRUD + admin impersonation
      • Granit.Identity.Local.Notifications 9 security notifications (welcome, password reset, 2FA, email change, impersonation)
      • Granit.Identity.Local.Privacy Personal-data export provider for the local identity store
    • Granit.Identity.Endpoints Minimal API endpoints (provider CRUD, user cache sync, GDPR, webhook)
PackageRoleDepends on
Granit.IdentityAbstractions, null defaults
Granit.Identity.FederatedFederated identity abstractionsGranit.Identity
Granit.Identity.Federated.KeycloakKeycloak Admin API implementation (+ realm-vs-client role distinction)Granit.Identity.Federated
Granit.Identity.Federated.EntraIdMicrosoft Graph API implementation (+ App Role boot-time sync)Granit.Identity.Federated
Granit.Identity.Federated.CognitoAWS Cognito User Pool API implementationGranit.Identity.Federated
Granit.Identity.Federated.GoogleCloudFirebase Auth implementation (Firebase Admin SDK)Granit.Identity.Federated
Granit.Authentication.JwtBearer.GoogleCloudJWT Bearer for Firebase Auth + claims transformationGranit.Authentication.JwtBearer
Granit.Identity.Federated.EntityFrameworkCoreEF Core user cacheGranit.Identity.Federated, Granit.Persistence
Granit.Identity.Federated.PrivacyPersonal-data export provider for the federated user cacheGranit.Identity.Federated, Granit.Privacy.BlobStorage
Granit.Identity.LocalShared domain types for self-hosted identity providersGranit.Identity, Granit.Events, Granit.Guids, Granit.Settings, Granit.Timing
Granit.Identity.Local.AspNetIdentityASP.NET Core Identity provider, role orchestration, tenant-aware role lookupGranit.Identity.Local, Granit.Persistence.EntityFrameworkCore
Granit.Identity.Local.EndpointsAccount self-service + role CRUD + admin impersonation REST endpointsGranit.Identity.Local, Granit.Authorization
Granit.Identity.Local.Notifications9 security email/in-app notifications (17 cultures)Granit.Identity.Local, Granit.Notifications.Abstractions, Granit.Templating
Granit.Identity.Local.PrivacyPersonal-data export provider for the local identity storeGranit.Identity.Local, Granit.Privacy.BlobStorage
Granit.Identity.EndpointsREST endpoints for user cache + provider CRUD + GDPRGranit.Identity, Granit.Authorization
graph TD
    I[Granit.Identity] --> C[Granit]
    IF[Granit.Identity.Federated] --> I
    IK[Granit.Identity.Federated.Keycloak] --> IF
    IEI[Granit.Identity.Federated.EntraId] --> IF
    IC[Granit.Identity.Federated.Cognito] --> IF
    IGC[Granit.Identity.Federated.GoogleCloud] --> IF
    AGC[Granit.Authentication.JwtBearer.GoogleCloud] --> JB[Granit.Authentication.JwtBearer]
    IEF[Granit.Identity.Federated.EntityFrameworkCore] --> IF
    IEF --> P[Granit.Persistence]
    IFP[Granit.Identity.Federated.Privacy] --> IF
    IFP --> PR[Granit.Privacy.BlobStorage]
    IL[Granit.Identity.Local] --> I
    IL --> S[Granit.Settings]
    ILA[Granit.Identity.Local.AspNetIdentity] --> IL
    ILE[Granit.Identity.Local.Endpoints] --> IL
    ILE --> AZ[Granit.Authorization]
    ILN[Granit.Identity.Local.Notifications] --> IL
    ILN --> N[Granit.Notifications.Abstractions]
    ILN --> T[Granit.Templating]
    ILP[Granit.Identity.Local.Privacy] --> IL
    ILP --> PR
    IE[Granit.Identity.Endpoints] --> I
    IE --> AZ
[DependsOn(typeof(GranitIdentityFederatedKeycloakModule))]
[DependsOn(typeof(GranitIdentityFederatedEntityFrameworkCoreModule))]
[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.MapGranitIdentityUserCache();
}
}

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.Federated.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.Federated.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.Federated.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.Federated.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.JwtBearer.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-SHA256 signature validation using the shared secret configured in IdentityWebhookOptions.

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.

Granit.Identity.Local.Notifications provides 9 out-of-the-box email and in-app notifications for local identity management. Wolverine event handlers listen to identity lifecycle events and dispatch notifications via Granit.Notifications.

NotificationChannelsOpt-outTrigger
Security.WelcomeEmailYesUser registration
Security.PasswordResetEmailNoForgot password flow
Security.EmailConfirmationEmailNoRegistration / resend
Security.PasswordChangedEmailNoPassword change (compromise alert)
Security.AccountLockedEmail, InAppNoFailed login attempts (includes password reset link for self-service unlock)
Security.TwoFactorChangedEmailNo2FA enabled/disabled
Security.EmailChangeAlertEmailNoEmail change alert (old address)
Security.EmailChangeConfirmationEmailNoEmail change confirm (new address)
Security.ImpersonationAlertEmail, InAppNoAdmin impersonation (GDPR/SOC2)
[DependsOn(typeof(GranitIdentityLocalNotificationsModule))]
public class AppModule : GranitModule { }
{
"Identity": {
"Notifications": {
"FrontendBaseUrl": "https://app.example.com",
"ResetPasswordPath": "reset-password",
"ConfirmEmailPath": "confirm-email",
"ChangeEmailPath": "confirm-email-change"
}
}
}

Templates are embedded in the package (9 types x 18 cultures) and wrapped by Layout.Email via the layout registry. Tenants can override any template or the layout via the database template store.

The Security.AccountLocked notification is sent when a user’s account is locked after exceeding the maximum number of failed login attempts. The email includes:

  • The number of failed attempts that triggered the lockout
  • The exact lockout expiry date (formatted per the user’s locale via Scriban)
  • A “Reset my password” button with a one-time reset token

Resetting the password via this link immediately unlocks the account and resets the exponential backoff counter — the user never needs to wait or contact support.

See Account lockout for the complete lockout security architecture.

Email change triggers two notifications from a single EmailChangeRequestedEto event:

  1. Security.EmailChangeAlert → sent to the current email (security alert)
  2. Security.EmailChangeConfirmation → sent to the new email (uses RecipientOverride)

The confirmation token is generated by ASP.NET Core Identity and encodes the new email — no additional database column is needed.

Granit.Identity.Federated.Notifications ships notifications surfacing federated-provider lifecycle events (Keycloak / Entra ID / Cognito / Firebase). Tenants opt in via the notifications admin UI.

NotificationTriggerChannelsSeverity
identity.sync_failedIdentityUserSyncFailedEto — login-time user-cache sync from the federated provider failedEmail, InAppWarning
identity.token_exchange_auditIdentityTokenExchangedEto — token exchange completed (audit trail of who acted on whose behalf)Email, InAppWarning
identity.user_provisioning_removedIdentityUserDeletedEto — federated user removed from the provider; cache entry deprovisionedEmail, InAppInfo

Email templates ship in EN + FR; additional cultures are produced via the translation script (US #1311).

CategoryKey typesPackage
ModuleGranitIdentityModule, GranitIdentityFederatedModule, GranitIdentityFederatedKeycloakModule, GranitIdentityFederatedCognitoModule, GranitIdentityFederatedGoogleCloudModule, GranitAuthenticationJwtBearerGoogleCloudModule, GranitIdentityFederatedEntityFrameworkCoreModule, GranitIdentityEndpointsModule, GranitIdentityLocalAspNetIdentityModule
AbstractionsIIdentityProvider, IIdentityUserReader, IIdentityUserWriter, IIdentityRoleManager, IIdentityGroupManager, IIdentitySessionManager, IIdentityPasswordManager, IIdentityCredentialVerifierGranit.Identity
LookupIUserLookupService, IUserCacheStats, IIdentityProviderCapabilitiesGranit.Identity
ModelsIdentityUser, IdentityUserCreate, IdentityUserUpdate, IdentityRole, IdentityGroup, IdentitySession, IdentityDeviceActivityGranit.Identity
KeycloakKeycloakIdentityProvider, KeycloakAdminOptionsGranit.Identity.Federated.Keycloak
CognitoCognitoIdentityProvider, CognitoAdminOptionsGranit.Identity.Federated.Cognito
Google CloudGoogleCloudIdentityProvider, GoogleCloudIdentityOptionsGranit.Identity.Federated.GoogleCloud
Google Cloud AuthGoogleCloudClaimsTransformation, GoogleCloudAuthenticationOptionsGranit.Authentication.JwtBearer.GoogleCloud
EF CoreUserCacheEntry, IUserCacheDbContext, UserCacheOptionsGranit.Identity.Federated.EntityFrameworkCore
EndpointsIdentityEndpointsOptions, IdentityWebhookOptionsGranit.Identity.Endpoints
NotificationsWelcomeNotificationType, PasswordResetNotificationType, EmailConfirmationNotificationType, PasswordChangedNotificationType, AccountLockedNotificationType, TwoFactorChangedNotificationType, EmailChangeAlertNotificationType, EmailChangeConfirmationNotificationType, ImpersonationAlertNotificationTypeGranit.Identity.Local.Notifications
Notifications optionsIdentityNotificationOptionsGranit.Identity.Local.Notifications
EventsPasswordChangedEto, AccountLockedEto (includes LockoutEndUtc + ResetToken), TwoFactorChangedEto, EmailConfirmationRequestedEto, EmailChangeRequestedEtoGranit.Identity.Local
ExtensionsAddGranitIdentity(), AddIdentityProvider<T>(), AddGranitIdentityKeycloak(), AddGranitIdentityCognito(), AddGranitIdentityGoogleCloud(), AddGranitGoogleCloudAuthentication(), AddGranitIdentityEntityFrameworkCore<T>(), AddGranitIdentityEndpoints(), MapGranitIdentityUserCache()