Authentication Module Integration
The PearDrop Authentication module provides identity management, role-based access control (RBAC), resource-based authorization, and 40+ built-in command authorizers covering common security scenarios.
Overview
The Authentication module manages:
- Users: Identity, email, verification status
- Roles: Permission containers (e.g., Admin, Editor, Viewer)
- Resources: Feature-level access control (e.g., "edit-tasks", "approve-reports")
- Role Assignments: Map users to roles
- Resource Assignments: Map roles to resources
This separation enables:
- Basic RBAC: "Does this user have the Admin role?"
- Fine-grained access: "Can this user access the 'edit-tasks' resource?"
- Cross-cutting concerns: Centralized business rule enforcement (e.g., "only active users")
Server-Side Registration
IAuthReadModels Contract
The Auth module exposes a read-only contract for checking user permissions:
public interface IAuthReadModels : IModuleReadModels
{
/// <summary>
/// Get current user context with roles and resources.
/// Returns null if user not authenticated or not found.
/// </summary>
Task<UserContextDto?> GetCurrentUserAsync(CancellationToken cancellationToken);
/// <summary>
/// Check if user has a specific role.
/// </summary>
Task<bool> UserHasRoleAsync(Guid userId, string roleName, CancellationToken cancellationToken);
/// <summary>
/// Get all resources assigned to a user (via their roles).
/// </summary>
Task<IEnumerable<string>> GetResourcesForUserAsync(Guid userId, CancellationToken cancellationToken);
/// <summary>
/// Check if a user is active (verified, not locked, not deleted).
/// </summary>
Task<bool> UserIsActiveAsync(Guid userId, CancellationToken cancellationToken);
}
Injecting Auth Read Models:
public sealed class MyQueryHandler(
IAuthReadModels authReadModels,
IMyModuleReadModels myModuleReadModels)
{
public async Task<QueryResult<MyDto>> Handle(
MyQuery request,
CancellationToken ct)
{
// Check if user is active
var userContext = await authReadModels.GetCurrentUserAsync(ct);
if (userContext == null)
{
return QueryResult<MyDto>.Failed("User not authenticated");
}
// Perform query
var data = await myModuleReadModels.MyEntities
.ToListAsync(ct);
return QueryResult<MyDto>.Succeeded(data);
}
}
Built-In Authorization Requirements
The Auth module provides 4 fundamental requirements for command/query authorization:
MustBeAuthenticatedRequirement
User must be authenticated (logged in).
public sealed class MyCommandAuthorizer :
AbstractIgnorableRequestAuthorizer<MyCommand>
{
public override IEnumerable<IAuthorizationRequirement> GetRequirements(MyCommand request)
{
return new[] { new MustBeAuthenticatedRequirement() };
}
}
MustNotBeAuthenticatedRequirement
User must NOT be logged in (useful for login/register pages).
public sealed class RegisterCommandAuthorizer :
AbstractIgnorableRequestAuthorizer<RegisterCommand>
{
public override IEnumerable<IAuthorizationRequirement> GetRequirements(RegisterCommand request)
{
// Can only register if not already logged in
return new[] { new MustNotBeAuthenticatedRequirement() };
}
}
MustBeInRoleRequirement
User must have a specific role.
public sealed class ApproveTaskAuthorizer :
AbstractIgnorableRequestAuthorizer<ApproveTaskCommand>
{
public override IEnumerable<IAuthorizationRequirement> GetRequirements(ApproveTaskCommand request)
{
return new[] { new MustBeInRoleRequirement("Manager") };
}
}
Multiple roles (OR logic):
public override IEnumerable<IAuthorizationRequirement> GetRequirements(DeleteUserCommand request)
{
// User must be in ANY of these roles
return new[] { new MustBeInRoleRequirement("Admin", "SuperUser") };
}
MustBeActiveUserRequirement
User must be active (verified email, not locked out, account not deleted).
public sealed class UpdateProfileAuthorizer :
AbstractIgnorableRequestAuthorizer<UpdateProfileCommand>
{
public override IEnumerable<IAuthorizationRequirement> GetRequirements(UpdateProfileCommand request)
{
return new[]
{
new MustBeAuthenticatedRequirement(),
new MustBeActiveUserRequirement()
};
}
}
Built-In Command Authorizers
The Auth module includes 40+ pre-built command authorizers for common scenarios:
User Management
CreateUserCommandAuthorizer: Only admins can create usersUpdateUserCommandAuthorizer: Users update their own profile; admins update othersDeleteUserCommandAuthorizer: Only adminsResetPasswordCommandAuthorizer: Unauthenticated users (forgot password), authenticated users (self)UpdateEmailCommandAuthorizer: Authenticated users only
Role Management
CreateRoleCommandAuthorizer: Only adminsUpdateRoleCommandAuthorizer: Only adminsDeleteRoleCommandAuthorizer: Only adminsAssignRoleToUserCommandAuthorizer: Only adminsRemoveRoleFromUserCommandAuthorizer: Only admins
Email & Verification
ResendVerificationEmailCommandAuthorizer: Unauthenticated or unverified usersVerifyEmailCommandAuthorizer: Unauthenticated or unverified users
All authorizers are automatically registered and honored in the command pipeline.
Custom Authorization with ToSystemUser()
For authorization logic that needs the current user context:
public sealed class TransferTaskAuthorizer :
AbstractIgnorableRequestAuthorizer<TransferTaskCommand>
{
private readonly IAuthReadModels authReadModels;
public TransferTaskAuthorizer(IAuthReadModels authReadModels)
{
this.authReadModels = authReadModels;
}
public override async Task<AuthorizationResult> AuthorizeAsync(
TransferTaskCommand request,
ClaimsPrincipal? user,
IAuthorizationService authService,
CancellationToken cancellationToken)
{
// Convert ClaimsPrincipal to application user context
var userContext = await this.authReadModels.GetCurrentUserAsync(cancellationToken);
if (userContext == null)
{
return AuthorizationResult.Failed();
}
// Check: user has Manager role
if (!userContext.Roles.Contains("Manager"))
{
return AuthorizationResult.Failed();
}
// Logic: can only transfer to active users
var canTransferTo = await this.authReadModels.UserIsActiveAsync(
request.TargetUserId,
cancellationToken);
return canTransferTo
? AuthorizationResult.Success()
: AuthorizationResult.Failed();
}
}
Blazor Component Authorization with Resources
Use [RequireResource] attribute to protect Blazor components by resource ID:
@* Only users with "edit-tasks" resource can see this button *@
<button @onclick="HandleEditClick" class="btn btn-primary">
<span @* Show only if user has required resource *@>
Edit Task
</span>
</button>
@code {
[CascadingParameter(Name = "UserResources")]
public IEnumerable<string> UserResources { get; set; } = Enumerable.Empty<string>();
private async Task HandleEditClick()
{
if (!UserResources.Contains("edit-tasks"))
{
// Handle unauthorized access
return;
}
// Proceed with edit logic
}
}
Cascading User Resources:
@* In Layout.razor *@
<CascadingValue Name="UserResources" Value="UserResources">
<Routes />
</CascadingValue>
@code {
private IEnumerable<string> UserResources { get; set; } = Enumerable.Empty<string>();
protected override async Task OnInitializedAsync()
{
var userContext = await AuthReadModels.GetCurrentUserAsync(CancellationToken.None);
if (userContext != null)
{
UserResources = userContext.Resources;
}
}
}
Resource-Based Authorization
Resources are feature-level access control distinct from roles:
Resource Definition
A resource is a string identifier like "edit-tasks" or "approve-reports".
Resource Assignment Flow
- Admin creates Resource (e.g., "edit-tasks")
- Admin creates Role (e.g., "TaskEditor")
- Admin assigns Resource to Role ("edit-tasks" → "TaskEditor")
- Admin assigns Role to User ("TaskEditor" → alice@example.com)
- Result: alice can perform operations requiring "edit-tasks" resource
Checking Resources in Queries/Commands
public sealed class GetAuditLogQueryAuthorizer :
AbstractIgnorableRequestAuthorizer<GetAuditLogQuery>
{
private readonly IAuthReadModels authReadModels;
public GetAuditLogQueryAuthorizer(IAuthReadModels authReadModels)
{
this.authReadModels = authReadModels;
}
public override async Task<AuthorizationResult> AuthorizeAsync(
GetAuditLogQuery request,
ClaimsPrincipal? user,
IAuthorizationService authService,
CancellationToken cancellationToken)
{
// Only users with "view-audit-log" resource
var userResources = await this.authReadModels.GetResourcesForUserAsync(
user.GetUserId(),
cancellationToken);
return userResources.Contains("view-audit-log")
? AuthorizationResult.Success()
: AuthorizationResult.Failed();
}
}
Development Mode: Bypassing Authorization
For local testing, disable authorization checks:
appsettings.Development.json:
{
"PearDrop": {
"IgnoreAuthorizations": true
}
}
Per-command override:
public sealed class MyCommandAuthorizer :
AbstractIgnorableRequestAuthorizer<MyCommand>
{
public override bool IgnoreAuthorization => true; // Skip this check in dev/testing
public override IEnumerable<IAuthorizationRequirement> GetRequirements(MyCommand request)
{
return Array.Empty<IAuthorizationRequirement>();
}
}
Integration Checklist
When using Auth module in your application:
- Register Auth module:
builder.Services.AddPearDropAuthentication(config) - Register Auth read models:
builder.Services.AddAuthReadModels() - Create command authorizers in
Infrastructure/Domain/{Aggregate}/CommandAuthorizers/ - Create query authorizers in
Infrastructure/Queries/QueryAuthorizers/ - Use
MustBeAuthenticatedRequirementfor protected operations - Use
MustBeInRoleRequirementfor role-based gating - Use
MustBeActiveUserRequirementfor operations requiring verified users - Implement custom authorizers for domain-specific rules
- Set
IgnoreAuthorizations: truein appsettings.Development.json for local dev - Use
[RequireResource]in Blazor components for UI-level gating
Common Patterns
Admin-Only Operation
public sealed class DeleteTenantAuthorizer :
AbstractIgnorableRequestAuthorizer<DeleteTenantCommand>
{
public override IEnumerable<IAuthorizationRequirement> GetRequirements(DeleteTenantCommand request)
{
return new[]
{
new MustBeAuthenticatedRequirement(),
new MustBeInRoleRequirement("GlobalAdmin")
};
}
}
User Self-Edit with Admin Override
public sealed class UpdateProfileAuthorizer :
AbstractIgnorableRequestAuthorizer<UpdateProfileCommand>
{
private readonly IAuthReadModels authReadModels;
public UpdateProfileAuthorizer(IAuthReadModels authReadModels)
{
this.authReadModels = authReadModels;
}
public override async Task<AuthorizationResult> AuthorizeAsync(
UpdateProfileCommand request,
ClaimsPrincipal? user,
IAuthorizationService authService,
CancellationToken cancellationToken)
{
var userId = user.GetUserId();
// Admin can update anyone
var isAdmin = await this.authReadModels.UserHasRoleAsync(userId, "Admin", cancellationToken);
if (isAdmin)
{
return AuthorizationResult.Success();
}
// Non-admins can only update themselves
return request.UserId == userId
? AuthorizationResult.Success()
: AuthorizationResult.Failed();
}
}
Related Topics
- Security & Authorization Patterns — Full authorization architecture and patterns
- Framework Core Integration — Server/client registration
- Multi-Tenancy — Tenant isolation with auth