User Management
This page covers the entity model, the IIdentityProvider bridge that integrates with
the Granit.Identity abstraction layer, and the admin REST API for managing users, roles,
groups, and OIDC resources.
Entities
Section titled “Entities”GranitUser
Section titled “GranitUser”Extends IdentityUser<Guid> with Granit multi-tenancy and audit support.
public class GranitUser : IdentityUser<Guid>, IMultiTenant{ public string? FirstName { get; set; } // max 256 public string? LastName { get; set; } // max 256 public Guid? TenantId { get; set; } // IMultiTenant public bool IsDeleted { get; set; } // manual soft-delete public DateTimeOffset? DeletedAt { get; set; } public string? DeletedBy { get; set; } // max 256 public string? CustomAttributesJson { get; set; } // JSON extensible attributes public DateTimeOffset CreatedAt { get; set; } // audit public string CreatedBy { get; set; } // audit, max 256 public DateTimeOffset? ModifiedAt { get; set; } // audit public string? ModifiedBy { get; set; } // audit, max 256}GranitRole
Section titled “GranitRole”Extends IdentityRole<Guid> with an optional description.
public class GranitRole : IdentityRole<Guid>{ public string? Description { get; set; } // max 512}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.
GranitUserGroup
Section titled “GranitUserGroup”Custom group entity (ASP.NET Core Identity has no native group concept). Multi-tenant
and audited with a unique constraint on (TenantId, Name).
public class GranitUserGroup : AuditedEntity, IMultiTenant{ public string Name { get; set; } // max 256, unique per tenant public string? Description { get; set; } // max 512 public Guid? TenantId { get; set; }}GranitUserGroupMember
Section titled “GranitUserGroupMember”Join entity linking a user to a group. Unique on (GroupId, UserId).
public class GranitUserGroupMember : AuditedEntity, IMultiTenant{ public Guid GroupId { get; set; } public Guid UserId { get; set; } public Guid? TenantId { get; set; }}OpenIddict entities
Section titled “OpenIddict entities”All four OpenIddict entities are extended with IMultiTenant for tenant isolation:
| Entity | Base class | Table |
|---|---|---|
GranitOpenIddictApplication | OpenIddictEntityFrameworkCoreApplication<Guid, ...> | openiddict_applications |
GranitOpenIddictAuthorization | OpenIddictEntityFrameworkCoreAuthorization<Guid, ...> | openiddict_authorizations |
GranitOpenIddictScope | OpenIddictEntityFrameworkCoreScope<Guid> | openiddict_scopes |
GranitOpenIddictToken | OpenIddictEntityFrameworkCoreToken<Guid, ...> | openiddict_tokens |
Applications and scopes with TenantId == null are global (visible to all tenants).
IIdentityProvider bridge
Section titled “IIdentityProvider bridge”Granit.Identity.Local.AspNetIdentity registers AspNetIdentityProvider as the
IIdentityProvider implementation, replacing the default NullIdentityProvider.
It implements all 7 sub-interfaces of the Granit.Identity abstraction layer:
| Interface | Implementation detail |
|---|---|
IIdentityUserReader | UserManager<GranitUser>.Users LINQ queries |
IIdentityUserWriter | UserManager.CreateAsync(), UpdateAsync() |
IIdentityRoleManager | RoleManager<GranitRole> operations |
IIdentityGroupManager | Direct EF Core queries on GranitUserGroup |
IIdentitySessionManager | No-op (SupportsIndividualSessionTermination = false) |
IIdentityPasswordManager | UserManager.ResetPasswordAsync() |
IIdentityCredentialVerifier | UserManager.CheckPasswordAsync() |
Provider capabilities
Section titled “Provider capabilities”AspNetIdentityProviderCapabilities declares what this provider supports:
ProviderName = "AspNetIdentity"SupportsIndividualSessionTermination = falseSupportsNativePasswordResetEmail = falseSupportsGroupHierarchy = falseSupportsCustomAttributes = trueMaxCustomAttributes = int.MaxValueSupportsCredentialVerification = trueSupportsUserCreation = trueIdentity settings (ASP.NET Core)
Section titled “Identity settings (ASP.NET Core)”AddGranitOpenIddict() configures ASP.NET Core Identity with
secure defaults:
options.User.RequireUniqueEmail = true;options.Lockout.MaxFailedAccessAttempts = 5;options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15);options.SignIn.RequireConfirmedEmail = true;Admin REST API
Section titled “Admin REST API”All admin endpoints are under /api/admin (configurable via OpenIddictEndpointsOptions.AdminRoutePrefix)
and require authorization. Permission constants are defined in OpenIddictPermissions.
Permissions
Section titled “Permissions”public static class OpenIddictPermissions{ public const string GroupName = "OpenIddict";
public static class Users // Read, Create, Manage, Delete, Impersonate public static class Roles // Read, Create, Delete public static class Groups // Read, Create, Manage, Delete public static class Applications // Read, Create, Manage, Delete, Rotate public static class Scopes // Read, Create, Manage, Delete public static class Authorizations // Read, Revoke}All permission strings follow the [Group].[Resource].[Action] convention
(e.g., OpenIddict.Users.Read).
Users (/api/admin/users)
Section titled “Users (/api/admin/users)”| Method | Path | Permission | Description |
|---|---|---|---|
GET | / | Users.Read | Paginated list (?search, ?page, ?pageSize) |
GET | /{userId} | Users.Read | User detail with roles and groups |
POST | / | Users.Create | Admin-initiated user creation (no email confirmation) |
DELETE | /{userId} | Users.Delete | Soft-delete + token revocation |
POST | /{userId}/impersonate | Users.Impersonate | Issue impersonation token |
Create user request:
{ "firstName": "Jane", "lastName": "Doe", "temporaryPassword": "Temp123!@#"}Roles (/api/admin/roles)
Section titled “Roles (/api/admin/roles)”| Method | Path | Permission | Description |
|---|---|---|---|
GET | / | Roles.Read | List all roles with descriptions |
POST | / | Roles.Create | Create a new role |
DELETE | /{roleName} | Roles.Delete | Delete role (409 if has members) |
GET | /{roleName}/members | Roles.Read | Paginated list of role members |
Groups (/api/admin/groups)
Section titled “Groups (/api/admin/groups)”| Method | Path | Permission | Description |
|---|---|---|---|
GET | / | Groups.Read | Paginated list for current tenant |
POST | / | Groups.Create | Create group (409 on duplicate name within tenant) |
DELETE | /{groupId} | Groups.Delete | Delete group + cascade memberships |
POST | /{groupId}/members | Groups.Manage | Add user to group |
DELETE | /{groupId}/members/{userId} | Groups.Manage | Remove user from group |
OIDC Applications (/api/admin/oidc/applications)
Section titled “OIDC Applications (/api/admin/oidc/applications)”| Method | Path | Permission | Description |
|---|---|---|---|
GET | / | Applications.Read | List all registered OIDC clients |
POST | / | Applications.Create | Register a new OIDC application |
DELETE | /{clientId} | Applications.Delete | Delete app + authorizations + tokens |
POST | /{clientId}/rotate-secret | Applications.Rotate | Rotate client secret (returns plaintext once) |
Create application request:
{ "clientId": "my-spa", "clientSecret": null, "displayName": "My SPA (PKCE)", "permissions": ["ept:authorization", "ept:token", "gt:authorization_code"], "redirectUris": ["https://app.example.com/callback"], "postLogoutRedirectUris": ["https://app.example.com"]}Application response: { id, clientId, displayName, type }
Secret rotation response: { clientId, newSecret } — the new secret is returned
once only in the response. It is hashed before storage and cannot be retrieved later.
OIDC Scopes (/api/admin/oidc/scopes)
Section titled “OIDC Scopes (/api/admin/oidc/scopes)”| Method | Path | Permission | Description |
|---|---|---|---|
GET | / | Scopes.Read | List all scopes with resources |
POST | / | Scopes.Create | Register a new scope (409 if name exists) |
DELETE | /{scopeName} | Scopes.Delete | Delete scope (404 if not found) |
Create scope request:
{ "name": "api", "displayName": "API Access", "resources": ["my-api"] }Scope response: { id, name, displayName }
OIDC Authorizations (/api/admin/oidc/authorizations)
Section titled “OIDC Authorizations (/api/admin/oidc/authorizations)”| Method | Path | Permission | Description |
|---|---|---|---|
GET | /?userId=&clientId= | Authorizations.Read | List authorizations (filterable) |
DELETE | /{authorizationId} | Authorizations.Revoke | Revoke single authorization + tokens |
DELETE | /user/{userId} | Authorizations.Revoke | Revoke all for user (GDPR / security incident) |
Authorization response: { id, subject, status, type }
The DELETE /user/{userId} endpoint iterates all authorizations for the subject and
revokes each one — used for GDPR erasure requests and security incident response.
Database tables
Section titled “Database tables”All tables are prefixed with openiddict_ (configurable via GranitOpenIddictDbProperties.DbTablePrefix).
The schema is configurable via GranitOpenIddictDbProperties.DbSchema.
| Table | Entity | Multi-tenant |
|---|---|---|
openiddict_users | GranitUser | Yes |
openiddict_roles | GranitRole | No (global) |
openiddict_user_roles | IdentityUserRole<Guid> | — |
openiddict_user_claims | IdentityUserClaim<Guid> | — |
openiddict_user_logins | IdentityUserLogin<Guid> | — |
openiddict_user_tokens | IdentityUserToken<Guid> | — |
openiddict_role_claims | IdentityRoleClaim<Guid> | — |
openiddict_user_groups | GranitUserGroup | Yes |
openiddict_user_group_members | GranitUserGroupMember | Yes |
openiddict_applications | GranitOpenIddictApplication | Yes |
openiddict_authorizations | GranitOpenIddictAuthorization | Yes |
openiddict_scopes | GranitOpenIddictScope | Yes |
openiddict_tokens | GranitOpenIddictToken | Yes |
Notable indexes
Section titled “Notable indexes”| Index | Type | Columns |
|---|---|---|
ix_openiddict_users_tenant_id | Non-unique | TenantId |
uq_openiddict_user_groups_tenant_name | Unique | TenantId, Name |
uq_openiddict_user_group_members_group_user | Unique | GroupId, UserId |
ix_openiddict_user_group_members_user_id | Non-unique | UserId |