Core API JWT Authentication
The BookStore API is currently open — anyone can add, update, or delete books without any credentials. This topic adds JWT (JSON Web Token) authentication so that only verified users can access protected endpoints.
What Is JWT?
A JWT is a compact, self-contained token that proves a user's identity. It is a string of three Base64-encoded parts separated by dots:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 ← Header
.
eyJzdWIiOiJhZG1pbkBib29rc3RvcmUuY29tIiwiSWQiOiIxIiwicm9sZSI6IkFkbWluIiwiZXhwIjoxNzA1MzM0ODAwfQ== ← Payload
.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c ← Signature
| Part | Contains |
|---|---|
| Header | Token type (JWT) and signing algorithm (HS256) |
| Payload | Claims: user Id, email, role, expiry date |
| Signature | Encrypted using a secret key — proves the token was not tampered with |
How JWT Authentication Works
Step 1: Client logs in
POST /api/auth/login
Body: { "email": "admin@bookstore.com", "password": "Admin123" }
Step 2: Server validates credentials and returns a JWT token
Response: { "token": "eyJhbGci..." }
Step 3: Client stores the token and sends it with every future request
GET /api/books
Authorization: Bearer eyJhbGci...
Step 4: Server validates the token and identifies the user
→ Valid token → Request proceeds to controller
→ Invalid / expired token → 401 Unauthorized
Step 1 – Install Required Package
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
Step 2 – Add JWT Settings to appsettings.json
{
"ConnectionStrings": {
"BookStoreDB": "..."
},
"JwtSettings": {
"Key": "BookStoreAPISecretKey2024!@#$%^",
"Issuer": "BookStoreAPI",
"Audience": "BookStoreClient",
"ExpiresInMinutes": 60
}
}
Step 3 – Create the User Model and AuthDto
// Models/User.cs
namespace BookStoreAPI.Models
{
public class User
{
public int Id { get; set; }
public string Email { get; set; } = string.Empty;
public string PasswordHash { get; set; } = string.Empty;
public string Role { get; set; } = "User";
}
}
// DTOs/LoginDto.cs
using System.ComponentModel.DataAnnotations;
namespace BookStoreAPI.DTOs
{
public class LoginDto
{
[Required]
[EmailAddress]
public string Email { get; set; } = string.Empty;
[Required]
public string Password { get; set; } = string.Empty;
}
}
Step 4 – Create the Token Service
// Services/TokenService.cs
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using BookStoreAPI.Models;
namespace BookStoreAPI.Services
{
public class TokenService
{
private readonly IConfiguration _config;
public TokenService(IConfiguration config)
{
_config = config;
}
public string GenerateToken(User user)
{
var jwtSettings = _config.GetSection("JwtSettings");
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, user.Email),
new Claim("Id", user.Id.ToString()),
new Claim(ClaimTypes.Role, user.Role),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
var key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(jwtSettings["Key"]!));
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: jwtSettings["Issuer"],
audience: jwtSettings["Audience"],
claims: claims,
expires: DateTime.UtcNow.AddMinutes(
int.Parse(jwtSettings["ExpiresInMinutes"]!)),
signingCredentials: credentials
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}
}
Step 5 – Create the Auth Controller
// Controllers/AuthController.cs
using Microsoft.AspNetCore.Mvc;
using BookStoreAPI.DTOs;
using BookStoreAPI.Models;
using BookStoreAPI.Services;
namespace BookStoreAPI.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
private readonly TokenService _tokenService;
// Simulating a user store (in production, use a database)
private static List<User> _users = new()
{
new User { Id = 1, Email = "admin@bookstore.com",
PasswordHash = "Admin123", Role = "Admin" },
new User { Id = 2, Email = "user@bookstore.com",
PasswordHash = "User123", Role = "User" }
};
public AuthController(TokenService tokenService)
{
_tokenService = tokenService;
}
[HttpPost("login")]
public IActionResult Login([FromBody] LoginDto dto)
{
// In production, compare hashed passwords (BCrypt)
var user = _users.FirstOrDefault(u =>
u.Email == dto.Email && u.PasswordHash == dto.Password);
if (user == null)
return Unauthorized("Invalid email or password.");
var token = _tokenService.GenerateToken(user);
return Ok(new { token });
}
}
}
Step 6 – Configure JWT in Program.cs
// Program.cs
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
var jwtSettings = builder.Configuration.GetSection("JwtSettings");
var key = Encoding.UTF8.GetBytes(jwtSettings["Key"]!);
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = jwtSettings["Issuer"],
ValidAudience = jwtSettings["Audience"],
IssuerSigningKey = new SymmetricSecurityKey(key)
};
});
builder.Services.AddAuthorization();
builder.Services.AddScoped<TokenService>();
Step 7 – Protect API Endpoints
Add [Authorize] to protect specific actions or the entire controller:
// Controllers/BooksController.cs
using Microsoft.AspNetCore.Authorization;
[ApiController]
[Route("api/[controller]")]
[Authorize] // ← Entire controller requires authentication
public class BooksController : ControllerBase
{
// GET /api/books — any authenticated user
[HttpGet]
[AllowAnonymous] // ← Override: this endpoint is public
public async Task<IActionResult> GetAll() { ... }
// GET /api/books/1 — any authenticated user
[HttpGet("{id:int}")]
[AllowAnonymous]
public async Task<IActionResult> GetById(int id) { ... }
// POST, PUT, DELETE — authentication required
[HttpPost]
public async Task<IActionResult> Create([FromBody] BookCreateDto dto) { ... }
[HttpPut("{id:int}")]
public async Task<IActionResult> Update(int id, [FromBody] BookUpdateDto dto) { ... }
[HttpDelete("{id:int}")]
public async Task<IActionResult> Delete(int id) { ... }
}
Testing JWT with Postman
1. Get the token
POST /api/auth/login
{
"email": "admin@bookstore.com",
"password": "Admin123"
}
Response: 200 OK
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
2. Use the token
POST /api/books
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json
{
"title": "Refactoring",
"author": "Martin Fowler",
"price": 32.99,
"category": "Technology",
"isAvailable": true
}
Response: 201 Created
3. Without token
POST /api/books
(No Authorization header)
Response: 401 Unauthorized
Key Points
- JWT is a signed token that proves identity — the server does not need to store sessions.
- The client logs in once, receives a JWT, and sends it in the
Authorization: Bearerheader with every subsequent request. - The server validates the token's signature, issuer, audience, and expiry on every request.
[Authorize]on a controller or action requires a valid JWT.[AllowAnonymous]overrides that for public endpoints.- In production, passwords must be hashed (using BCrypt or PBKDF2) — never stored as plain text.
