Skip to content
fullstackhero

Reference

Identity module

JWT bearer + refresh tokens, ASP.NET Identity with roles + permissions, user groups, operator impersonation, two-factor TOTP, sessions, and password-policy enforcement.

views 0 Last updated

The Identity module is fullstackhero’s load-bearing auth layer. It wraps ASP.NET Identity with JWT bearer + refresh, a flat-but-granular permission system enforced via .RequirePermission() on endpoints, user groups, operator impersonation with a full audit trail, two-factor TOTP, per-device sessions, password history, and an outbox-backed integration event publisher for things like user registration and password changes. Around 8,800 lines of code across 195 files, it’s the most substantial module in the kit.

What ships in v10

  • JWT bearer + refresh tokens with a rotating refresh on each POST /tokens/refresh.
  • ASP.NET Identity on top of FshUser : IdentityUser and FshRole : IdentityRole — custom password policy, 5-attempt lockout for 15 minutes, custom claims, passkey table.
  • Permission-based authorization via a flat registry (PermissionConstants) and the .RequirePermission(...) fluent helper on endpoints.
  • User groups — organise users; groups own roles (GroupRoles) and propagate permissions to members.
  • Operator impersonation — a SuperAdmin can issue a time-bound impersonation grant against another user (cross-tenant supported via IGlobalEntity), with revocation list and an audit trail.
  • Two-factor TOTP — enrol with a QR code, verify, disable per user. Optional, not enforced.
  • Sessions — per-device records with IP, user-agent, OS, browser, and revocation; cleanup runs as a hosted service.
  • Password policy — history (default 5), expiry (default 90 days) with warning window (14 days), enforced via IPasswordHistoryService + IPasswordExpiryService.
  • 48 endpoints under /api/v1/identity/... covering tokens, users, roles, groups, sessions, impersonation, and 2FA. Rate-limited auth policy applied to login, refresh, confirm-email, forgot-password, reset-password, and self-register.
  • Outbox-backed integration eventsUserRegisteredIntegrationEvent and TokenGeneratedIntegrationEvent go out via the Eventing module’s outbox dispatcher.

Architecture at a glance

src/Modules/Identity/
├── Modules.Identity/ ~8,800 LoC, 195 files
│ ├── IdentityModule.cs IModule entry — order 100
│ ├── Domain/
│ │ ├── FshUser.cs IdentityUser + IHasDomainEvents
│ │ ├── FshRole.cs IdentityRole + Description
│ │ ├── Group.cs IAuditable, ISoftDeletable
│ │ ├── ImpersonationGrant.cs IGlobalEntity (cross-tenant)
│ │ ├── UserSession.cs IHasDomainEvents
│ │ └── PasswordHistory.cs
│ ├── Data/IdentityDbContext.cs MultiTenantIdentityDbContext
│ ├── Features/v1/ 48 features in 11 areas
│ │ ├── Tokens/ Generate, Refresh
│ │ ├── Users/ Register + 14 mgmt features
│ │ ├── Roles/ 6 features
│ │ ├── Groups/ 8 features
│ │ ├── Sessions/ 7 features
│ │ ├── Impersonation/ 4 features
│ │ └── TwoFactor/ 3 features
│ └── Services/ Split by responsibility
│ ├── ITokenService IssueAsync, IssueAccessOnlyAsync
│ ├── ICurrentUserService ICurrentUser implementation
│ ├── IUserRegistrationService User creation
│ ├── IUserPasswordService Change/reset/expiry checks
│ ├── IUserPermissionService Role/group permission aggregation
│ ├── ISessionService Session CRUD + cleanup
│ └── IImpersonationGrantService Grant + revoke
└── Modules.Identity.Contracts/ ~1,000 LoC public commands/queries/services

The module loads at order 100 — the first module — so every later module can rely on ICurrentUser, the JWT pipeline, and the permission registry being already in place.

The token pipeline

Login goes through POST /api/v1/identity/tokens/generate. The handler validates email/password through UserManager, checks 2FA if enrolled, mints an access token + refresh token via ITokenService, opens a UserSession, and publishes a TokenGeneratedIntegrationEvent to the outbox.

// src/Modules/Identity/Modules.Identity.Contracts/v1/Tokens/TokenGeneration/
public sealed record GenerateTokenCommand(
string Email,
string Password,
string? TwoFactorCode = null)
: ICommand<TokenResponse>;
public sealed record TokenResponse(
string AccessToken,
string RefreshToken,
int ExpiresInSeconds,
string TokenType = "Bearer");
public sealed record RefreshTokenCommand(
string RefreshToken)
: ICommand<RefreshTokenCommandResponse>;

The access token carries the standard claims plus tenant (resolved via Finbuckle on the request that issued it) and the user’s permission set. Permission enforcement on every other endpoint is just:

endpoints.MapPost("/users", handler)
.RequirePermission(IdentityPermissions.Users.Create);

PathAwareAuthorizationHandler lets you mix anonymous endpoints (the login itself, forgot-password, confirm-email) with permission-gated ones without separate policies per endpoint.

Public API highlights

48 endpoints means a lot of contracts. Major ones:

Tokens

TypePurpose
GenerateTokenCommand(email, password, twoFactorCode?)Login
RefreshTokenCommand(refreshToken)Rotate the refresh token, mint a new access token

Users

TypePurpose
RegisterUserCommandOperator-driven user creation; requires Users.Create
SelfRegisterUserCommandAnonymous self-registration (rate-limited)
ConfirmEmailCommand(userId, token)Email confirmation step
ForgotPasswordCommand(email) → tokenIssue reset token
ResetPasswordCommand(email, token, password)Apply reset; respects history
ChangePasswordCommand(userId, old, new)Self-service change
ToggleUserStatusCommand(userId, isActive)Activate / deactivate user
AssignUserRolesCommand(userId, roleIds[])Replace user’s role set
SetProfileImageCommand(userId, imageUrl)Profile image
GetMeQuery()Current user with permissions

Roles

TypePurpose
UpsertRoleCommand(name, description)Create or rename
UpdateRolePermissionsCommand(roleId, permissions[])Set the role’s permission set
GetRolePermissionsQuery(roleId)Fetch current permissions

Sessions

GetMySessionsQuery, RevokeSessionCommand, RevokeAllSessionsCommand, plus admin variants (GetTenantSessionsQuery, AdminRevokeSessionCommand) for ops.

Impersonation

StartImpersonationCommand(impersonatedUserId, durationMinutes, reason) returns an impersonation access token. EndImpersonationCommand ends the active grant. RevokeImpersonationGrantCommand(grantId, reason) is the kill switch for admins.

Two-Factor

EnrollTwoFactorCommand returns a QR code + secret; VerifyEnrollTwoFactorCommand(code) activates; DisableTwoFactorCommand turns it off.

Endpoints

All 48 endpoints are under /api/v1/identity/. The rate-limited auth policy covers POST /tokens/generate, POST /tokens/refresh, POST /users/forgot-password, POST /users/reset-password, POST /users/confirm-email, and POST /users/self-register. Full table:

VerbRouteWhat it does
POST/tokensLogin
POST/tokens/refreshRotate refresh token
GET/users/meCurrent user profile
POST/users/registerOperator-driven user creation
POST/users/self-registerAnonymous registration
POST/users/forgot-passwordIssue password-reset token
POST/users/reset-passwordReset using token
POST/users/confirm-emailConfirm email address
POST/users/{id}/change-passwordChange password
GET/usersList users
POST/users/searchSearch users
GET/users/{id}Get user by id
PUT/users/{id}Update user
DELETE/users/{id}Delete user
PATCH/users/{id}/statusActivate / deactivate
POST/users/{id}/rolesAssign roles
GET/users/{id}/rolesList user roles
GET/users/{id}/groupsList user groups
GET/users/me/permissionsCurrent user permissions
POST/users/{id}/profile-imageSet profile image
GET/rolesList roles
GET/roles/{id}Get role
POST,PUT/rolesUpsert role
DELETE/roles/{id}Delete role
GET/roles/{id}/permissionsGet role permissions
PUT/roles/{id}/permissionsUpdate role permissions
GET/groupsList groups
GET/groups/{id}Get group
POST/groupsCreate group
PUT/groups/{id}Update group
DELETE/groups/{id}Delete group
GET/groups/{id}/membersList members
POST/groups/{id}/membersAdd members
DELETE/groups/{id}/members/{userId}Remove member
GET/sessions/meMy sessions
DELETE/sessions/{id}Revoke a session
DELETE/sessions/meRevoke all my sessions
GET/sessions/tenantTenant sessions (admin)
GET/sessions/user/{userId}User sessions (admin)
DELETE/sessions/user/{userId}/{id}Admin revoke one session
DELETE/sessions/user/{userId}Admin revoke all user sessions
POST/impersonation/startStart impersonation
POST/impersonation/endEnd impersonation
GET/impersonation/grantsList grants
DELETE/impersonation/grants/{id}Revoke grant
POST/2fa/enrollEnroll 2FA
POST/2fa/verifyVerify enrolment
DELETE/2faDisable 2FA

Configuration

appsettings.json
{
"PasswordPolicy": {
"PasswordHistoryCount": 5, // prevent reuse of the last N passwords
"PasswordExpiryDays": 90, // force change cadence
"PasswordExpiryWarningDays": 14,
"EnforcePasswordExpiry": true
},
"Jwt": {
"Issuer": "fullstackhero",
"Audience": "fullstackhero-api",
"AccessTokenMinutes": 60,
"RefreshTokenDays": 14,
"SigningKey": "set via secrets manager or env var"
}
}

Lockout is configured by IdentityOptions and defaults to 5 failed attempts → 15-minute lock. The identity-outbox-dispatcher Hangfire job runs every minute to dispatch outbox events.

How to extend

Add a new permission

  1. Pick a stable resource + action pair (e.g. Users, Export).
  2. Add a constant to IdentityPermissions in the Contracts assembly.
  3. Register it through PermissionConstants.Register([...]) during module startup (Identity already does this; mirror the pattern).
  4. Apply it on the endpoint: .RequirePermission(IdentityPermissions.Users.Export).
  5. Seed roles with the permission via UpdateRolePermissionsCommand or the role-permission seeder.

Add a custom claim to the JWT

Subclass TokenService (or wrap with a decorator) and add the claim during IssueAsync. The existing user metadata service is the canonical place to source the claim value from.

The ConfirmEmailCommand flow is a token exchange. Replace the token-generation step (currently UserManager.GenerateEmailConfirmationTokenAsync) and the redeem step (ConfirmEmailAsync) to issue and accept an OTP. The endpoint surface stays the same.

Tests

  • Unit tests at src/Tests/Identity.Tests/ (33 test files) cover handlers, password policy, JWT options.
  • Integration tests at src/Tests/Integration.Tests/Tests/Impersonation/ cover the cross-tenant impersonation flow.