Skip to content

CWE-285: Improper Authorization - C# / ASP.NET Core

Overview

In ASP.NET Core applications, improper authorization occurs when authentication/authorization middleware is configured but controllers, actions, Razor Pages, or minimal API endpoints lack effective authorization metadata, allowing unauthenticated or under-authorized access to protected resources. Missing, overly broad, or overridden authorization checks can expose resources even when authentication middleware is enabled.

Detection Context

This guidance applies when a security scanner detects:

  • UseAuthentication() and UseAuthorization() configured in Program.cs or Startup.cs
  • Controllers, actions, Razor Pages, or endpoints missing effective [Authorize], RequireAuthorization(), or fallback-policy coverage
  • Protected endpoints accidentally marked with [AllowAnonymous]
  • Applies to ASP.NET Core Web and Web API projects

Primary Defence: Apply [Authorize] at the controller class level or configure a fallback policy to secure endpoints by default, use method-level [Authorize(Roles = "...")] or [Authorize(Policy = "...")] for role-based or policy-based authorization on privileged operations, explicitly mark truly public endpoints with [AllowAnonymous], and enable both UseAuthentication() and UseAuthorization() middleware in the correct order to prevent unauthorized access.

Attribute Precedence Rules

Critical Understanding:

  • When both [Authorize] and [AllowAnonymous] apply to an endpoint, [AllowAnonymous] takes precedence
  • This can accidentally disable authorization checks
  • A method-level [Authorize] does not re-secure an action when [AllowAnonymous] still applies from the class or endpoint metadata

Remediation Strategy

Apply [Authorize] at Controller Level (Secure by Default)

// VULNERABLE - No authorization attribute
public class UserController : ControllerBase
{
    [HttpGet("profile/{id}")]
    public IActionResult GetUserProfile(int id)
    {
        // Anyone can access this, even unauthenticated!
        var user = _userService.GetUser(id);
        return Ok(user);
    }

    [HttpDelete("{id}")]
    public IActionResult DeleteUser(int id)
    {
        // Admin function accessible to anyone!
        _userService.DeleteUser(id);
        return NoContent();
    }
}

// SECURE - Authorize at class level, selective anonymous access
[Authorize]  // All methods require authentication by default
[ApiController]
[Route("api/[controller]")]
public class UserController : ControllerBase
{
    [HttpGet("profile/{id}")]
    public IActionResult GetUserProfile(int id)
    {
        // Requires authentication AND object-level authorization
        var user = _userService.GetUserForCurrentUser(id, User);
        return Ok(user);
    }

    [HttpDelete("{id}")]
    [Authorize(Roles = "Admin")]  // Additional role restriction
    public IActionResult DeleteUser(int id)
    {
        // Requires Admin role
        _userService.DeleteUser(id);
        return NoContent();
    }

    [HttpGet("public-info")]
    [AllowAnonymous]  // Explicitly allow public access
    public IActionResult GetPublicInfo()
    {
        // Public endpoint - no auth required
        return Ok(new { AppVersion = "1.0" });
    }
}

Role-Based Authorization

[Authorize]
[ApiController]
[Route("api/[controller]")]
public class OrderController : ControllerBase
{
    // Any authenticated user can view orders
    [HttpGet]
    public IActionResult GetOrders()
    {
        var orders = _orderService.GetOrdersForUser(User);
        return Ok(orders);
    }

    // Only users with "Manager" role
    [HttpGet("all")]
    [Authorize(Roles = "Manager")]
    public IActionResult GetAllOrders()
    {
        var orders = _orderService.GetAllOrders();
        return Ok(orders);
    }

    // Multiple roles (OR logic)
    [HttpGet("reports")]
    [Authorize(Roles = "Manager,Admin")]
    public IActionResult GetReports()
    {
        return Ok(_reportService.GetReports());
    }

    // Admin-only function
    [HttpDelete("{id}")]
    [Authorize(Roles = "Admin")]
    public IActionResult DeleteOrder(int id)
    {
        _orderService.DeleteOrder(id);
        return NoContent();
    }
}

Policy-Based Authorization (Best Practice)

// Program.cs - Define policies
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("RequireAdminRole", policy =>
        policy.RequireRole("Admin"));

    options.AddPolicy("RequireManagerOrAdmin", policy =>
        policy.RequireRole("Manager", "Admin"));

    options.AddPolicy("AtLeast21", policy =>
        policy.Requirements.Add(new MinimumAgeRequirement(21)));

    options.AddPolicy("CanEditProfile", policy =>
        policy.Requirements.Add(new ResourceOwnerRequirement()));
});

// Register custom authorization handlers
builder.Services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();
builder.Services.AddSingleton<IAuthorizationHandler, ProfileOwnerHandler>();

// Controller using policies
[Authorize]
[ApiController]
[Route("api/[controller]")]
public class AdminController : ControllerBase
{
    [HttpGet("dashboard")]
    [Authorize(Policy = "RequireAdminRole")]
    public IActionResult GetDashboard()
    {
        return Ok(_dashboardService.GetAdminDashboard());
    }

    [HttpPost("users")]
    [Authorize(Policy = "RequireManagerOrAdmin")]
    public IActionResult CreateUser([FromBody] UserDto user)
    {
        var result = _userService.CreateUser(user);
        return CreatedAtAction(nameof(GetUser), new { id = result.Id }, result);
    }
}

// Custom authorization requirement
public class MinimumAgeRequirement : IAuthorizationRequirement
{
    public int MinimumAge { get; }

    public MinimumAgeRequirement(int minimumAge)
    {
        MinimumAge = minimumAge;
    }
}

// Custom authorization handler
public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        MinimumAgeRequirement requirement)
    {
        var dateOfBirthClaim = context.User.FindFirst(
            c => c.Type == ClaimTypes.DateOfBirth);

        if (dateOfBirthClaim == null)
        {
            return Task.CompletedTask;
        }

        var dateOfBirth = Convert.ToDateTime(dateOfBirthClaim.Value);
        var age = DateTime.Today.Year - dateOfBirth.Year;

        if (dateOfBirth.Date > DateTime.Today.AddYears(-age))
        {
            age--;
        }

        if (age >= requirement.MinimumAge)
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

User-Specific Authorization

[Authorize]
[ApiController]
[Route("api/[controller]")]
public class ProfileController : ControllerBase
{
    // Restrict to specific named users only when account names are stable.
    // Prefer policies or resource-based authorization for most applications.
    [HttpGet("admin-profile")]
    [Authorize(Users = "alice@company.com,bob@company.com")]
    public IActionResult GetAdminProfile()
    {
        return Ok(_profileService.GetAdminProfile());
    }

    // Better approach: Resource-based authorization
    [HttpPut("{id}")]
    public async Task<IActionResult> UpdateProfile(
        int id, 
        [FromBody] ProfileDto profile,
        [FromServices] IAuthorizationService authService)
    {
        var userProfile = await _profileService.GetProfileAsync(id);

        if (userProfile == null)
        {
            return NotFound();
        }

        // Check if user can edit this specific resource
        var authResult = await authService.AuthorizeAsync(
            User, 
            userProfile, 
            "CanEditProfile");

        if (!authResult.Succeeded)
        {
            return Forbid();
        }

        await _profileService.UpdateProfileAsync(id, profile);
        return NoContent();
    }
}

// Resource-based authorization handler
public class ResourceOwnerRequirement : IAuthorizationRequirement
{
}

public class ProfileOwnerHandler : 
    AuthorizationHandler<ResourceOwnerRequirement, Profile>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        ResourceOwnerRequirement requirement,
        Profile resource)
    {
        var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;

        if (userId != null && resource.UserId.ToString() == userId)
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

Common Scenarios and Recommendations

Public Endpoints (Appropriate for Anonymous Access)

[ApiController]
[Route("api/[controller]")]
public class PublicController : ControllerBase
{
    // Login page - should be anonymous
    [HttpPost("login")]
    [AllowAnonymous]
    public IActionResult Login([FromBody] LoginDto credentials)
    {
        var token = _authService.Authenticate(credentials);
        return Ok(new { Token = token });
    }

    // Error page - should be anonymous
    [HttpGet("error")]
    [AllowAnonymous]
    public IActionResult Error()
    {
        return Ok(new { Message = "An error occurred" });
    }

    // Contact form - typically anonymous
    [HttpPost("contact")]
    [AllowAnonymous]
    public IActionResult SubmitContact([FromBody] ContactDto contact)
    {
        _contactService.ProcessContact(contact);
        return Accepted();
    }

    // Public content
    [HttpGet("about")]
    [AllowAnonymous]
    public IActionResult About()
    {
        return Ok(_contentService.GetAboutContent());
    }
}

Razor Pages Authorization

// VULNERABLE - No authorization
public class UserProfileModel : PageModel
{
    public void OnGet()
    {
        // Anyone can access
    }
}

// SECURE - With [Authorize]
[Authorize]
public class UserProfileModel : PageModel
{
    public void OnGet()
    {
        // Requires authentication; still add resource checks before returning user-specific data
    }
}

// Role-based for Razor Pages
[Authorize(Roles = "Admin")]
public class AdminDashboardModel : PageModel
{
    public void OnGet()
    {
        // Admin only
    }
}

MVC Controllers

[Authorize]
public class AccountController : Controller
{
    [AllowAnonymous]
    public IActionResult Login()
    {
        return View();
    }

    [AllowAnonymous]
    public IActionResult Register()
    {
        return View();
    }

    // Requires authentication (from class-level [Authorize])
    public IActionResult Profile()
    {
        return View();
    }

    [Authorize(Roles = "Admin")]
    public IActionResult AdminPanel()
    {
        return View();
    }
}

Dangerous Attribute Combinations

Issue 1: Conflicting Attributes (AllowAnonymous Wins)

// DANGEROUS - [AllowAnonymous] takes precedence!
[Authorize]
[AllowAnonymous]  // This wins - NO auth check!
public class DangerousController : ControllerBase
{
    // All methods are accessible without authentication
}

// CORRECT - Remove conflicting attribute
[Authorize]
public class SecureController : ControllerBase
{
    // All methods require authentication
}

Issue 2: Method-Level Override

[AllowAnonymous]  // Class level
public class MixedController : ControllerBase
{
    [Authorize]  // This is ignored! AllowAnonymous at class level applies
    public IActionResult SecureMethod()
    {
        // Still accessible without auth!
        return Ok();
    }
}

// CORRECT - Authorize at class, AllowAnonymous at method
[Authorize]  // Class level
public class ProperController : ControllerBase
{
    [AllowAnonymous]  // Method level - overrides class
    public IActionResult PublicMethod()
    {
        // No auth required
        return Ok();
    }

    public IActionResult SecureMethod()
    {
        // Auth required (from class attribute)
        return Ok();
    }
}

Issue 3: Missing Attributes on Sensitive Operations

// VULNERABLE
[Authorize]
public class OrderController : ControllerBase
{
    [HttpGet]
    public IActionResult GetOrders()
    {
        return Ok(_orderService.GetOrdersForUser(User));
    }

    // Missing additional role check!
    [HttpDelete("{id}")]
    public IActionResult DeleteOrder(int id)
    {
        // Any authenticated user can delete!
        _orderService.Delete(id);
        return NoContent();
    }
}

// SECURE
[Authorize]
public class OrderController : ControllerBase
{
    [HttpGet]
    public IActionResult GetOrders()
    {
        return Ok(_orderService.GetOrdersForUser(User));
    }

    [HttpDelete("{id}")]
    [Authorize(Roles = "Admin,Manager")]  // Restrict to admins
    public IActionResult DeleteOrder(int id)
    {
        _orderService.Delete(id);
        return NoContent();
    }
}

Alternative Authorization Middleware

Custom Middleware Authorization

// Program.cs
app.Use(async (context, next) =>
{
    if (context.Request.Path.StartsWithSegments("/api/admin"))
    {
        if (context.User?.Identity?.IsAuthenticated != true)
        {
            context.Response.StatusCode = 401;
            return;
        }

        if (!context.User.IsInRole("Admin"))
        {
            context.Response.StatusCode = 403;
            await context.Response.WriteAsync("Forbidden");
            return;
        }
    }

    await next();
});

Prefer built-in [Authorize], policies, and RequireAuthorization() for most applications. Custom middleware is easy to place in the wrong order or scope too narrowly; if used, it must run after authentication and before the protected endpoint executes.

Endpoint-Level Authorization (Minimal APIs)

// Program.cs - Minimal API
app.MapGet("/api/public", () => "Public data")
    .AllowAnonymous();

app.MapGet("/api/private", () => "Private data")
    .RequireAuthorization();

app.MapGet("/api/admin", () => "Admin data")
    .RequireAuthorization("RequireAdminRole");

app.MapPost("/api/orders", (Order order, ClaimsPrincipal user, IOrderService orderService) =>
{
    // Create the order for the authenticated user; do not trust user IDs in the body
    var created = orderService.CreateForUser(order, user);
    return Results.Created($"/api/orders/{created.Id}", created);
})
.RequireAuthorization(policy => 
    policy.RequireRole("Customer", "Admin"));

Configuration Requirements

Program.cs Setup

var builder = WebApplication.CreateBuilder(args);

// Add authentication services
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = builder.Configuration["Jwt:Issuer"],
            ValidAudience = builder.Configuration["Jwt:Audience"],
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]))
        };
    });

// Add authorization services with policies
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("RequireAdminRole", 
        policy => policy.RequireRole("Admin"));

    options.FallbackPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();  // Advanced hardening: require auth by default
});

var app = builder.Build();

// Place authentication and authorization after routing when routing is explicit,
// and before mapped controllers/endpoints execute.
app.UseAuthentication();  // Must come before UseAuthorization
app.UseAuthorization();   // Enables [Authorize] attributes

app.MapControllers();
app.Run();

Summary of Best Practices

  1. Apply [Authorize] at controller level - Require authentication by default
  2. Use [AllowAnonymous] selectively - Only for truly public endpoints (login, contact, error pages)
  3. Never combine [Authorize] and [AllowAnonymous] at the same level - AllowAnonymous always wins
  4. Use role-based authorization for admin/privileged functions
  5. Prefer policy-based authorization for complex requirements
  6. Implement resource-based authorization for user-specific data access
  7. Always enable UseAuthentication() and UseAuthorization() in correct order
  8. Test authorization thoroughly - Verify both positive and negative cases
  9. Document public endpoints - Make intentional anonymous access explicit
  10. Use fallback policies - Consider requiring auth by default across the app, and verify intentional anonymous endpoints still have explicit metadata

Common Vulnerable Patterns

Missing Authorization Attributes on Controllers

// VULNERABLE - No authorization attribute
[ApiController]
[Route("api/[controller]")]
public class UserController : ControllerBase
{
    [HttpGet("{id}")]
    public IActionResult GetUser(int id)
    {
        // Anyone can access this, even unauthenticated!
        var user = _userService.GetUser(id);
        return Ok(user);
    }

    [HttpDelete("{id}")]
    public IActionResult DeleteUser(int id)
    {
        // Critical operation accessible to anyone!
        _userService.DeleteUser(id);
        return NoContent();
    }
}

Conflicting Attributes (AllowAnonymous Wins)

// VULNERABLE - AllowAnonymous takes precedence over Authorize
[Authorize]
[AllowAnonymous]  // This wins - NO auth required!
public class DangerousController : ControllerBase
{
    [HttpGet("admin/delete")]
    public IActionResult DeleteAll()
    {
        // Still accessible without authentication!
        _service.DeleteAllData();
        return NoContent();
    }
}

Missing Role Checks on Privileged Operations

// VULNERABLE - Any authenticated user can access
[Authorize]
public class AdminController : ControllerBase
{
    [HttpPost("users")]
    public IActionResult CreateUser([FromBody] UserDto user)
    {
        // Missing role check - any authenticated user can create users!
        _userService.CreateUser(user);
        return Created();
    }
}

Class-Level AllowAnonymous Overriding Method-Level Authorize

// VULNERABLE - Method-level Authorize is ignored
[AllowAnonymous]  // Class-level
public class ProblemsController : ControllerBase
{
    [Authorize]  // This is ignored! AllowAnonymous at class level wins
    public IActionResult SecureMethod()
    {
        // Still accessible without authentication!
        return Ok(_sensitiveData);
    }
}

Secure Patterns

Controller-Level Authorization with Selective Anonymous Access

// SECURE - Authorize at class level, selective anonymous
[Authorize]  // All methods require authentication by default
[ApiController]
[Route("api/[controller]")]
public class UserController : ControllerBase
{
    [HttpGet("{id}")]
    public IActionResult GetUser(int id)
    {
        // Requires authentication and returns only data this principal may see
        var user = _userService.GetUserForCurrentUser(id, User);
        return Ok(user);
    }

    [HttpDelete("{id}")]
    [Authorize(Roles = "Admin")]  // Additional role requirement
    public IActionResult DeleteUser(int id)
    {
        // Requires Admin role
        _userService.DeleteUser(id);
        return NoContent();
    }

    [HttpGet("public")]
    [AllowAnonymous]  // Explicitly allow public access
    public IActionResult GetPublicInfo()
    {
        return Ok(new { Version = "1.0" });
    }
}

Why this works:

  • Authenticated by default: Applying [Authorize] at the controller class level establishes authentication as the default requirement for all actions within that controller, reducing accidental exposure of protected endpoints
  • Automatic inheritance: Any new methods added to the controller automatically inherit this protection without requiring explicit authorization attributes
  • Layered authorization: Method-level [Authorize(Roles = "Admin")] adds an additional layer of authorization on top of the class-level authentication requirement, ensuring that only users who are both authenticated AND have the Admin role can access the DeleteUser endpoint
  • Explicit exceptions: The [AllowAnonymous] attribute on GetPublicInfo() creates a clear, documented exception to the authentication requirement, making it obvious to code reviewers that this endpoint is intentionally public
  • Clear public exceptions: Developers must explicitly opt out of authentication for public endpoints, reducing the chance that a newly added endpoint is accidentally anonymous. Resource-specific authorization is still required for user-owned or tenant-owned data.

Role-Based Authorization (Secure Implementation)

// SECURE - Proper role checks for privileged operations
[Authorize]
[ApiController]
[Route("api/[controller]")]
public class AdminController : ControllerBase
{
    [HttpGet("dashboard")]
    [Authorize(Roles = "Admin")]
    public IActionResult GetDashboard()
    {
        return Ok(_dashboardService.GetData());
    }

    [HttpPost("users")]
    [Authorize(Roles = "Admin,Manager")]  // Multiple roles (OR logic)
    public IActionResult CreateUser([FromBody] UserDto user)
    {
        _userService.CreateUser(user);
        return Created();
    }
}

Why this works:

  • Declarative access control: Role-based authorization using the [Authorize(Roles = "...")] attribute provides a declarative way to restrict access to specific user roles, with ASP.NET Core automatically verifying that the authenticated user's role claims match the specified roles before allowing the action to execute
  • Single and multiple role support: The GetDashboard method demonstrates single-role authorization where only users with the "Admin" role can access the endpoint, while CreateUser shows multiple-role authorization with comma-separated values ("Admin,Manager") implementing OR logic - users with either role can access it
  • Maintainability advantage: This approach is more maintainable than imperative role checks scattered throughout the code because the authorization logic is visible in the method signature and processed by the authorization middleware before the action executes
  • Layered authentication and authorization: The class-level [Authorize] ensures all users are authenticated first, then method-level role attributes add additional restrictions for privileged operations
  • Automatic claim validation: Role claims are typically added to the user's ClaimsIdentity during authentication (via JWT tokens or cookie authentication), and the authorization middleware validates these claims automatically without requiring custom code in each controller action

Policy-Based Authorization

// SECURE - Using policies for complex authorization
// Program.cs
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("RequireAdminRole", policy =>
        policy.RequireRole("Admin"));

    options.AddPolicy("RequireManagerOrAdmin", policy =>
        policy.RequireRole("Manager", "Admin"));

    options.AddPolicy("AtLeast21", policy =>
        policy.Requirements.Add(new MinimumAgeRequirement(21)));
});

// Controller
[Authorize]
[ApiController]
[Route("api/[controller]")]
public class AdminController : ControllerBase
{
    [HttpGet("dashboard")]
    [Authorize(Policy = "RequireAdminRole")]
    public IActionResult GetDashboard()
    {
        return Ok(_dashboardService.GetAdminDashboard());
    }
}

Why this works:

  • Flexible authorization: Policy-based approach centralizes logic in Program.cs via AddAuthorization(), enabling rule changes without controller modifications
  • Complex requirements: Policies support sophisticated logic beyond simple role checks (e.g., AtLeast21 with custom IAuthorizationRequirement and AuthorizationHandler<T>)
  • Clear intent: [Authorize(Policy = "RequireAdminRole")] attribute references policy by name, making authorization intent clear while separating implementation
  • Reusability: Policies enable shared authorization logic across controllers/actions, reducing duplication and inconsistencies
  • Contextual decisions: Custom handlers access full AuthorizationHandlerContext (user claims, HTTP context) for sophisticated decisions impossible with simple role attributes

Resource-Based Authorization

// SECURE - User can only edit their own resources
[Authorize]
[ApiController]
[Route("api/[controller]")]
public class ProfileController : ControllerBase
{
    [HttpPut("{id}")]
    public async Task<IActionResult> UpdateProfile(
        int id, 
        [FromBody] ProfileDto profile,
        [FromServices] IAuthorizationService authService)
    {
        var userProfile = await _profileService.GetProfileAsync(id);

        if (userProfile == null)
        {
            return NotFound();
        }

        // Check if user can edit this specific resource
        var authResult = await authService.AuthorizeAsync(
            User, 
            userProfile, 
            "CanEditProfile");

        if (!authResult.Succeeded)
        {
            return Forbid();
        }

        await _profileService.UpdateProfileAsync(id, profile);
        return NoContent();
    }
}

Why this works:

  • Resource-specific decisions: Evaluates authorization against specific resource instances (e.g., user can edit own profile, not others')
  • Imperative evaluation: IAuthorizationService.AuthorizeAsync() called in action with current User, loaded userProfile resource, and policy name
  • Handler access to properties: Authorization handlers examine resource properties - ProfileOwnerHandler compares resource.UserId to authenticated user's ID claim
  • Proper HTTP semantics: Returns Forbid() (403) when authResult.Succeeded is false - user authenticated but lacks permission for this specific resource
  • Centralized logic: Authorization in handlers (not controller code) maintains separation of concerns while enabling fine-grained access control for multi-tenant/user-specific data

Remediation Steps

  1. Inventory controllers, Razor pages, SignalR hubs, minimal APIs, and custom middleware that expose protected data or actions.
  2. Define the intended access rule for each endpoint: public, authenticated, role-based, policy-based, or resource/object-specific.
  3. Secure endpoints by default with controller-level [Authorize], endpoint .RequireAuthorization(), or a fallback policy, then mark intentionally public endpoints with [AllowAnonymous].
  4. Add role or policy checks for privileged operations, and use IAuthorizationService.AuthorizeAsync() for object-level decisions such as ownership, tenant isolation, or per-record permissions.
  5. Remove accidental [AllowAnonymous] metadata and verify that custom middleware runs after authentication and before protected endpoints execute.
  6. Review service-layer methods used by protected endpoints so request body fields such as userId, tenantId, or role cannot override the authenticated principal.

Testing

  • Verify successful access for each intended actor, including normal users, privileged users, and intentionally anonymous endpoints.
  • Verify denied access for unauthenticated requests, authenticated users with the wrong role or policy, and users trying to access another user's or tenant's records.
  • Exercise direct API calls instead of only UI flows, including guessed route IDs, tampered request bodies, modified JWT claims, and minimal API endpoints.
  • Confirm HTTP behavior is intentional: use 401 Unauthorized for unauthenticated requests and 403 Forbidden or a deliberate not-found response for authenticated users who lack access.
  • Add regression tests for each new endpoint so future changes cannot accidentally remove authorization metadata or bypass object-level checks.

Additional Resources