Your [Authorize] Attribute Is Compliance Theater
Authentication proves identity. Authorization enforces access rights. ISO/IEC 27001 Control A.9 requires both — and teams consistently confuse them.
I’ve reviewed too many ASP.NET Core applications where authentication exists but authorization is theater. Teams feel secure because they see [Authorize] attributes on controllers. Then you find [AllowAnonymous] scattered across action methods, hardcoded role strings in business logic, and zero audit trail when authorization fails.
This isn’t just bad architecture. It’s a violation of ISO/IEC 27001 Control A.9 (Access Control), which explicitly requires:
- A.9.1: Business requirements of access control
- A.9.2: User access management
- A.9.4: System and application access control
The standard doesn’t care that your authentication works. It demands that access rights are enforced based on business rules, managed centrally, and audited comprehensively. String-based role checks scattered across your codebase don’t satisfy any of those requirements.
Here’s what inadequate access control looks like in the codebases I’ve reviewed, why auditors flag it immediately, and what actually works.
The Fatal Pattern: Authentication Without Authorization
Here’s what I encounter repeatedly in ASP.NET Core applications that claim to be “secure”:
[Authorize] // Authentication required...
public class DocumentsController : ControllerBase
{
// ...but anyone authenticated can access everything
[HttpGet("{id}")]
[AllowAnonymous] // Wait, why is this here?
public async Task<IActionResult> GetDocument(int id) { /* ... */ }
[HttpPost]
public async Task<IActionResult> CreateDocument(CreateDocumentRequest request)
{
// Authorization check buried in business logic
if (!User.IsInRole("DocumentCreator"))
return Forbid(); // No audit trail
/* ... */
}
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteDocument(int id)
{
// Different pattern, same problem
var userRoles = User.Claims
.Where(c => c.Type == ClaimTypes.Role)
.Select(c => c.Value);
if (!userRoles.Contains("Admin") && !userRoles.Contains("DocumentManager"))
return Forbid(); // Still no audit trail
/* ... */
}
}
This code violates multiple ISO 27001 controls simultaneously:
Violation 1: Inconsistent Access Control (A.9.4.1)
The [AllowAnonymous] attribute on GetDocument contradicts the controller-level [Authorize] attribute. Why does document retrieval bypass authentication entirely? This creates an access control gap that auditors will flag immediately.
ISO requirement: “Access to systems and applications shall be controlled in accordance with access control policy.”
There is no coherent access control policy here. Anonymous access for reads, role checks for writes, and no documented business justification.
Violation 2: Decentralized Authorization Logic (A.9.2.1)
Authorization decisions are scattered across controller actions using different patterns:
- String-based role checks:
User.IsInRole("DocumentCreator") - Manual claim enumeration:
User.Claims.Where(...) - Inconsistent role requirements:
"Admin"vs"DocumentManager"with no explanation
ISO requirement: “A formal user access provisioning process shall be implemented to assign or revoke access rights.”
How do you audit these access rights? Where is the central policy that defines who can create, read, update, or delete documents? The authorization rules are embedded in controller code. Every change requires a deployment. Every audit requires reading through controller implementations.
Violation 3: No Audit Trail (A.9.4.5)
When authorization fails, the application returns 403 Forbidden. But where’s the audit log?
ISO requirement: “Access to information and application system functions shall be restricted in accordance with the access control policy.”
The standard requires you to demonstrate that access controls are enforced. Without audit logs of authorization failures, you cannot prove compliance. Returning Forbid() silently discards evidence that the control was tested.
Violation 4: Brittle Role Management (A.9.2.2)
Hardcoded role strings like "DocumentCreator" and "Admin" create maintenance nightmares. When business requirements change:
- Which role takes precedence?
- Who decides when roles should be renamed?
- How do you handle role deprecation without breaking production?
ISO requirement: “User access rights shall be reviewed at regular intervals.”
You can’t review access rights that are hardcoded strings scattered across dozens of controllers. Every review requires grepping the codebase and hoping developers remembered to update every location.
What Actually Works: Policy-Based Authorization
ISO 27001 Control A.9 is satisfied when authorization is:
- Centralized — defined in one location, not scattered across controllers
- Policy-driven — based on business requirements, not implementation details
- Auditable — authorization decisions are logged comprehensively
- Testable — policies can be verified independently of controllers
Here’s how ASP.NET Core implements this correctly:
Step 1: Define Authorization Policies
// Program.cs
builder.Services.AddAuthorization(options =>
{
// Business requirements become named policies
options.AddPolicy("ViewDocuments", policy =>
policy.RequireClaim("permission", "documents.read"));
options.AddPolicy("CreateDocuments", policy =>
policy.RequireClaim("permission", "documents.write"));
// Resource-based authorization for ownership
options.AddPolicy("EditOwnDocuments", policy =>
policy.Requirements.Add(new DocumentOwnershipRequirement()));
});
This satisfies A.9.1 (Business requirements of access control) by codifying access requirements as named policies that reflect business operations, not technical role names.
Step 2: Implement Custom Authorization Handlers
For complex business rules, use authorization handlers:
public class DocumentOwnershipRequirement : IAuthorizationRequirement { }
public class DocumentOwnershipHandler : AuthorizationHandler<DocumentOwnershipRequirement, Document>
{
private readonly ILogger<DocumentOwnershipHandler> _logger;
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
DocumentOwnershipRequirement requirement,
Document resource)
{
var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (userId == resource.OwnerId)
{
context.Succeed(requirement);
}
else
{
// A.9.4.5: Audit authorization failure
_logger.LogWarning("User {UserId} denied access to document {DocumentId}",
userId, resource.Id);
}
return Task.CompletedTask;
}
}
This satisfies A.9.4.1 (Restriction of access to information) by centralizing business logic that determines access rights. Authorization decisions are made in one location, not repeated across controllers.
Step 3: Apply Policies Declaratively
[Authorize]
public class DocumentsController : ControllerBase
{
private readonly IAuthorizationService _authorizationService;
private readonly ILogger<DocumentsController> _logger;
[HttpGet("{id}")]
[Authorize(Policy = "ViewDocuments")] // Declarative policy
public async Task<IActionResult> GetDocument(int id)
{
var document = await _documentService.GetDocumentAsync(id);
if (document == null) return NotFound();
// Resource-based authorization for ownership
var authResult = await _authorizationService
.AuthorizeAsync(User, document, "EditOwnDocuments");
if (!authResult.Succeeded)
{
// A.9.4.5: Audit authorization failure
_logger.LogWarning(
"User {UserId} denied access to document {DocumentId}",
User.FindFirst(ClaimTypes.NameIdentifier)?.Value, id);
return Forbid();
}
return Ok(document);
}
[HttpPost]
[Authorize(Policy = "CreateDocuments")]
public async Task<IActionResult> CreateDocument(CreateDocumentRequest request)
{
_logger.LogInformation("User {UserId} creating document",
User.FindFirst(ClaimTypes.NameIdentifier)?.Value);
/* ... */
}
}
Key Improvements
Centralized Policy Definition: All authorization logic lives in
Program.cs, not scattered across controllers. When business requirements change, you update policies in one location.Claims-Based Permissions: Instead of role strings, use permission claims (
documents.read,documents.write,documents.manage). This separates identity (who you are) from authorization (what you can do).Resource-Based Authorization: Use
IAuthorizationServicefor dynamic checks that depend on resource properties (e.g., document ownership). This handles scenarios where static policies aren’t sufficient.Comprehensive Audit Logging: Every authorization decision is logged with context (user ID, resource ID, action attempted). This satisfies A.9.4.5 audit requirements.
Testing Authorization Policies
ISO 27001 Control A.12.1.2 requires testing of security controls. Here’s how to verify authorization policies through automated integration tests:
public class DocumentAuthorizationTests : IClassFixture<WebApplicationFactory<Program>>
{
[Fact]
public async Task GetDocument_WithoutAuthentication_Returns401()
{
var client = _factory.CreateClient();
var response = await client.GetAsync("/api/documents/1");
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
[Fact]
public async Task GetDocument_WithoutViewPermission_Returns403()
{
var client = _factory.CreateClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", TokenWithoutReadPermission());
var response = await client.GetAsync("/api/documents/1");
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
}
}
GitHub Actions Integration
Run these tests in CI/CD to verify authorization policies before deployment:
name: Security Compliance
on:
pull_request:
paths:
- 'src/**/*.cs'
- 'tests/**/*.cs'
jobs:
authorization-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.0.x'
- name: Run Authorization Tests
run: dotnet test --filter "Category=Authorization" --logger "trx;LogFileName=authorization-results.trx"
- name: Publish Test Results
if: always()
uses: EnricoMi/publish-unit-test-result-action@v2
with:
files: '**/authorization-results.trx'
This satisfies A.12.1.2 (Testing of security controls) by automating verification that access controls function as intended.
Health Checks for Authentication Services
ISO 27001 Control A.17.1.1 requires monitoring availability of critical services. Authentication is a critical service. Here’s how to monitor it:
// Program.cs
builder.Services.AddHealthChecks()
.AddCheck<AuthenticationServiceHealthCheck>("authentication");
public class AuthenticationServiceHealthCheck : IHealthCheck
{
private readonly IConfiguration _configuration;
private readonly HttpClient _httpClient;
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
var authEndpoint = _configuration["Authentication:Authority"];
try
{
var response = await _httpClient.GetAsync(
$"{authEndpoint}/.well-known/openid-configuration",
cancellationToken);
return response.IsSuccessStatusCode
? HealthCheckResult.Healthy()
: HealthCheckResult.Degraded($"Status: {response.StatusCode}");
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy("Authentication unavailable", ex);
}
}
}
// Expose endpoint
app.MapHealthChecks("/health");
This enables external monitoring systems to verify authentication service availability, satisfying operational monitoring requirements.
Mapping ISO Controls to Implementation
Here’s how policy-based authorization satisfies specific ISO 27001 requirements:
| ISO Control | Requirement | Implementation |
|---|---|---|
| A.9.1.1 | Access control policy | Centralized policy definitions in Program.cs |
| A.9.2.1 | User access provisioning | Claims-based permissions managed via identity provider |
| A.9.2.2 | User access rights management | Permission claims reviewed and updated independently of code |
| A.9.4.1 | Information access restriction | [Authorize(Policy = "...")] attributes enforce policy |
| A.9.4.5 | Access control to program source code | Resource-based authorization with IAuthorizationService |
| A.12.1.2 | Testing of security controls | Automated integration tests verify authorization logic |
| A.12.4.1 | Event logging | Comprehensive audit logs of authorization decisions |
| A.17.1.1 | Continuity capabilities | Health checks monitor authentication service availability |
What This Means for Your Team
If you’re building ASP.NET Core applications that need to satisfy ISO 27001 compliance:
Stop using role strings in business logic. They scatter authorization decisions across your codebase, making audits painful and policy changes risky.
Define authorization policies that map to business operations.
"ViewDocuments"is clearer than checking whetherUser.IsInRole("DocumentReader")— though you’ll need both policies and resource-based checks for complex scenarios.Use claims for permissions, not roles. Roles represent groups. Permissions represent capabilities. ISO 27001 cares about capabilities.
Implement comprehensive audit logging. Every authorization failure must be logged with sufficient context to reconstruct what happened and why.
Test your authorization policies. Write integration tests that verify policies work correctly. Run them in CI/CD before deployment.
Monitor authentication service availability. Health checks aren’t optional. They’re how you demonstrate operational monitoring.
Authentication proves identity. Authorization enforces access rights. ISO/IEC 27001 Control A.9 requires both.
The gap between [Authorize] and actual access control is where I find compliance violations in every audit. Policy-based authorization closes that gap — when you map policies to business operations, not developer convenience.
![Your [Authorize] Attribute Is Compliance Theater
Your [Authorize] Attribute Is Compliance Theater](/images/security.png?v=530c4f0b5995d08df3450423fd03c5e0)