Authorization
Model-bound queries support authorization through standard ASP.NET Core authorization attributes as well as the convenient [Roles] attribute provided by the Arc.
Using the Authorize Attribute
Section titled “Using the Authorize Attribute”You can secure query methods using the standard [Authorize] attribute:
[ReadModel]public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance){ [Authorize] public static IEnumerable<DebitAccount> GetAllAccounts(IMongoCollection<DebitAccount> collection) => collection.Find(_ => true).ToList();
[Authorize(Roles = "Admin,Manager")] public static IEnumerable<DebitAccount> GetSensitiveAccounts(IMongoCollection<DebitAccount> collection) => collection.Find(a => a.Balance > 100000).ToList();}Using the Roles Attribute
Section titled “Using the Roles Attribute”The Arc provides a more convenient [Roles] attribute for cleaner syntax when specifying multiple roles:
[ReadModel]public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance){ [Roles("Admin", "Auditor")] public static IEnumerable<DebitAccount> GetAdminAccounts(IMongoCollection<DebitAccount> collection) => collection.Find(_ => true).ToList();
[Roles("Manager")] public static IEnumerable<DebitAccount> GetManagerAccounts(IMongoCollection<DebitAccount> collection) => collection.Find(a => a.Owner != CustomerId.Empty).ToList();}The user needs to have at least one of the specified roles to execute the query.
Read Model-Level Authorization
Section titled “Read Model-Level Authorization”You can apply authorization at the read model level to protect all query methods:
[ReadModel][Roles("User")] // All methods require at least "User" rolepublic record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance){ public static IEnumerable<DebitAccount> GetAllAccounts(IMongoCollection<DebitAccount> collection) => collection.Find(_ => true).ToList();
[Roles("Admin")] // Override read model-level authorization public static IEnumerable<DebitAccount> GetAdminOnlyAccounts(IMongoCollection<DebitAccount> collection) => collection.Find(a => a.Balance < 0).ToList();}Method-Level Authorization Override
Section titled “Method-Level Authorization Override”Method-level authorization attributes override class-level ones:
[ReadModel][Authorize] // Require authentication for all methodspublic record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance){ // Inherits class-level [Authorize] - requires authentication public static IEnumerable<DebitAccount> GetUserAccounts(IMongoCollection<DebitAccount> collection) => collection.Find(_ => true).ToList();
[Roles("Admin", "Manager")] // Overrides class-level, requires specific roles public static IEnumerable<DebitAccount> GetPrivilegedAccounts(IMongoCollection<DebitAccount> collection) => collection.Find(a => a.Balance > 50000).ToList();
[AllowAnonymous] // Completely overrides class-level authorization public static int GetTotalAccountCount(IMongoCollection<DebitAccount> collection) => (int)collection.CountDocuments(_ => true);}Policy-Based Authorization
Section titled “Policy-Based Authorization”For more complex authorization scenarios, you can use policy-based authorization:
[ReadModel]public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance){ [Authorize(Policy = "RequireAccountAccess")] public static DebitAccount GetAccountById( AccountId id, IMongoCollection<DebitAccount> collection) => collection.Find(a => a.Id == id).FirstOrDefault();
[Authorize(Policy = "RequireHighValueAccess")] public static IEnumerable<DebitAccount> GetHighValueAccounts( IMongoCollection<DebitAccount> collection) => collection.Find(a => a.Balance > 1000000).ToList();}Context-Dependent Authorization
Section titled “Context-Dependent Authorization”Access user context within query methods for dynamic authorization:
[ReadModel]public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance){ [Authorize] public static IEnumerable<DebitAccount> GetMyAccounts( IMongoCollection<DebitAccount> collection, IHttpContextAccessor httpContextAccessor) { var userId = httpContextAccessor.HttpContext?.User?.FindFirst("sub")?.Value; if (string.IsNullOrEmpty(userId)) return Enumerable.Empty<DebitAccount>();
var customerId = new CustomerId(Guid.Parse(userId)); return collection.Find(a => a.Owner == customerId).ToList(); }
[Authorize] public static DebitAccount? GetAccountIfOwned( AccountId accountId, IMongoCollection<DebitAccount> collection, IHttpContextAccessor httpContextAccessor) { var userId = httpContextAccessor.HttpContext?.User?.FindFirst("sub")?.Value; if (string.IsNullOrEmpty(userId)) return null;
var customerId = new CustomerId(Guid.Parse(userId)); return collection.Find(a => a.Id == accountId && a.Owner == customerId).FirstOrDefault(); }}Role Hierarchies
Section titled “Role Hierarchies”Implement role hierarchies with custom authorization:
[ReadModel]public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance){ [Roles("User")] // Basic users can see their own accounts public static IEnumerable<DebitAccount> GetBasicAccounts(IMongoCollection<DebitAccount> collection) => collection.Find(a => a.Balance >= 0).ToList();
[Roles("Manager", "Admin")] // Managers and admins can see more public static IEnumerable<DebitAccount> GetManagerAccounts(IMongoCollection<DebitAccount> collection) => collection.Find(a => a.Balance > -1000).ToList();
[Roles("Admin")] // Only admins can see all accounts including severely overdrawn public static IEnumerable<DebitAccount> GetAllAccountsIncludingProblematic(IMongoCollection<DebitAccount> collection) => collection.Find(_ => true).ToList();}Observable Query Authorization
Section titled “Observable Query Authorization”Authorization also applies to observable queries:
[ReadModel]public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance){ [Authorize] public static ISubject<IEnumerable<DebitAccount>> GetAccountsObservable( IMongoCollection<DebitAccount> collection) => collection.Observe();
[Roles("Admin")] public static ISubject<IEnumerable<DebitAccount>> GetAdminAccountsObservable( IMongoCollection<DebitAccount> collection) => collection.Observe(a => a.Balance < 0);}Authorization with Query Parameters
Section titled “Authorization with Query Parameters”Combine authorization with parameter-based filtering:
[ReadModel]public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance){ [Authorize] public static IEnumerable<DebitAccount> GetAccountsByOwner( CustomerId ownerId, IMongoCollection<DebitAccount> collection, IHttpContextAccessor httpContextAccessor) { var currentUserId = httpContextAccessor.HttpContext?.User?.FindFirst("sub")?.Value; var isAdmin = httpContextAccessor.HttpContext?.User?.IsInRole("Admin") == true;
// Users can only see their own accounts unless they're admin if (!isAdmin && currentUserId != ownerId.Value.ToString()) { return Enumerable.Empty<DebitAccount>(); }
return collection.Find(a => a.Owner == ownerId).ToList(); }}Custom Authorization Attributes
Section titled “Custom Authorization Attributes”Create custom authorization attributes for domain-specific logic:
public class RequireAccountOwnershipAttribute : AuthorizeAttribute{ public RequireAccountOwnershipAttribute() : base("RequireAccountOwnership") { }}
[ReadModel]public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance){ [RequireAccountOwnership] public static DebitAccount GetAccountDetails( AccountId id, IMongoCollection<DebitAccount> collection) => collection.Find(a => a.Id == id).FirstOrDefault();}Authorization Results
Section titled “Authorization Results”When authorization fails, the query pipeline automatically returns an unauthorized result. The query method will not be executed:
// In your policy handler or middlewarepublic class AccountOwnershipHandler : AuthorizationHandler<AccountOwnershipRequirement>{ protected override Task HandleRequirementAsync( AuthorizationHandlerContext context, AccountOwnershipRequirement requirement) { var userId = context.User.FindFirst("sub")?.Value;
// Check if user owns the account being accessed if (userId is not null /* && user owns the account being accessed */) { context.Succeed(requirement); }
return Task.CompletedTask; }}Anonymous Access
Section titled “Anonymous Access”Use [AllowAnonymous] to allow public access to specific query methods. This attribute bypasses all authorization requirements, including class-level [Authorize] attributes and role requirements.
How AllowAnonymous Works
Section titled “How AllowAnonymous Works”The authorization system evaluates attributes in the following order:
- Method-level
[AllowAnonymous]- If present on the method, allows anonymous access immediately - Method-level
[Authorize]or[Roles]- If present on the method, these take precedence over class-level attributes - Class-level
[AllowAnonymous]- If present on the class (and no method-level authorization), allows anonymous access - Class-level
[Authorize]or[Roles]- Applied when no method-level attributes are specified
Method-Level AllowAnonymous
Section titled “Method-Level AllowAnonymous”Override class-level authorization for specific query methods:
[ReadModel][Authorize] // Require authentication by defaultpublic record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance){ [AllowAnonymous] // Override class-level authorization for public data public static int GetTotalAccountCount(IMongoCollection<DebitAccount> collection) => (int)collection.CountDocuments(_ => true);
[AllowAnonymous] public static decimal GetAverageBalance(IMongoCollection<DebitAccount> collection) { var accounts = collection.Find(_ => true).ToList(); return accounts.Count > 0 ? accounts.Average(a => a.Balance) : 0; }
// This method requires authentication (inherits from class) public static IEnumerable<DebitAccount> GetAllAccounts(IMongoCollection<DebitAccount> collection) => collection.Find(_ => true).ToList();}Class-Level AllowAnonymous
Section titled “Class-Level AllowAnonymous”Apply [AllowAnonymous] at the class level to make all query methods publicly accessible by default:
[ReadModel][AllowAnonymous] // All methods are publicly accessible by defaultpublic record PublicStatistics(string Category, int Count){ public static IEnumerable<PublicStatistics> GetAllStatistics( IMongoCollection<PublicStatistics> collection) => collection.Find(_ => true).ToList();
public static PublicStatistics? GetByCategory( string category, IMongoCollection<PublicStatistics> collection) => collection.Find(s => s.Category == category).FirstOrDefault();
[Authorize] // Override class-level: this specific method requires authentication public static IEnumerable<PublicStatistics> GetSensitiveStatistics( IMongoCollection<PublicStatistics> collection) => collection.Find(s => s.Category.StartsWith("Internal")).ToList();}Common Use Cases for AllowAnonymous
Section titled “Common Use Cases for AllowAnonymous”- Public statistics or counts - Aggregate data that doesn’t expose sensitive information
- Product catalogs - Public product listings for e-commerce sites
- Public content - Blog posts, articles, or documentation
- Health checks - System status information for monitoring
- Search endpoints - Public search functionality
Best Practices
Section titled “Best Practices”- Apply authorization at the appropriate level - Use class-level for broad protection, method-level for specific requirements
- Use the
[Roles]attribute - More convenient than the standard[Authorize(Roles = "...")]syntax - Implement defense in depth - Combine multiple authorization layers when appropriate
- Consider user context - Use
IHttpContextAccessorto access current user information for dynamic authorization - Test authorization - Ensure unauthorized users cannot access protected queries
- Use policies for complex logic - Implement custom authorization policies for domain-specific rules
- Be explicit about public access - Use
[AllowAnonymous]to clearly indicate intentionally public methods - Log authorization failures - Monitor and log unauthorized access attempts
- Keep authorization simple - Complex authorization logic should be in services, not query methods
Note: Authorization is evaluated before the query method is called. If authorization fails, the query will not be executed and the result will indicate the authorization failure. The proxy generator automatically creates TypeScript types that respect your authorization constraints, helping prevent unauthorized client-side calls.