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
| Concept | Question | Example |
|---|---|---|
| Authentication | Who are you? | JWT token identifies the user as admin@bookstore.com |
| Authorization | What can you do? | Admin role can DELETE; User role cannot |
Roles in the BookStore API
| Role | Allowed Actions |
|---|---|
| Admin | GET, POST, PUT, DELETE (full access) |
| User | GET only (read-only access) |
| Anonymous | GET /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 Code | Meaning | BookStore Example |
|---|---|---|
| 401 Unauthorized | No token or token is invalid/expired | Request sent without Authorization header |
| 403 Forbidden | Valid token, but user does not have permission | User 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.
