Current User¶
ICurrentUser is the enriched per-request user context — it includes resolved roles and flattened permissions from the RBAC system. Built by IdentityMiddleware on every request, it's the bridge between authentication (JWT claims) and authorization (permission checks).
ICurrentUser¶
public interface ICurrentUser
{
string UserId { get; }
string Email { get; }
string Name { get; }
IReadOnlySet<string> Roles { get; }
IReadOnlySet<int> Permissions { get; }
bool HasPermission<TPermission>(TPermission permission) where TPermission : Enum;
bool HasRole(string roleName);
string? TenantId { get; }
bool IsAuthenticated { get; }
}
Unlike IUserContext (which reads JWT claims directly), ICurrentUser includes the full RBAC state: which roles the user holds, and which permissions those roles grant.
Usage in Handlers¶
ICurrentUser is not effects-based — inject it directly or access it through IServiceProvider:
// Use HasPermission for programmatic checks
if (!currentUser.HasPermission(Permission.Contacts_Delete))
return FailEff<Unit>(Error.New(403, "Insufficient permissions"));
For declarative access control on handlers, use [Authorize] and [Require] instead — see Authorization.
IdentityMiddleware¶
IdentityMiddleware runs after ASP.NET's UseAuthentication() / UseAuthorization() and builds ICurrentUser per-request:
- Reads
UserIdfromCorrelationContext(set by prior auth middleware) - If unauthenticated: registers
CurrentUser.Anonymous - If authenticated:
- Calls
IIdentityStore.GetRolesAsync(userId)to resolve role assignments - Calls
IPermissionResolver.ResolvePermissions(roleNames)to flatten roles into permissions - Reads email/name from
HttpContext.Userclaims - Constructs
CurrentUserwith the full RBAC state
- Calls
UseAuthentication() → sets ClaimsPrincipal from JWT
UseAuthorization() → runs ASP.NET authorization
IdentityMiddleware → builds ICurrentUser from IIdentityStore + IPermissionResolver
IIdentityStore¶
The persistence interface for user identity and role management:
public interface IIdentityStore
{
Task<UserIdentity?> GetAsync(string userId, CancellationToken ct = default);
Task<IReadOnlyList<RoleAssignment>> GetRolesAsync(string userId, CancellationToken ct = default);
Task AssignRoleAsync(string userId, string roleName, CancellationToken ct = default);
Task RevokeRoleAsync(string userId, string roleName, CancellationToken ct = default);
}
Supporting records:
public sealed record UserIdentity(
string UserId, string Email, string Name,
IReadOnlyList<RoleAssignment> Roles, DateTimeOffset CreatedAt);
public sealed record RoleAssignment(
string RoleName, DateTimeOffset AssignedAt, string? AssignedBy);
The default InMemoryIdentityStore is [DevelopmentOnly]. Production implementations persist to your data store.
IPermissionResolver¶
Resolves a set of role names into a flattened set of permission integers:
public interface IPermissionResolver
{
IReadOnlySet<int> ResolvePermissions(IReadOnlySet<string> roles);
}
When you define [Permissions] and [Roles], the generator creates a GeneratedPermissionResolver that maps role names to their granted permissions. Without RBAC attributes, NullPermissionResolver.Instance returns an empty set.
User Entity Conventions¶
When using IdentityModule<TUser>, the generator inspects your user entity's properties:
| Property | Type | Effect |
|---|---|---|
Email |
string |
Required — login identifier |
PasswordHash |
string |
Enables local auth (login/register) endpoints |
GoogleSub or GoogleId |
string |
Enables Google OAuth endpoints |
Roles |
IReadOnlySet<string> |
Enables role management |
Diagnostics¶
| ID | Severity | Description | Code Fix |
|---|---|---|---|
| DSID02 | Info | User entity has no PasswordHash — local auth endpoints disabled |
— |
| DSID03 | Info | User entity has no Roles property — role management disabled |
Add Roles property |
| DSID07 | Info | User entity has neither PasswordHash nor GoogleSub/GoogleId |
— |