Core API Authorization and Role Based Access

Authentication answers "Who are you?" — Authorization answers "What are you allowed to do?" The BookStore API needs role-based authorization so that admins can add or delete books, while regular users can only read them.

Authentication vs Authorization

ConceptQuestionExample
AuthenticationWho are you?JWT token identifies the user as admin@bookstore.com
AuthorizationWhat can you do?Admin role can DELETE; User role cannot

Roles in the BookStore API

RoleAllowed Actions
AdminGET, POST, PUT, DELETE (full access)
UserGET only (read-only access)
AnonymousGET /api/books only (public listing)

How Roles Work with JWT

The user's role is stored inside the JWT token as a claim. When the token is created during login, the role is embedded:

// Services/TokenService.cs
var claims = new[]
{
    new Claim(JwtRegisteredClaimNames.Sub, user.Email),
    new Claim("Id", user.Id.ToString()),
    new Claim(ClaimTypes.Role, user.Role),   // ← "Admin" or "User"
    new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};

When the JWT arrives with a request, ASP.NET Core reads the role claim and makes it available in the controller through User.IsInRole("Admin") or via [Authorize(Roles = "Admin")].

Role-Based Authorization with [Authorize(Roles)]

// Controllers/BooksController.cs
using Microsoft.AspNetCore.Authorization;

[ApiController]
[Route("api/[controller]")]
public class BooksController : ControllerBase
{
    private readonly IBookService _bookService;
    private readonly IMapper _mapper;

    public BooksController(IBookService bookService, IMapper mapper)
    {
        _bookService = bookService;
        _mapper = mapper;
    }

    // Anyone can view all books
    [HttpGet]
    [AllowAnonymous]
    public async Task<IActionResult> GetAll()
    {
        var books = await _bookService.GetAllAsync();
        return Ok(_mapper.Map<List<BookResponseDto>>(books));
    }

    // Anyone can view a single book
    [HttpGet("{id:int}")]
    [AllowAnonymous]
    public async Task<IActionResult> GetById(int id)
    {
        var book = await _bookService.GetByIdAsync(id);
        if (book == null) return NotFound();
        return Ok(_mapper.Map<BookResponseDto>(book));
    }

    // Only Admin can add books
    [HttpPost]
    [Authorize(Roles = "Admin")]
    public async Task<IActionResult> Create([FromBody] BookCreateDto dto)
    {
        var book = _mapper.Map<Book>(dto);
        var created = await _bookService.CreateAsync(book);
        var result = _mapper.Map<BookResponseDto>(created);
        return CreatedAtAction(nameof(GetById), new { id = result.Id }, result);
    }

    // Only Admin can update books
    [HttpPut("{id:int}")]
    [Authorize(Roles = "Admin")]
    public async Task<IActionResult> Update(int id, [FromBody] BookUpdateDto dto)
    {
        var book = _mapper.Map<Book>(dto);
        var success = await _bookService.UpdateAsync(id, book);
        if (!success) return NotFound();
        return NoContent();
    }

    // Only Admin can delete books
    [HttpDelete("{id:int}")]
    [Authorize(Roles = "Admin")]
    public async Task<IActionResult> Delete(int id)
    {
        var success = await _bookService.DeleteAsync(id);
        if (!success) return NotFound();
        return NoContent();
    }
}

Multiple Roles on One Endpoint

Multiple roles can be allowed for a single action. The pipe | character separates roles in the same attribute:

// Both Admin and Manager can update books
[Authorize(Roles = "Admin,Manager")]
[HttpPut("{id:int}")]
public async Task<IActionResult> Update(int id, [FromBody] BookUpdateDto dto) { ... }

Policy-Based Authorization

Roles are simple, but sometimes authorization rules are more complex. Policy-based authorization allows custom logic to be defined and reused across multiple endpoints.

Define a Policy

// Program.cs
builder.Services.AddAuthorization(options =>
{
    // Policy: user must be Admin AND account must be active
    options.AddPolicy("AdminOnly", policy =>
        policy.RequireRole("Admin"));

    // Policy: user must have a specific claim
    options.AddPolicy("CanManageBooks", policy =>
        policy.RequireRole("Admin", "Manager")
              .RequireClaim("department", "books"));

    // Policy: minimum age (example of custom requirement)
    options.AddPolicy("PremiumUser", policy =>
        policy.RequireAuthenticatedUser()
              .RequireClaim("subscription", "premium"));
});

Use the Policy

[Authorize(Policy = "AdminOnly")]
[HttpDelete("{id:int}")]
public async Task<IActionResult> Delete(int id)
{
    var success = await _bookService.DeleteAsync(id);
    if (!success) return NotFound();
    return NoContent();
}

Custom Authorization Requirements

For complex logic, a custom requirement and handler can be created. This example ensures a user can only update books they added:

// Authorization/MinPriceRequirement.cs
using Microsoft.AspNetCore.Authorization;

public class MinPriceRequirement : IAuthorizationRequirement
{
    public decimal MinPrice { get; }
    public MinPriceRequirement(decimal minPrice) => MinPrice = minPrice;
}
// Authorization/MinPriceHandler.cs
using Microsoft.AspNetCore.Authorization;

public class MinPriceHandler : AuthorizationHandler<MinPriceRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context, MinPriceRequirement requirement)
    {
        // Check if the user has a premium claim to allow high-value books
        if (context.User.HasClaim(c => c.Type == "subscription" && c.Value == "premium"))
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

Accessing User Claims Inside a Controller

Inside any action method, the authenticated user's claims can be accessed through the User property:

[HttpGet("my-books")]
[Authorize]
public async Task<IActionResult> GetMyBooks()
{
    // Read the user's Id from the JWT token claims
    var userIdClaim = User.FindFirst("Id")?.Value;
    var userRole = User.FindFirst(ClaimTypes.Role)?.Value;
    var userEmail = User.FindFirst(JwtRegisteredClaimNames.Sub)?.Value;

    // Return only books that belong to this user (if tracking ownership)
    return Ok(new
    {
        UserId = userIdClaim,
        Role = userRole,
        Email = userEmail
    });
}

Testing Role-Based Access in Postman

Scenario 1: Admin logs in and deletes a book
  POST /api/auth/login  → { email: "admin@bookstore.com", password: "Admin123" }
  Token received → Role: "Admin"

  DELETE /api/books/1
  Authorization: Bearer {admin-token}
  Result: 204 No Content ✓

────────────────────────────────────────────────────────────
Scenario 2: Regular user tries to delete a book
  POST /api/auth/login  → { email: "user@bookstore.com", password: "User123" }
  Token received → Role: "User"

  DELETE /api/books/2
  Authorization: Bearer {user-token}
  Result: 403 Forbidden ✗

────────────────────────────────────────────────────────────
Scenario 3: No token sent
  DELETE /api/books/2
  (No Authorization header)
  Result: 401 Unauthorized ✗

401 vs 403 – The Difference

Status CodeMeaningBookStore Example
401 UnauthorizedNo token or token is invalid/expiredRequest sent without Authorization header
403 ForbiddenValid token, but user does not have permissionUser role tries to DELETE a book

Key Points

  • Authorization decides what an authenticated user is allowed to do — it runs after authentication.
  • [Authorize(Roles = "Admin")] restricts an endpoint to users with the Admin role.
  • The user's role is stored as a claim inside the JWT token and read automatically by ASP.NET Core.
  • Policy-based authorization enables complex rules that go beyond simple role checks.
  • 401 means the user is not identified; 403 means the user is identified but not permitted.

Leave a Comment