Skip to content

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.

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
}

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.

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

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

All four OpenIddict entities are extended with IMultiTenant for tenant isolation:

EntityBase classTable
GranitOpenIddictApplicationOpenIddictEntityFrameworkCoreApplication<Guid, ...>openiddict_applications
GranitOpenIddictAuthorizationOpenIddictEntityFrameworkCoreAuthorization<Guid, ...>openiddict_authorizations
GranitOpenIddictScopeOpenIddictEntityFrameworkCoreScope<Guid>openiddict_scopes
GranitOpenIddictTokenOpenIddictEntityFrameworkCoreToken<Guid, ...>openiddict_tokens

Applications and scopes with TenantId == null are global (visible to all tenants).

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:

InterfaceImplementation detail
IIdentityUserReaderUserManager<GranitUser>.Users LINQ queries
IIdentityUserWriterUserManager.CreateAsync(), UpdateAsync()
IIdentityRoleManagerRoleManager<GranitRole> operations
IIdentityGroupManagerDirect EF Core queries on GranitUserGroup
IIdentitySessionManagerNo-op (SupportsIndividualSessionTermination = false)
IIdentityPasswordManagerUserManager.ResetPasswordAsync()
IIdentityCredentialVerifierUserManager.CheckPasswordAsync()

AspNetIdentityProviderCapabilities declares what this provider supports:

ProviderName = "AspNetIdentity"
SupportsIndividualSessionTermination = false
SupportsNativePasswordResetEmail = false
SupportsGroupHierarchy = false
SupportsCustomAttributes = true
MaxCustomAttributes = int.MaxValue
SupportsCredentialVerification = true
SupportsUserCreation = true

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;

All admin endpoints are under /api/admin (configurable via OpenIddictEndpointsOptions.AdminRoutePrefix) and require authorization. Permission constants are defined in OpenIddictPermissions.

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

MethodPathPermissionDescription
GET/Users.ReadPaginated list (?search, ?page, ?pageSize)
GET/{userId}Users.ReadUser detail with roles and groups
POST/Users.CreateAdmin-initiated user creation (no email confirmation)
DELETE/{userId}Users.DeleteSoft-delete + token revocation
POST/{userId}/impersonateUsers.ImpersonateIssue impersonation token

Create user request:

{
"email": "[email protected]",
"firstName": "Jane",
"lastName": "Doe",
"temporaryPassword": "Temp123!@#"
}
MethodPathPermissionDescription
GET/Roles.ReadList all roles with descriptions
POST/Roles.CreateCreate a new role
DELETE/{roleName}Roles.DeleteDelete role (409 if has members)
GET/{roleName}/membersRoles.ReadPaginated list of role members
MethodPathPermissionDescription
GET/Groups.ReadPaginated list for current tenant
POST/Groups.CreateCreate group (409 on duplicate name within tenant)
DELETE/{groupId}Groups.DeleteDelete group + cascade memberships
POST/{groupId}/membersGroups.ManageAdd user to group
DELETE/{groupId}/members/{userId}Groups.ManageRemove user from group

OIDC Applications (/api/admin/oidc/applications)

Section titled “OIDC Applications (/api/admin/oidc/applications)”
MethodPathPermissionDescription
GET/Applications.ReadList all registered OIDC clients
POST/Applications.CreateRegister a new OIDC application
DELETE/{clientId}Applications.DeleteDelete app + authorizations + tokens
POST/{clientId}/rotate-secretApplications.RotateRotate 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.

MethodPathPermissionDescription
GET/Scopes.ReadList all scopes with resources
POST/Scopes.CreateRegister a new scope (409 if name exists)
DELETE/{scopeName}Scopes.DeleteDelete 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)”
MethodPathPermissionDescription
GET/?userId=&clientId=Authorizations.ReadList authorizations (filterable)
DELETE/{authorizationId}Authorizations.RevokeRevoke single authorization + tokens
DELETE/user/{userId}Authorizations.RevokeRevoke 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.

All tables are prefixed with openiddict_ (configurable via GranitOpenIddictDbProperties.DbTablePrefix). The schema is configurable via GranitOpenIddictDbProperties.DbSchema.

TableEntityMulti-tenant
openiddict_usersGranitUserYes
openiddict_rolesGranitRoleNo (global)
openiddict_user_rolesIdentityUserRole<Guid>
openiddict_user_claimsIdentityUserClaim<Guid>
openiddict_user_loginsIdentityUserLogin<Guid>
openiddict_user_tokensIdentityUserToken<Guid>
openiddict_role_claimsIdentityRoleClaim<Guid>
openiddict_user_groupsGranitUserGroupYes
openiddict_user_group_membersGranitUserGroupMemberYes
openiddict_applicationsGranitOpenIddictApplicationYes
openiddict_authorizationsGranitOpenIddictAuthorizationYes
openiddict_scopesGranitOpenIddictScopeYes
openiddict_tokensGranitOpenIddictTokenYes
IndexTypeColumns
ix_openiddict_users_tenant_idNon-uniqueTenantId
uq_openiddict_user_groups_tenant_nameUniqueTenantId, Name
uq_openiddict_user_group_members_group_userUniqueGroupId, UserId
ix_openiddict_user_group_members_user_idNon-uniqueUserId