Skip to main content

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 users
  • UpdateUserCommandAuthorizer: Users update their own profile; admins update others
  • DeleteUserCommandAuthorizer: Only admins
  • ResetPasswordCommandAuthorizer: Unauthenticated users (forgot password), authenticated users (self)
  • UpdateEmailCommandAuthorizer: Authenticated users only

Role Management

  • CreateRoleCommandAuthorizer: Only admins
  • UpdateRoleCommandAuthorizer: Only admins
  • DeleteRoleCommandAuthorizer: Only admins
  • AssignRoleToUserCommandAuthorizer: Only admins
  • RemoveRoleFromUserCommandAuthorizer: Only admins

Email & Verification

  • ResendVerificationEmailCommandAuthorizer: Unauthenticated or unverified users
  • VerifyEmailCommandAuthorizer: 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

  1. Admin creates Resource (e.g., "edit-tasks")
  2. Admin creates Role (e.g., "TaskEditor")
  3. Admin assigns Resource to Role ("edit-tasks" → "TaskEditor")
  4. Admin assigns Role to User ("TaskEditor" → alice@example.com)
  5. 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 MustBeAuthenticatedRequirement for protected operations
  • Use MustBeInRoleRequirement for role-based gating
  • Use MustBeActiveUserRequirement for operations requiring verified users
  • Implement custom authorizers for domain-specific rules
  • Set IgnoreAuthorizations: true in 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();
}
}