Skip to main content

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:

  1. Query Authorizer (IAuthorizationHandler<TQuery> or ignorable authorizer)
  2. 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