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



























