4.4 KiB
4.4 KiB
Command Authorization
Secure your commands with authorization services.
Overview
Command authorization controls who can execute commands using the ICommandAuthorizationService<TCommand> interface.
Interface
public interface ICommandAuthorizationService<in TCommand>
{
Task<bool> CanExecuteAsync(
TCommand command,
ClaimsPrincipal user,
CancellationToken cancellationToken = default);
}
Basic Authorization
Authenticated Users Only
public class CreateUserAuthorizationService : ICommandAuthorizationService<CreateUserCommand>
{
public Task<bool> CanExecuteAsync(
CreateUserCommand command,
ClaimsPrincipal user,
CancellationToken cancellationToken)
{
return Task.FromResult(user.Identity?.IsAuthenticated == true);
}
}
// Registration
builder.Services.AddScoped<ICommandAuthorizationService<CreateUserCommand>, CreateUserAuthorizationService>();
Role-Based Authorization
public class DeleteUserAuthorizationService : ICommandAuthorizationService<DeleteUserCommand>
{
public Task<bool> CanExecuteAsync(
DeleteUserCommand command,
ClaimsPrincipal user,
CancellationToken cancellationToken)
{
return Task.FromResult(user.IsInRole("Admin"));
}
}
Advanced Authorization
Resource-Based Authorization
public class UpdateUserAuthorizationService : ICommandAuthorizationService<UpdateUserCommand>
{
private readonly IUserRepository _userRepository;
public UpdateUserAuthorizationService(IUserRepository userRepository)
{
_userRepository = userRepository;
}
public async Task<bool> CanExecuteAsync(
UpdateUserCommand command,
ClaimsPrincipal user,
CancellationToken cancellationToken)
{
// Admins can update any user
if (user.IsInRole("Admin"))
return true;
// Users can only update their own profile
var userId = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
return command.UserId.ToString() == userId;
}
}
Claims-Based Authorization
public class DeleteOrderAuthorizationService : ICommandAuthorizationService<DeleteOrderCommand>
{
private readonly IOrderRepository _orderRepository;
public async Task<bool> CanExecuteAsync(
DeleteOrderCommand command,
ClaimsPrincipal user,
CancellationToken cancellationToken)
{
var order = await _orderRepository.GetByIdAsync(command.OrderId, cancellationToken);
if (order == null)
return false;
// Check if user has required claim
var canDelete = user.HasClaim("Permission", "DeleteOrder");
if (!canDelete)
return false;
// Check if user owns the order
var userId = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
return order.UserId.ToString() == userId;
}
}
Multi-Tenant Authorization
public class CreateProductAuthorizationService : ICommandAuthorizationService<CreateProductCommand>
{
public Task<bool> CanExecuteAsync(
CreateProductCommand command,
ClaimsPrincipal user,
CancellationToken cancellationToken)
{
var userTenantId = user.FindFirst("TenantId")?.Value;
if (string.IsNullOrEmpty(userTenantId))
return Task.FromResult(false);
// Ensure command is for user's tenant
return Task.FromResult(command.TenantId == userTenantId);
}
}
HTTP Responses
Unauthorized (401)
When user is not authenticated:
HTTP/1.1 401 Unauthorized
Forbidden (403)
When user is authenticated but not authorized:
HTTP/1.1 403 Forbidden
Combining with ASP.NET Core Authorization
var app = builder.Build();
// Require authentication for all endpoints
app.UseAuthentication();
app.UseAuthorization();
// CQRS endpoints inherit auth requirements
app.UseSvrntyCqrs();
Best Practices
✅ DO
- Use for access control
- Check resource ownership
- Validate tenant isolation
- 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