Claude
Skills
Sign in
Back

aspnetcore-authorization

Included with Lifetime
$97 forever

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

Backend & APIs

What this skill does


# 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/tokens/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;

     

Related in Backend & APIs