Aspnetcore Authorization

作者 DuendeSoftwarefb32edc51982无许可证9 个星标收录于 2026年10月8日更新于 2026年10月8日仓库4周前更新

ASP.NET Core authorization patterns including policy-based authorization, IAuthorizationHandler implementations, scope-based authorization for APIs, authorization middleware configuration, and minimal API authorization.

AI 生成的概览

提供 ASP.NET Core 授权实现的参考指南,涵盖策略、处理程序、作用域和基于资源的检查。

功能
该技能提供 ASP.NET Core 授权的参考模式和代码示例。内容涵盖基于策略的授权、基于作用域的 API 保护、自定义 IAuthorizationHandler 实现、使用 IAuthorizationService 的基于资源的授权、Minimal API 授权以及回退/默认策略。它还列出了常见陷阱,例如未注册处理程序、误用 context.Fail(),以及只检查客户端作用域而不检查用户声明。
适用场景
适用于在 ASP.NET Core 应用中实现或审查授权时使用,例如保护 API 端点、编写自定义授权处理程序,或为控制器、Razor Pages 或 Minimal API 配置策略。在需要将 OAuth 作用域与用户声明结合,或根据 OIDC 声明实施基于角色的访问控制时也很有用。
运行要求
不需要脚本或工具,仅为说明和代码示例。应用这些示例需要 ASP.NET Core 项目;对于基于作用域的模式,还需要 OAuth/OIDC 令牌颁发方(如 IdentityServer)。文档中包含指向外部文档站点的链接。

ASP.NET Core Authorization

When to Use This Skill

Use this skill when:

  • Implementing policy-based authorization in ASP.NET Core
  • Protecting API endpoints with scope-based or claim-based checks
  • Writing custom IAuthorizationHandler implementations
  • Configuring authorization for Minimal APIs, controllers, or Razor Pages
  • Enforcing role-based access control using OIDC claims
  • Combining multiple authorization requirements into composite policies

Core Principles

  1. Policy-Based Over Role-Based — Use authorization policies instead of [Authorize(Roles = "...")]. Policies are composable, testable, and decoupled from claim types.
  2. Scope ≠ Permission — OAuth scopes represent what the client is allowed to do. User claims represent what the user is allowed to do. Combine both for proper API authorization.
  3. Authorization is Separate from Authentication — Authentication (see aspnetcore-authentication) establishes identity. Authorization decides access based on that identity.
  4. Fail Closed — Default to denying access. Require explicit authorization on all endpoints.
  5. Resource-Based When Needed — For decisions that depend on the resource being accessed (e.g., "can this user edit this document?"), use IAuthorizationService with resource-based authorization.

Related Skills

  • aspnetcore-authentication — Authentication middleware that provides the identity
  • claims-authorization — Advanced claims transformation and authorization patterns
  • identityserver-configuration — Server-side scope and resource configuration
  • oauth-oidc-protocols — Understanding scopes, claims, and token contents

Docs: https://docs.duendesoftware.com/identityserver/apis/aspnetcore/authorization/


Pattern 1: Basic Policy-Based Authorization

Define policies at startup and reference them on endpoints:

csharp
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(options =>{    // Policy that requires the user to be authenticated    options.FallbackPolicy = new AuthorizationPolicyBuilder()        .RequireAuthenticatedUser()        .Build();
    // Policy requiring a specific scope in the access token    options.AddPolicy("read:catalog", policy =>        policy.RequireClaim("scope", "catalog.read"));
    // Policy requiring a specific role    options.AddPolicy("admin", policy =>        policy.RequireRole("admin"));
    // Policy combining multiple requirements    options.AddPolicy("catalog-editor", policy =>    {        policy.RequireAuthenticatedUser();        policy.RequireClaim("scope", "catalog.write");        policy.RequireClaim("department", "merchandising");    });});

Applying Policies

csharp
// Minimal APIapp.MapGet("/products", () => Results.Ok())    .RequireAuthorization("read:catalog");
// Controller[Authorize(Policy = "catalog-editor")]public class CatalogController : ControllerBase { }
// Razor Page[Authorize(Policy = "admin")]public class AdminModel : PageModel { }

Pattern 2: Scope-Based Authorization for APIs

APIs protected by IdentityServer need to validate scopes from the access token. Scopes represent what the client application is permitted to do.

Simple Scope Check

csharp
builder.Services.AddAuthorization(options =>{    options.AddPolicy("api.read", policy =>        policy.RequireClaim("scope", "catalog.read"));
    options.AddPolicy("api.write", policy =>        policy.RequireClaim("scope", "catalog.write"));});
app.MapGet("/products", GetProducts).RequireAuthorization("api.read");app.MapPost("/products", CreateProduct).RequireAuthorization("api.write");

Scope as Space-Delimited String

When EmitScopesAsSpaceDelimitedStringInJwt = true on IdentityServer, scopes arrive as a single space-delimited string rather than an array. Use a custom handler:

csharp
public class ScopeRequirement : IAuthorizationRequirement{    public string Scope { get; }    public ScopeRequirement(string scope) => Scope = scope;}
public class ScopeHandler : AuthorizationHandler<ScopeRequirement>{    protected override Task HandleRequirementAsync(        AuthorizationHandlerContext context,        ScopeRequirement requirement)    {        var scopeClaim = context.User.FindFirst("scope");        if (scopeClaim is null)        {            return Task.CompletedTask; // Not handled = denied        }
        // Handle both array claims and space-delimited string        var scopes = scopeClaim.Value.Split(' ', StringSplitOptions.RemoveEmptyEntries);        if (scopes.Contains(requirement.Scope))        {            context.Succeed(requirement);        }
        return Task.CompletedTask;    }}
// Registrationbuilder.Services.AddSingleton<IAuthorizationHandler, ScopeHandler>();builder.Services.AddAuthorization(options =>{    options.AddPolicy("catalog.read", policy =>        policy.Requirements.Add(new ScopeRequirement("catalog.read")));});

Pattern 3: Custom Authorization Handlers

For complex authorization logic, implement IAuthorizationHandler:

csharp
// Requirement — what needs to be satisfiedpublic class MinimumTenureRequirement : IAuthorizationRequirement{    public int MinimumYears { get; }    public MinimumTenureRequirement(int years) => MinimumYears = years;}
// Handler — how to evaluate the requirementpublic class MinimumTenureHandler : AuthorizationHandler<MinimumTenureRequirement>{    protected override Task HandleRequirementAsync(        AuthorizationHandlerContext context,        MinimumTenureRequirement requirement)    {        var hireDateClaim = context.User.FindFirst("hire_date");        if (hireDateClaim is null)        {            return Task.CompletedTask;        }
        if (DateTimeOffset.TryParse(hireDateClaim.Value, out var hireDate))        {            var tenure = DateTimeOffset.UtcNow - hireDate;            if (tenure.TotalDays >= requirement.MinimumYears * 365.25)            {                context.Succeed(requirement);            }        }
        return Task.CompletedTask;    }}
// Registrationbuilder.Services.AddSingleton<IAuthorizationHandler, MinimumTenureHandler>();builder.Services.AddAuthorization(options =>{    options.AddPolicy("senior-staff", policy =>        policy.Requirements.Add(new MinimumTenureRequirement(5)));});

Multiple Handlers for One Requirement

When any handler succeeding should grant access (OR logic):

csharp
// Both handlers evaluate the same requirement// If EITHER succeeds, the requirement is satisfiedpublic class AdminByRoleHandler : AuthorizationHandler<AdminRequirement>{    protected override Task HandleRequirementAsync(        AuthorizationHandlerContext context, AdminRequirement requirement)    {        if (context.User.IsInRole("admin"))            context.Succeed(requirement);        return Task.CompletedTask;    }}
public class AdminByDepartmentHandler : AuthorizationHandler<AdminRequirement>{    protected override Task HandleRequirementAsync(        AuthorizationHandlerContext context, AdminRequirement requirement)    {        if (context.User.HasClaim("department", "it-operations"))            context.Succeed(requirement);        return Task.CompletedTask;    }}

Key concept: Multiple requirements in a policy use AND logic (all must be satisfied). Multiple handlers for the same requirement use OR logic (any can satisfy it).


Pattern 4: Resource-Based Authorization

When authorization depends on the resource being accessed, use IAuthorizationService:

csharp
public class DocumentAuthorizationHandler    : AuthorizationHandler<OperationAuthorizationRequirement, Document>{    protected override Task HandleRequirementAsync(        AuthorizationHandlerContext context,        OperationAuthorizationRequirement requirement,        Document resource)    {        var userId = context.User.FindFirst("sub")?.Value;
        if (requirement == Operations.Read)        {            // Anyone in the same department can read            if (context.User.HasClaim("department", resource.Department))                context.Succeed(requirement);        }        else if (requirement == Operations.Edit)        {            // Only the owner can edit            if (resource.OwnerId == userId)                context.Succeed(requirement);        }
        return Task.CompletedTask;    }}
public static class Operations{    public static readonly OperationAuthorizationRequirement Read = new() { Name = nameof(Read) };    public static readonly OperationAuthorizationRequirement Edit = new() { Name = nameof(Edit) };}

Using in a Controller

csharp
public class DocumentsController : ControllerBase{    private readonly IAuthorizationService _authz;    private readonly IDocumentRepository _docs;
    public DocumentsController(IAuthorizationService authz, IDocumentRepository docs)    {        _authz = authz;        _docs = docs;    }
    [HttpGet("{id}")]    public async Task<IActionResult> Get(string id)    {        var document = await _docs.GetAsync(id);        if (document is null) return NotFound();
        var result = await _authz.AuthorizeAsync(            User,            document,            Operations.Read);
        if (!result.Succeeded) return Forbid();
        return Ok(document);    }}

Pattern 5: Minimal API Authorization

Minimal APIs use the same authorization system with a fluent API:

csharp
// Require authentication on all endpoints by defaultapp.MapGet("/public", () => "Anyone can see this")    .AllowAnonymous();
app.MapGet("/products", GetProducts)    .RequireAuthorization("read:catalog");
app.MapPost("/products", CreateProduct)    .RequireAuthorization("catalog-editor");
// Inline policyapp.MapDelete("/products/{id}", DeleteProduct)    .RequireAuthorization(policy =>        policy.RequireClaim("scope", "catalog.write")              .RequireRole("admin"));
// Group-level authorizationvar adminGroup = app.MapGroup("/admin")    .RequireAuthorization("admin");
adminGroup.MapGet("/users", GetUsers);adminGroup.MapPost("/users", CreateUser);

Pattern 6: Combining Client Scope + User Claims

In APIs protected by IdentityServer, proper authorization often requires checking both the client's scope and the user's claims:

csharp
public class ApiWriteRequirement : IAuthorizationRequirement { }
public class ApiWriteHandler : AuthorizationHandler<ApiWriteRequirement>{    protected override Task HandleRequirementAsync(        AuthorizationHandlerContext context,        ApiWriteRequirement requirement)    {        // Check 1: Client must have the write scope        var hasScope = context.User.HasClaim(c =>            c.Type == "scope" && c.Value.Split(' ').Contains("catalog.write"));
        // Check 2: User must be in the editor role        var isEditor = context.User.IsInRole("editor");
        if (hasScope && isEditor)        {            context.Succeed(requirement);        }
        return Task.CompletedTask;    }}

Why both? A malicious client could request broad scopes, but the user may not have permission. A privileged user operating through a restricted client should be limited by that client's scopes.


Pattern 7: Fallback and Default Policies

csharp
builder.Services.AddAuthorization(options =>{    // DefaultPolicy: applied when [Authorize] has no policy name    options.DefaultPolicy = new AuthorizationPolicyBuilder()        .RequireAuthenticatedUser()        .Build();
    // FallbackPolicy: applied to endpoints with NO [Authorize] attribute    // Setting this makes all endpoints require authentication by default    options.FallbackPolicy = new AuthorizationPolicyBuilder()        .RequireAuthenticatedUser()        .Build();});
PolicyApplied WhenUse Case
DefaultPolicy[Authorize] with no policy nameBasic "must be logged in" check
FallbackPolicyEndpoints with no [Authorize] attributeSecure-by-default for APIs

Tip: Set FallbackPolicy to require authentication, then use [AllowAnonymous] only on endpoints that genuinely need it (health checks, public assets).


Common Pitfalls

1. Using Role Strings Instead of Policies

csharp
// ❌ WRONG — Hardcoded role strings scattered across controllers[Authorize(Roles = "admin,superadmin,it-ops")]public IActionResult Dashboard() { }
// ✅ CORRECT — Centralized policyoptions.AddPolicy("dashboard-access", policy =>    policy.RequireRole("admin", "superadmin", "it-ops"));
[Authorize(Policy = "dashboard-access")]public IActionResult Dashboard() { }

2. Not Registering Authorization Handlers

csharp
// ❌ WRONG — Handler exists but never registered// Policy silently denies because no handler evaluates the requirement
// ✅ CORRECT — Register the handler in DIbuilder.Services.AddSingleton<IAuthorizationHandler, ScopeHandler>();

3. Calling context.Fail() in Handlers

context.Fail() actively denies authorization regardless of what other handlers say — it's a hard veto. Not calling context.Succeed() simply means "I have no opinion"; other handlers can still satisfy the requirement.

csharp
// ❌ WRONG — Fail() is a hard veto: it denies even if another handler would succeedprotected override Task HandleRequirementAsync(...){    if (!context.User.HasClaim("scope", "api.read"))        context.Fail(); // Forces denial — blocks all other handlers permanently!    return Task.CompletedTask;}
// ✅ CORRECT — Simply don't call Succeed(); let other handlers tryprotected override Task HandleRequirementAsync(...){    if (context.User.HasClaim("scope", "api.read"))        context.Succeed(requirement);    // Not calling Succeed() means "I don't know" — other handlers may still succeed    return Task.CompletedTask;}

Only call context.Fail() when you need to guarantee denial even if other handlers would approve (e.g., a security blocklist check). In most cases, simply omit the Succeed() call.

4. Ignoring Client vs User Authorization

csharp
// ❌ WRONG — Only checking user role, ignoring client scopeoptions.AddPolicy("write", p => p.RequireRole("editor"));// A client without the write scope could still pass this check
// ✅ CORRECT — Check both scope and user claimsoptions.AddPolicy("write", p =>{    p.RequireClaim("scope", "catalog.write"); // Client permission    p.RequireRole("editor");                   // User permission});

Resources

来源与署名

来源:DuendeSoftware/duende-skills位于skills/aspnetcore-authorization提交fb32edc

许可证: 无许可证

内容归原作者所有。SourceWeft 从公开仓库中收录这些内容。

举报或申请下架