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
PartContains
HeaderToken type (JWT) and signing algorithm (HS256)
PayloadClaims: user Id, email, role, expiry date
SignatureEncrypted 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: Bearer header 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.

Leave a Comment