[Authorize]
Require authentication, roles or a named policy on controllers and minimal APIs with [Authorize], and get 401 versus 403 and combined attributes right.
[Authorize(Policy = "Refunds", Roles = "finance,admin", AuthenticationSchemes = "Bearer")]- Added in .NET ASP.NET Core 1.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$policy (constructor) / Policyoptional | string?= null (the default policy: an authenticated user) | Name of a policy registered with AddAuthorization/AddAuthorizationBuilder. Unknown names throw at request time. |
$Rolesoptional | string?= null | Comma-separated roles; the user needs ANY one of them (ClaimsPrincipal.IsInRole). Use several attributes to require ALL. |
$AuthenticationSchemesoptional | string?= null (the default authenticate scheme) | Comma-separated schemes to authenticate with for this endpoint instead of the default scheme. |
Return value
n/aAdds IAuthorizeData metadata to the endpoint. The authorization middleware combines all such metadata into one policy and evaluates it before the endpoint runs.
Errors
InvalidOperationException at request time for an unknown policy name, or when no authentication scheme is configured and a challenge is needed. Missing UseAuthorization() with authorization metadata on an endpoint also throws when the endpoint executes.
Changelog
- PHP ASP.NET Core 3.0Evaluated by UseAuthorization() on endpoint metadata instead of an MVC filter.
- PHP ASP.NET Core 6.0Works on minimal API handlers (lambdas and methods); equivalent to RequireAuthorization().
- PHP ASP.NET Core 10Cookie authentication returns 401/403 instead of redirecting for API endpoints such as [ApiController] actions and minimal APIs returning JSON.
[Authorize] is endpoint metadata. At startup it is attached to the endpoint built from the controller, action or handler. Per request, the authorization middleware (UseAuthorization(), added automatically by WebApplication when authorization services are registered) collects every IAuthorizeData on the endpoint, combines them into one AuthorizationPolicy, authenticates with the requested schemes and evaluates it. The endpoint never runs when it fails.
Combination rules
- Bare
[Authorize]uses the default policy: any authenticated user. - Attributes on the class and on the action are combined with AND; the action cannot loosen the class.
Roles = "finance,admin"means finance OR admin. Two attributes,[Authorize(Roles = "finance")]and[Authorize(Roles = "admin")], mean both.- Group-level
RequireAuthorization()in minimal APIs combines with handler attributes the same way. [AllowAnonymous]anywhere on the endpoint skips the whole evaluation.
401 or 403
| Situation | Result | Produced by |
|---|---|---|
| No or invalid credentials | 401, WWW-Authenticate header | Challenge of the scheme |
| Authenticated, policy fails | 403 | Forbid of the scheme |
Clients must treat them differently: 401 means get a token, 403 means do not retry.
Roles or policies
Roles is a shortcut that hard-codes role names in attributes all over the code. Prefer a named policy ([Authorize(Policy = "Refunds")]) defined once in Program.cs: it can combine roles, scopes and claims, and changing the rule touches one place. Policies are also what resource-based checks build on.
Do not check scopes with RequireClaim("scope", ...). It compares the whole claim value, and an access token carries all its scopes in one space-separated claim ("scope": "payments:read payments:refund"), so the policy returns 403 for a token that has the scope. A test that adds one claim per scope passes and hides the bug. Split the value, as the Scopes.Has assertion in the example does, or use a scope requirement handler (see Authorization Policies).
Production failure modes
- Roles never match.
IsInRolechecks the identity'sRoleClaimType. With the JwtBearer defaults (MapInboundClaims = true)roleandrolesare mapped toClaimTypes.Roleand work. WithMapInboundClaims = false(or a custom role claim name) claims keep their issued names such asroles, whileRoleClaimTypestill expects theClaimTypes.RoleURI. SetTokenValidationParameters.RoleClaimTypeto match, or every role check returns 403. - Forgotten endpoints. Opt-in attributes mean a new endpoint is public until someone remembers. Set a
FallbackPolicythat requires an authenticated user, so public endpoints must opt out with[AllowAnonymous]. - Middleware order. Calling
UseAuthorization()beforeUseAuthentication()or before routing means it cannot see the user or the endpoint.
Example
When to use it
- Allow GET /api/payments/report for users in either the finance or the admin role
- Require both the finance role and the payments:refund scope for refunds through one named policy
- Accept only bearer tokens, never browser cookies, on an integration controller in an app that uses both
Frequently asked questions
Why do I get 403 although my token has the role?
The identity's RoleClaimType does not match the claim the token uses (for example roles versus the long ClaimTypes.Role URI). Set TokenValidationParameters.RoleClaimType, or check the claims in HttpContext.User while debugging.
Can an action make its controller's [Authorize] less strict?
No. Requirements combine with AND. Move the action to another controller, or use [AllowAnonymous] if it must be completely public.
Do I need to call UseAuthentication and UseAuthorization?
WebApplication adds both automatically when the services are registered. If you call them yourself, put them after UseRouting (or anywhere for WebApplication's implicit routing) and authentication before authorization.
Is [Authorize] enough for multi-tenant data?
No. It checks who the caller is, not which rows they may touch. Filter by tenant in queries and use resource-based authorization for individual records.
More examples
Roles from a JWT that uses a "roles" claim
<code>Roles</code> checks <code>ClaimsPrincipal.IsInRole</code>, which compares against the identity's <code>RoleClaimType</code>. If the token carries roles in a <code>roles</code> claim but the identity expects a different claim type, every role check fails with 403 even though the token visibly contains the role. Set <code>RoleClaimType</code> to match the issuer.
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = builder.Configuration["Auth:Authority"];
options.Audience = "orders-api";
options.MapInboundClaims = false; // keep claim names as issued
options.TokenValidationParameters.RoleClaimType = "roles";
options.TokenValidationParameters.NameClaimType = "sub";
});
builder.Services.AddAuthorization();
var app = builder.Build();
app.MapDelete("/orders/{id:int}", [Authorize(Roles = "orders.admin")] (int id) => TypedResults.NoContent());
app.Run();
Requiring a specific scheme on one endpoint
The default scheme is cookies for the browser app, but <code>AuthenticationSchemes</code> makes this controller authenticate only with bearer tokens. A logged-in browser session therefore cannot call the integration endpoint, which closes a cross-site request forgery path.
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie()
.AddJwtBearer(o => o.Authority = builder.Configuration["Auth:Authority"]);
builder.Services.AddAuthorization();
var app = builder.Build();
app.MapControllers();
app.Run();
[ApiController]
[Route("api/integrations")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public class IntegrationsController : ControllerBase
{
// Machine clients only: a browser cookie is not accepted here.
[HttpPost("orders")]
public IActionResult Import() => Accepted();
}
Minimal API: attribute or RequireAuthorization
<code>[Authorize]</code> on a lambda and <code>.RequireAuthorization()</code> add the same metadata, and requirements from the group and the endpoint combine with AND. Pick one style per codebase; the fluent form keeps the rules next to the route when lambdas grow long.
using Microsoft.AspNetCore.Authorization;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication().AddJwtBearer(o => o.MapInboundClaims = false);
// Scopes arrive as one space-separated claim, so split it instead of using RequireClaim.
builder.Services.AddAuthorizationBuilder()
.AddPolicy("InventoryWrite", p => p.RequireAssertion(context =>
context.User.FindAll(c => c.Type is "scope" or "scp")
.Any(c => c.Value.Split(' ', StringSplitOptions.RemoveEmptyEntries).Contains("inventory:write"))));
var app = builder.Build();
var inventory = app.MapGroup("/inventory").RequireAuthorization(); // every endpoint: authenticated
inventory.MapGet("/{sku}", (string sku) => TypedResults.Ok(new { sku, quantity = 12 }));
// Group requirement AND this policy.
inventory.MapPut("/{sku}", [Authorize(Policy = "InventoryWrite")] (string sku, int quantity) =>
TypedResults.NoContent());
// Same effect with the fluent API instead of the attribute.
inventory.MapDelete("/{sku}", (string sku) => TypedResults.NoContent())
.RequireAuthorization("InventoryWrite");
app.Run();
Discussion