dotnet-cqrs/docs/core-features/queries/query-authorization.md

3.5 KiB

Query Authorization

Secure your queries with authorization services.

Interface

public interface IQueryAuthorizationService<in TQuery>
{
    Task<bool> CanExecuteAsync(
        TQuery query,
        ClaimsPrincipal user,
        CancellationToken cancellationToken = default);
}

Basic Authorization

Authenticated Users Only

public class GetUserAuthorizationService : IQueryAuthorizationService<GetUserQuery>
{
    public Task<bool> CanExecuteAsync(
        GetUserQuery query,
        ClaimsPrincipal user,
        CancellationToken cancellationToken)
    {
        return Task.FromResult(user.Identity?.IsAuthenticated == true);
    }
}

// Registration
builder.Services.AddScoped<IQueryAuthorizationService<GetUserQuery>, GetUserAuthorizationService>();

Resource-Based Authorization

Own Data Only

public class GetUserAuthorizationService : IQueryAuthorizationService<GetUserQuery>
{
    public Task<bool> CanExecuteAsync(
        GetUserQuery query,
        ClaimsPrincipal user,
        CancellationToken cancellationToken)
    {
        // Admins can view any user
        if (user.IsInRole("Admin"))
            return Task.FromResult(true);

        // Users can only view their own data
        var userId = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
        return Task.FromResult(query.UserId.ToString() == userId);
    }
}

Row-Level Security

public class ListOrdersAuthorizationService : IQueryAuthorizationService<ListOrdersQuery>
{
    public Task<bool> CanExecuteAsync(
        ListOrdersQuery query,
        ClaimsPrincipal user,
        CancellationToken cancellationToken)
    {
        // Ensure user can only see their own orders (enforced in query handler)
        var userId = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;

        if (string.IsNullOrEmpty(userId))
            return Task.FromResult(false);

        // Authorization passes - handler will filter by userId
        return Task.FromResult(true);
    }
}

// In handler:
public async Task<List<OrderDto>> HandleAsync(ListOrdersQuery query, CancellationToken cancellationToken)
{
    var userId = _httpContextAccessor.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;

    var orders = await _context.Orders
        .Where(o => o.UserId.ToString() == userId) // Filter by user
        .ToListAsync(cancellationToken);

    return orders.Select(MapToDto).ToList();
}

Tenant Isolation

public class GetCustomerAuthorizationService : IQueryAuthorizationService<GetCustomerQuery>
{
    public Task<bool> CanExecuteAsync(
        GetCustomerQuery query,
        ClaimsPrincipal user,
        CancellationToken cancellationToken)
    {
        var tenantId = user.FindFirst("TenantId")?.Value;

        if (string.IsNullOrEmpty(tenantId))
            return Task.FromResult(false);

        // Authorization passes - handler will filter by tenant
        return Task.FromResult(true);
    }
}

Best Practices

DO

  • Check resource ownership
  • Validate tenant isolation
  • Use for access control
  • Log authorization failures
  • Return boolean (true/false)

DON'T

  • Don't throw exceptions
  • Don't perform business logic
  • Don't modify data
  • Don't bypass framework checks

HTTP Responses

  • 401 Unauthorized - User not authenticated
  • 403 Forbidden - User authenticated but not authorized

See Also