liveBook · Manning

Authorization

As explained in .NET in Action, Second Edition

Authorization is the process of determining which actions or resources an authenticated or anonymous principal is permitted to access.

Authentication establishes who is making an API request; authorization decides what that user may do. Requiring authentication on an entire controller still lets every authenticated user read and modify every catalog entry. To address weak web-service access control, give read and write actions different permissions. section 9.6 section 9.5

For a catalog where authenticated users may read books but only one designated user may change them:

  • Remove the controller-level [Authorize] attribute. Put [Authorize("AuthenticatedUsers")] on the GET actions and [Authorize("OnlyMe")] on the POST, PATCH, and DELETE actions. section 9.6
  • Register both named policies. Each requires an authenticated user and selects JWT bearer authentication; OnlyMe also requires a custom authorization requirement. section 9.6
  • Register an authorization handler for that requirement. It reads the authenticated user’s preferred_username claim and succeeds only when its value matches the designated email address. section 9.6
Listing 9.20 Controller action-level authorization
[ApiController]
[Route("[controller]")]
public class CatalogController : ControllerBase           #1
{
  [HttpGet]
  [Authorize("AuthenticatedUsers")]                       #2
  public IAsyncEnumerable<Book> GetBooks(
      string? titleFilter = null)
  { /*...*/ }
  
  [HttpGet("{id}")]
  [Authorize("AuthenticatedUsers")]                       #2
  public Task<Book?> GetBook(int id)
  { /*...*/ }
  
  [HttpPost]
  [Authorize("OnlyMe")]                                   #3
  public async Task<Book> CreateBook(
    BookCreateCommand command)
  { /*...*/ }
  
  [HttpPatch("{id}")]
  [ProducesResponseType(StatusCodes.Status404NotFound)]
  [ProducesResponseType(StatusCodes.Status204NoContent)]
  [Authorize("OnlyMe")]                                   #3
  public async Task<IActionResult> UpdateBook(
    int id, BookUpdateCommand command)
  { /*...*/ }
  
  [HttpDelete("{id}")]
  [ProducesResponseType(StatusCodes.Status404NotFound)]
  [ProducesResponseType(StatusCodes.Status204NoContent)]
  [Authorize("OnlyMe")]                                   #3
  public async Task<IActionResult> DeleteBook(int id)
  { /*...*/ }
}

See where action-level attributes separate read access from permission to modify the catalog. section 9.6

Listing 9.21 Creating authorization policies
using Microsoft.AspNetCore.Authorization;
  
builder.Services.AddAuthorization(options =>                 #1
{
  options.AddPolicy("AuthenticatedUsers",                    #2
    policyBuilder => {                                       #3
      policyBuilder
        .RequireAuthenticatedUser()                          #4
        .AddAuthenticationSchemes(                           #5
          JwtBearerDefaults.AuthenticationScheme);           #6
    });
  options.AddPolicy("OnlyMe",                                #7
    policyBuilder => {
      policyBuilder
        .RequireAuthenticatedUser()
        .AddAuthenticationSchemes(
          JwtBearerDefaults.AuthenticationScheme)
        .AddRequirements(new OnlyMeRequirement());           #8
    });
});
builder.Services.AddSingleton<IAuthorizationHandler,         #9
  OnlyMeRequirementHandler>();                               #10
  
var app = builder.Build();                                   #11

See how the named policies combine authentication requirements, the JWT bearer scheme, and the custom requirement. section 9.6

Listing 9.22 Custom authorization requirement and handler
using Microsoft.AspNetCore.Authorization;
  
namespace ManningBooksApi;
  
public class OnlyMeRequirement :                     #1
  IAuthorizationRequirement { }                      #2
  
public class OnlyMeRequirementHandler :
  AuthorizationHandler<OnlyMeRequirement>            #3
{
  protected override Task HandleRequirementAsync(    #4
    AuthorizationHandlerContext context,             #5
    OnlyMeRequirement requirement)
  {
    if (context.User != null)                        #6
    {
      var emailClaim = context.User
        .FindFirst("preferred_username");            #7
      if (emailClaim != null
        && emailClaim.Value == "your@email.com")     #8
      {
        context.Succeed(requirement);                #9
      }
    }
  
    return Task.CompletedTask;                       #10
  }
}

See how a requirement handler checks a claim and calls Succeed only for the designated user. section 9.6

To inspect the claim available for a policy, authenticate in Swagger UI, make a GET request, and inspect its bearer token. Prefer a local decoder such as DevToys: pasting a live token into a website carries a risk even when the site says it does not transmit the data. section 9.6

How .NET in Action, Second Edition covers it

How deep it goes
Works through action attributes, named policies, and a claim-checking handler; does not develop broader Identity account management.
Examples
Uses catalog GET, POST, PATCH, and DELETE actions plus a preferred_username claim check.
What it assumes
Assumes an API already authenticates requests with JWT bearer tokens before adding action-specific permissions.
What you can do after
Apply it via ASP.NET Core policies and an authorization handler to separate catalog readers from a designated editor.
How it connects
Connects authentication, JWT bearer tokens, claims, authorization policies, and threat modeling.

Also explained in

Compare how 28 books explain authorization →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of .NET in Action, Second Edition. The section links point to the parts of the book it draws on.