Skip to content
Back to skills

Aspnetcore Authorization

ASecurity

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

  • 9 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added October 16, 2026
ai-agentsapisecurity

Works with

  • cli
  • api

Security analysis

A100/100

Scanned October 16, 2026

npx -y skills add DuendeSoftware/duende-skills --skill aspnetcore-authorization --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Aspnetcore Authorization?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Aspnetcore Authorization
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/duendesoftware-aspnetcore-authorization/badge)](https://www.skillsdirectory.com/skills/duendesoftware-aspnetcore-authorization)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
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.
invocable: false
---

# 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 API
app.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;
    }
}

// Registration
builder.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 satisfied
public class MinimumTenureRequirement : IAuthorizationRequirement
{
    public int MinimumYears { get; }
    public MinimumTenureRequirement(int years) => MinimumYears = years;
}

// Handler — how to evaluate the requirement
public 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;
    }
}

// Registration
builder.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 satisfied
public 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 default
app.MapGet("/public", () => "Anyone can see this")
    .AllowAnonymous();

app.MapGet("/products", GetProducts)
    .RequireAuthorization("read:catalog");

app.MapPost("/products", CreateProduct)
    .RequireAuthorization("catalog-editor");

// Inline policy
app.MapDelete("/products/{id}", DeleteProduct)
    .RequireAuthorization(policy =>
        policy.RequireClaim("scope", "catalog.write")
              .RequireRole("admin"));

// Group-level authorization
var 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();
});
```

| Policy | Applied When | Use Case |
|--------|-------------|----------|
| `DefaultPolicy` | `[Authorize]` with no policy name | Basic "must be logged in" check |
| `FallbackPolicy` | Endpoints with no `[Authorize]` attribute | Secure-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 policy
options.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 DI
builder.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 succeed
protected 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 try
protected 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 scope
options.AddPolicy("write", p => p.RequireRole("editor"));
// A client without the write scope could still pass this check

// ✅ CORRECT — Check both scope and user claims
options.AddPolicy("write", p =>
{
    p.RequireClaim("scope", "catalog.write"); // Client permission
    p.RequireRole("editor");                   // User permission
});
```

---

## Resources

- [Authorization in ASP.NET Core — Microsoft Docs](https://learn.microsoft.com/aspnet/core/security/authorization/introduction)
- [Policy-Based Authorization — Microsoft Docs](https://learn.microsoft.com/aspnet/core/security/authorization/policies)
- [Resource-Based Authorization — Microsoft Docs](https://learn.microsoft.com/aspnet/core/security/authorization/resourcebased)
- [Protecting APIs — Duende Docs](https://docs.duendesoftware.com/identityserver/apis/)
- [API Authorization — Duende Docs](https://docs.duendesoftware.com/identityserver/apis/aspnetcore/authorization/)

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.