Query Authorizers
Query authorizers are the read-side security gate in PearDrop. They run before query handlers and prevent unauthorized reads.
Why query authorizers matter
Even if your query only reads data, it still needs authorization checks:
- Prevent users from reading data outside their role
- Prevent cross-tenant data access
- Enforce resource-level read permissions
Execution order
For query requests, pipeline order is:
- Query Authorizer (
IAuthorizationHandler<TQuery>or ignorable authorizer) - Query Handler (
IQueryProcessor<TQuery, TResult>)
If authorization fails, the handler never runs.
File location
By convention, place query authorizers here:
YourApp.App.Client/
└── Contracts/
└── Queries/
└── GetEquipmentByIdQuery.cs
YourApp.App/
└── Infrastructure/
└── Queries/
├── QueryHandlers/
│ └── GetEquipmentByIdQueryHandler.cs
└── QueryAuthorizers/
└── GetEquipmentByIdQueryAuthorizer.cs
The query contract is defined in the client project and used by server-side query authorizers and handlers.
Pattern 1: Policy requirements authorizer (CLI default)
The CLI-generated pattern uses requirement-based authorization:
using Microsoft.Extensions.Options;
using PearDrop.AuthorizationRequirements;
using PearDrop.Domain;
using PearDrop.Settings;
using MyApp.App.Client.Contracts.Queries;
namespace MyApp.Infrastructure.Queries.QueryAuthorizers;
public class GetEquipmentByIdQueryAuthorizer(IOptions<CoreSettings> coreSettings)
: AbstractIgnorableRequestAuthorizer<GetEquipmentByIdQuery>(coreSettings)
{
public override void BuildIgnorablePolicy(GetEquipmentByIdQuery request)
{
this.UseRequirement(new MustBeAuthenticatedRequirement());
// Optional role requirement
// this.UseRequirement(new MustHaveRoleRequirement("EquipmentViewer"));
}
}
Use this when you want standardized policy/requirement checks and feature toggles around authorization behavior.
Pattern 2: Explicit HTTP context checks
For request-specific checks (ownership/tenant/resource scope), use explicit checks:
using MediatR.Behaviors.Authorization;
using PearDrop.Extensions;
using MyApp.App.Client.Contracts.Queries;
namespace MyApp.Infrastructure.Queries.QueryAuthorizers;
public sealed class GetMyProfileQueryAuthorizer : IAuthorizationHandler<GetMyProfileQuery>
{
private readonly IHttpContextAccessor httpContextAccessor;
private readonly ILogger<GetMyProfileQueryAuthorizer> logger;
public GetMyProfileQueryAuthorizer(
IHttpContextAccessor httpContextAccessor,
ILogger<GetMyProfileQueryAuthorizer> logger)
{
this.httpContextAccessor = httpContextAccessor;
this.logger = logger;
}
public Task Handle(GetMyProfileQuery request, CancellationToken cancellationToken)
{
var httpContext = this.httpContextAccessor.HttpContext;
if (httpContext == null)
{
throw new UnauthorizedAccessException("No HTTP context");
}
var userMaybe = httpContext.ToSystemUser();
if (userMaybe.HasNoValue)
{
this.logger.LogWarning("Unauthenticated query access attempt");
throw new UnauthorizedAccessException("Must be authenticated");
}
return Task.CompletedTask;
}
}
Tenant-aware query checks
For multi-tenant reads, block cross-tenant access in the authorizer:
var user = httpContext.User;
var tenantClaim = user.FindFirst("TenantId")?.Value;
if (!Guid.TryParse(tenantClaim, out var userTenantId))
{
throw new UnauthorizedAccessException("Unable to determine tenant");
}
if (request.TenantId != userTenantId)
{
throw new UnauthorizedAccessException("Cross-tenant query not allowed");
}
What belongs in query authorizer vs query handler
- Authorizer: authN/authZ decisions (user identity, roles, tenant scope, access rights)
- Handler: projection query logic (filters, sort, pagination, shape)
Keep security checks out of handler logic where possible.
Registration
Ensure authorizers are registered in the module:
services.AddAuthorizersFromAssembly(typeof(Module).Assembly);
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Module).Assembly));
Client/Server split summary
- Client project: query contracts (
IQuery, result records) - Server project: query authorizers, query handlers, read model access
This is the same contract-execution split used on the write side for commands.
Common mistakes
- Missing authorizer registration: query runs without security gate
- Performing expensive DB work in authorizer: keep checks fast
- Mixing security and projection shaping in the same class