← Duende SkillsCONTENT HISTORY

Update to Duende Skills

Snapshot Sep 30, 2026 · 23:14 UTC · version 0.3.0

Collection source: not recorded for this historical snapshot.

WHAT CHANGED · RULE-BASED ANALYSIS

First saved snapshot

No earlier snapshot is available to establish a change.

Compare saved observations

Download comparison JSON
Full technical diff · 0 changed fields
Full snapshot data
{
  "name": "aspnetcore-authorization",
  "description": "ASP.NET Core authorization patterns including policy-based authorization, IAuthorizationHandler implementations, scope-based authorization for APIs, authorization middleware configuration, and minimal API authorization.",
  "included_files": [],
  "skill_md_contents": "---\nname: aspnetcore-authorization\ndescription: ASP.NET Core authorization patterns including policy-based authorization, IAuthorizationHandler implementations, scope-based authorization for APIs, authorization middleware configuration, and minimal API authorization.\ninvocable: false\n---\n\n# ASP.NET Core Authorization\n\n## When to Use This Skill\n\nUse this skill when:\n- Implementing policy-based authorization in ASP.NET Core\n- Protecting API endpoints with scope-based or claim-based checks\n- Writing custom `IAuthorizationHandler` implementations\n- Configuring authorization for Minimal APIs, controllers, or Razor Pages\n- Enforcing role-based access control using OIDC claims\n- Combining multiple authorization requirements into composite policies\n\n## Core Principles\n\n1. **Policy-Based Over Role-Based** — Use authorization policies instead of `[Authorize(Roles = \"...\")]`. Policies are composable, testable, and decoupled from claim types.\n2. **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.\n3. **Authorization is Separate from Authentication** — Authentication (see `aspnetcore-authentication`) establishes identity. Authorization decides access based on that identity.\n4. **Fail Closed** — Default to denying access. Require explicit authorization on all endpoints.\n5. **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.\n\n## Related Skills\n\n- `aspnetcore-authentication` — Authentication middleware that provides the identity\n- `claims-authorization` — Advanced claims transformation and authorization patterns\n- `identityserver-configuration` — Server-side scope and resource configuration\n- `oauth-oidc-protocols` — Understanding scopes, claims, and token contents\n\nDocs: https://docs.duendesoftware.com/identityserver/apis/aspnetcore/authorization/\n\n---\n\n## Pattern 1: Basic Policy-Based Authorization\n\nDefine policies at startup and reference them on endpoints:\n\n```csharp\nvar builder = WebApplication.CreateBuilder(args);\n\nbuilder.Services.AddAuthorization(options =>\n{\n    // Policy that requires the user to be authenticated\n    options.FallbackPolicy = new AuthorizationPolicyBuilder()\n        .RequireAuthenticatedUser()\n        .Build();\n\n    // Policy requiring a specific scope in the access token\n    options.AddPolicy(\"read:catalog\", policy =>\n        policy.RequireClaim(\"scope\", \"catalog.read\"));\n\n    // Policy requiring a specific role\n    options.AddPolicy(\"admin\", policy =>\n        policy.RequireRole(\"admin\"));\n\n    // Policy combining multiple requirements\n    options.AddPolicy(\"catalog-editor\", policy =>\n    {\n        policy.RequireAuthenticatedUser();\n        policy.RequireClaim(\"scope\", \"catalog.write\");\n        policy.RequireClaim(\"department\", \"merchandising\");\n    });\n});\n```\n\n### Applying Policies\n\n```csharp\n// Minimal API\napp.MapGet(\"/products\", () => Results.Ok())\n    .RequireAuthorization(\"read:catalog\");\n\n// Controller\n[Authorize(Policy = \"catalog-editor\")]\npublic class CatalogController : ControllerBase { }\n\n// Razor Page\n[Authorize(Policy = \"admin\")]\npublic class AdminModel : PageModel { }\n```\n\n---\n\n## Pattern 2: Scope-Based Authorization for APIs\n\nAPIs protected by IdentityServer need to validate scopes from the access token. Scopes represent what the *client application* is permitted to do.\n\n### Simple Scope Check\n\n```csharp\nbuilder.Services.AddAuthorization(options =>\n{\n    options.AddPolicy(\"api.read\", policy =>\n        policy.RequireClaim(\"scope\", \"catalog.read\"));\n\n    options.AddPolicy(\"api.write\", policy =>\n        policy.RequireClaim(\"scope\", \"catalog.write\"));\n});\n\napp.MapGet(\"/products\", GetProducts).RequireAuthorization(\"api.read\");\napp.MapPost(\"/products\", CreateProduct).RequireAuthorization(\"api.write\");\n```\n\n### Scope as Space-Delimited String\n\nWhen `EmitScopesAsSpaceDelimitedStringInJwt = true` on IdentityServer, scopes arrive as a single space-delimited string rather than an array. Use a custom handler:\n\n```csharp\npublic class ScopeRequirement : IAuthorizationRequirement\n{\n    public string Scope { get; }\n    public ScopeRequirement(string scope) => Scope = scope;\n}\n\npublic class ScopeHandler : AuthorizationHandler<ScopeRequirement>\n{\n    protected override Task HandleRequirementAsync(\n        AuthorizationHandlerContext context,\n        ScopeRequirement requirement)\n    {\n        var scopeClaim = context.User.FindFirst(\"scope\");\n        if (scopeClaim is null)\n        {\n            return Task.CompletedTask; // Not handled = denied\n        }\n\n        // Handle both array claims and space-delimited string\n        var scopes = scopeClaim.Value.Split(' ', StringSplitOptions.RemoveEmptyEntries);\n        if (scopes.Contains(requirement.Scope))\n        {\n            context.Succeed(requirement);\n        }\n\n        return Task.CompletedTask;\n    }\n}\n\n// Registration\nbuilder.Services.AddSingleton<IAuthorizationHandler, ScopeHandler>();\nbuilder.Services.AddAuthorization(options =>\n{\n    options.AddPolicy(\"catalog.read\", policy =>\n        policy.Requirements.Add(new ScopeRequirement(\"catalog.read\")));\n});\n```\n\n---\n\n## Pattern 3: Custom Authorization Handlers\n\nFor complex authorization logic, implement `IAuthorizationHandler`:\n\n```csharp\n// Requirement — what needs to be satisfied\npublic class MinimumTenureRequirement : IAuthorizationRequirement\n{\n    public int MinimumYears { get; }\n    public MinimumTenureRequirement(int years) => MinimumYears = years;\n}\n\n// Handler — how to evaluate the requirement\npublic class MinimumTenureHandler : AuthorizationHandler<MinimumTenureRequirement>\n{\n    protected override Task HandleRequirementAsync(\n        AuthorizationHandlerContext context,\n        MinimumTenureRequirement requirement)\n    {\n        var hireDateClaim = context.User.FindFirst(\"hire_date\");\n        if (hireDateClaim is null)\n        {\n            return Task.CompletedTask;\n        }\n\n        if (DateTimeOffset.TryParse(hireDateClaim.Value, out var hireDate))\n        {\n            var tenure = DateTimeOffset.UtcNow - hireDate;\n            if (tenure.TotalDays >= requirement.MinimumYears * 365.25)\n            {\n                context.Succeed(requirement);\n            }\n        }\n\n        return Task.CompletedTask;\n    }\n}\n\n// Registration\nbuilder.Services.AddSingleton<IAuthorizationHandler, MinimumTenureHandler>();\nbuilder.Services.AddAuthorization(options =>\n{\n    options.AddPolicy(\"senior-staff\", policy =>\n        policy.Requirements.Add(new MinimumTenureRequirement(5)));\n});\n```\n\n### Multiple Handlers for One Requirement\n\nWhen any handler succeeding should grant access (OR logic):\n\n```csharp\n// Both handlers evaluate the same requirement\n// If EITHER succeeds, the requirement is satisfied\npublic class AdminByRoleHandler : AuthorizationHandler<AdminRequirement>\n{\n    protected override Task HandleRequirementAsync(\n        AuthorizationHandlerContext context, AdminRequirement requirement)\n    {\n        if (context.User.IsInRole(\"admin\"))\n            context.Succeed(requirement);\n        return Task.CompletedTask;\n    }\n}\n\npublic class AdminByDepartmentHandler : AuthorizationHandler<AdminRequirement>\n{\n    protected override Task HandleRequirementAsync(\n        AuthorizationHandlerContext context, AdminRequirement requirement)\n    {\n        if (context.User.HasClaim(\"department\", \"it-operations\"))\n            context.Succeed(requirement);\n        return Task.CompletedTask;\n    }\n}\n```\n\n> **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).\n\n---\n\n## Pattern 4: Resource-Based Authorization\n\nWhen authorization depends on the resource being accessed, use `IAuthorizationService`:\n\n```csharp\npublic class DocumentAuthorizationHandler\n    : AuthorizationHandler<OperationAuthorizationRequirement, Document>\n{\n    protected override Task HandleRequirementAsync(\n        AuthorizationHandlerContext context,\n        OperationAuthorizationRequirement requirement,\n        Document resource)\n    {\n        var userId = context.User.FindFirst(\"sub\")?.Value;\n\n        if (requirement == Operations.Read)\n        {\n            // Anyone in the same department can read\n            if (context.User.HasClaim(\"department\", resource.Department))\n                context.Succeed(requirement);\n        }\n        else if (requirement == Operations.Edit)\n        {\n            // Only the owner can edit\n            if (resource.OwnerId == userId)\n                context.Succeed(requirement);\n        }\n\n        return Task.CompletedTask;\n    }\n}\n\npublic static class Operations\n{\n    public static readonly OperationAuthorizationRequirement Read = new() { Name = nameof(Read) };\n    public static readonly OperationAuthorizationRequirement Edit = new() { Name = nameof(Edit) };\n}\n```\n\n### Using in a Controller\n\n```csharp\npublic class DocumentsController : ControllerBase\n{\n    private readonly IAuthorizationService _authz;\n    private readonly IDocumentRepository _docs;\n\n    public DocumentsController(IAuthorizationService authz, IDocumentRepository docs)\n    {\n        _authz = authz;\n        _docs = docs;\n    }\n\n    [HttpGet(\"{id}\")]\n    public async Task<IActionResult> Get(string id)\n    {\n        var document = await _docs.GetAsync(id);\n        if (document is null) return NotFound();\n\n        var result = await _authz.AuthorizeAsync(\n            User,\n            document,\n            Operations.Read);\n\n        if (!result.Succeeded) return Forbid();\n\n        return Ok(document);\n    }\n}\n```\n\n---\n\n## Pattern 5: Minimal API Authorization\n\nMinimal APIs use the same authorization system with a fluent API:\n\n```csharp\n// Require authentication on all endpoints by default\napp.MapGet(\"/public\", () => \"Anyone can see this\")\n    .AllowAnonymous();\n\napp.MapGet(\"/products\", GetProducts)\n    .RequireAuthorization(\"read:catalog\");\n\napp.MapPost(\"/products\", CreateProduct)\n    .RequireAuthorization(\"catalog-editor\");\n\n// Inline policy\napp.MapDelete(\"/products/{id}\", DeleteProduct)\n    .RequireAuthorization(policy =>\n        policy.RequireClaim(\"scope\", \"catalog.write\")\n              .RequireRole(\"admin\"));\n\n// Group-level authorization\nvar adminGroup = app.MapGroup(\"/admin\")\n    .RequireAuthorization(\"admin\");\n\nadminGroup.MapGet(\"/users\", GetUsers);\nadminGroup.MapPost(\"/users\", CreateUser);\n```\n\n---\n\n## Pattern 6: Combining Client Scope + User Claims\n\nIn APIs protected by IdentityServer, proper authorization often requires checking *both* the client's scope and the user's claims:\n\n```csharp\npublic class ApiWriteRequirement : IAuthorizationRequirement { }\n\npublic class ApiWriteHandler : AuthorizationHandler<ApiWriteRequirement>\n{\n    protected override Task HandleRequirementAsync(\n        AuthorizationHandlerContext context,\n        ApiWriteRequirement requirement)\n    {\n        // Check 1: Client must have the write scope\n        var hasScope = context.User.HasClaim(c =>\n            c.Type == \"scope\" && c.Value.Split(' ').Contains(\"catalog.write\"));\n\n        // Check 2: User must be in the editor role\n        var isEditor = context.User.IsInRole(\"editor\");\n\n        if (hasScope && isEditor)\n        {\n            context.Succeed(requirement);\n        }\n\n        return Task.CompletedTask;\n    }\n}\n```\n\n> **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.\n\n---\n\n## Pattern 7: Fallback and Default Policies\n\n```csharp\nbuilder.Services.AddAuthorization(options =>\n{\n    // DefaultPolicy: applied when [Authorize] has no policy name\n    options.DefaultPolicy = new AuthorizationPolicyBuilder()\n        .RequireAuthenticatedUser()\n        .Build();\n\n    // FallbackPolicy: applied to endpoints with NO [Authorize] attribute\n    // Setting this makes all endpoints require authentication by default\n    options.FallbackPolicy = new AuthorizationPolicyBuilder()\n        .RequireAuthenticatedUser()\n        .Build();\n});\n```\n\n| Policy | Applied When | Use Case |\n|--------|-------------|----------|\n| `DefaultPolicy` | `[Authorize]` with no policy name | Basic \"must be logged in\" check |\n| `FallbackPolicy` | Endpoints with no `[Authorize]` attribute | Secure-by-default for APIs |\n\n> **Tip:** Set `FallbackPolicy` to require authentication, then use `[AllowAnonymous]` only on endpoints that genuinely need it (health checks, public assets).\n\n---\n\n## Common Pitfalls\n\n### 1. Using Role Strings Instead of Policies\n\n```csharp\n// ❌ WRONG — Hardcoded role strings scattered across controllers\n[Authorize(Roles = \"admin,superadmin,it-ops\")]\npublic IActionResult Dashboard() { }\n\n// ✅ CORRECT — Centralized policy\noptions.AddPolicy(\"dashboard-access\", policy =>\n    policy.RequireRole(\"admin\", \"superadmin\", \"it-ops\"));\n\n[Authorize(Policy = \"dashboard-access\")]\npublic IActionResult Dashboard() { }\n```\n\n### 2. Not Registering Authorization Handlers\n\n```csharp\n// ❌ WRONG — Handler exists but never registered\n// Policy silently denies because no handler evaluates the requirement\n\n// ✅ CORRECT — Register the handler in DI\nbuilder.Services.AddSingleton<IAuthorizationHandler, ScopeHandler>();\n```\n\n### 3. Calling context.Fail() in Handlers\n\n`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.\n\n```csharp\n// ❌ WRONG — Fail() is a hard veto: it denies even if another handler would succeed\nprotected override Task HandleRequirementAsync(...)\n{\n    if (!context.User.HasClaim(\"scope\", \"api.read\"))\n        context.Fail(); // Forces denial — blocks all other handlers permanently!\n    return Task.CompletedTask;\n}\n\n// ✅ CORRECT — Simply don't call Succeed(); let other handlers try\nprotected override Task HandleRequirementAsync(...)\n{\n    if (context.User.HasClaim(\"scope\", \"api.read\"))\n        context.Succeed(requirement);\n    // Not calling Succeed() means \"I don't know\" — other handlers may still succeed\n    return Task.CompletedTask;\n}\n```\n\n> 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.\n\n### 4. Ignoring Client vs User Authorization\n\n```csharp\n// ❌ WRONG — Only checking user role, ignoring client scope\noptions.AddPolicy(\"write\", p => p.RequireRole(\"editor\"));\n// A client without the write scope could still pass this check\n\n// ✅ CORRECT — Check both scope and user claims\noptions.AddPolicy(\"write\", p =>\n{\n    p.RequireClaim(\"scope\", \"catalog.write\"); // Client permission\n    p.RequireRole(\"editor\");                   // User permission\n});\n```\n\n---\n\n## Resources\n\n- [Authorization in ASP.NET Core — Microsoft Docs](https://learn.microsoft.com/aspnet/core/security/authorization/introduction)\n- [Policy-Based Authorization — Microsoft Docs](https://learn.microsoft.com/aspnet/core/security/authorization/policies)\n- [Resource-Based Authorization — Microsoft Docs](https://learn.microsoft.com/aspnet/core/security/authorization/resourcebased)\n- [Protecting APIs — Duende Docs](https://docs.duendesoftware.com/identityserver/apis/)\n- [API Authorization — Duende Docs](https://docs.duendesoftware.com/identityserver/apis/aspnetcore/authorization/)\n"
}

SHA-256: ceb14046b7721b02977722e75c8bedd91117d64ecd3a288c34a8826804d757a6