Build a Secure REST API with ASP.NET Core 8, Entity Framework, and JWT Authentication

by Didin J. on Jul 31, 2025 Build a Secure REST API with ASP.NET Core 8, Entity Framework, and JWT Authentication

Build a secure REST API using ASP.NET Core 8, Entity Framework, and JWT authentication with step-by-step user login, role-based access, and testing.

In modern web application development, building secure and scalable APIs is a top priority. ASP.NET Core 8 continues to lead the way by offering powerful features, improved performance, and long-term support for developers. Coupled with Entity Framework Core for data access and JWT (JSON Web Tokens) for stateless authentication, you can create robust REST APIs ready for production environments.

In this tutorial, you’ll learn how to build a secure REST API using ASP.NET Core 8, Entity Framework Core, and JWT Authentication. We’ll walk through the essential steps—from project setup and database modeling to implementing authentication and securing API endpoints. By the end, you'll have a fully working API with user registration, login, and protected routes.

Whether you're a .NET beginner or looking to upgrade your skills to the latest version, this tutorial will provide a comprehensive guide to best practices in API development with .NET 8.


1. Prerequisites

Before we begin, ensure you have the following tools and environment set up:

  • .NET SDK 8.0

  • Visual Studio 2022 (with ASP.NET and web development workload) or Visual Studio Code with C# extension

  • PostgreSQL or SQL Server (we’ll use PostgreSQL in this tutorial)

  • Postman or any other API testing tool

  • Basic knowledge of C#, ASP.NET Core, and RESTful APIs


2. Setting Up the ASP.NET Core 8 Web API Project

Let’s start by creating a new ASP.NET Core Web API project using the .NET CLI:

dotnet new webapi -n SecureApiJwtAuth
cd SecureApiJwtAuth

This command creates a new folder SecureApiJwtAuth with a Web API template including a WeatherForecastController.

You can remove the default controller since we’ll create our own later:

rm Controllers/WeatherForecastController.cs

If you're using Visual Studio, you can create the project through the GUI by selecting ASP.NET Core Web API and checking the box for Use controllers (uncheck minimal API).

Project Structure (after setup)

SecureApiJwtAuth/
├── Controllers/
├── Models/
├── Data/
├── Services/
├── DTOs/
├── Program.cs
├── appsettings.json
└── ...

We’ll organize the code using a clean architecture: separating models, data access, services, and controllers.


3. Installing Required NuGet Packages

To build our secure API with Entity Framework Core and JWT authentication, we’ll need a few additional NuGet packages. Run the following commands from the root of your project:

dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.Tools
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer --version 8.0.7
dotnet add package System.IdentityModel.Tokens.Jwt
dotnet add package BCrypt.Net-Next

✅ If you're using SQL Server instead of PostgreSQL, replace the PostgreSQL package with: 

dotnet add package Microsoft.EntityFrameworkCore.SqlServer

 

Explanation of the Packages

  • Microsoft.EntityFrameworkCore, Design, and Tools: For working with EF Core and managing migrations.

  • Npgsql.EntityFrameworkCore.PostgreSQL: EF Core provider for PostgreSQL.

  • Microsoft.AspNetCore.Authentication.JwtBearer: Enables JWT bearer authentication in ASP.NET Core.

  • System.IdentityModel.Tokens.Jwt: Provides types for creating and validating JWT tokens.

After installing, restore the project:

dotnet restore


4. Configuring the Database with Entity Framework Core

Let’s configure our PostgreSQL database connection in the appsettings.json file.

{
  "ConnectionStrings": {
    "DefaultConnection": "Host=localhost;Port=5432;Database=SecureApiDb;Username=your_user;Password=your_password"
  },
  "Jwt": {
    "Key": "your_super_secret_key_here",
    "Issuer": "DjamwareSecureAPI",
    "Audience": "DjamwareUsers",
    "ExpiresInMinutes": 60
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

🔐 Replace "your_super_secret_key_here" with a secure key (you can generate a base64 string for this) and update your database credentials.

psql postgres -U djamware 
create database SecureApiDb;
\q

Then in Program.cs, add EF Core and JWT configuration:

using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using SecureApiJwtAuth.Data;

var builder = WebApplication.CreateBuilder(args);

// Add DbContext
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));

// Add JWT Authentication
var jwtSettings = builder.Configuration.GetSection("Jwt");
builder.Services.AddAuthentication(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(Encoding.UTF8.GetBytes(jwtSettings["Key"]))
        };
    });

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

app.UseSwagger();
app.UseSwaggerUI();

app.UseHttpsRedirection();

app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

app.Run();


5. Creating Models and the DbContext

We'll start by creating a simple user model and the corresponding DbContext class.

Create a Models folder and add the User.cs file:

using System.ComponentModel.DataAnnotations;

namespace SecureApiJwtAuth.Models
{
    public class User
    {
        [Key]
        public int Id { get; set; }

        [Required]
        [MaxLength(100)]
        public required string Username { get; set; }

        [Required]
        public required string PasswordHash { get; set; }

        [MaxLength(50)]
        public string Role { get; set; } = "User";
    }
}

🔒 Note: We will store the password as a hashed value using a secure hashing algorithm later.

Create a Data folder and add the AppDbContext.cs file:

using Microsoft.EntityFrameworkCore;
using SecureApiJwtAuth.Models;

namespace SecureApiJwtAuth.Data
{
    public class AppDbContext : DbContext
    {
        public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
        {
        }

        public DbSet<User> Users { get; set; }
    }
}

Add Initial Migration and Update the Database

Before we run the migration, make sure EF Core CLI tools are installed:

dotnet tool install --global dotnet-ef

Then run the following commands to create and apply the migration:

dotnet ef migrations add InitialCreate
dotnet ef database update

This will create the SecureApiDb PostgreSQL database with a Users table.


6. Implementing the User Registration and Login APIs

We'll implement two endpoints:

  • POST /api/auth/register – Register a new user

  • POST /api/auth/login – Authenticate and receive a JWT token

✅ Step 1: Create DTOs

Create a folder DTOs/ and add these two files:

DTOs/RegisterDto.cs:

namespace SecureApiJwtAuth.DTOs
{
    public class RegisterDto
    {
        public required string Username { get; set; }
        public required string Password { get; set; }
    }
}

DTOs/LoginDto.cs:

​namespace SecureApiJwtAuth.DTOs
{
    public class LoginDto
    {
        public required string Username { get; set; }
        public required string Password { get; set; }
    }
}

✅ Step 2: Create the Auth Controller

Create a new file Controllers/AuthController.cs:

// Controllers/AuthController.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using SecureApiJwtAuth.Data;
using SecureApiJwtAuth.DTOs;
using SecureApiJwtAuth.Models;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using BCrypt.Net;

namespace SecureApiJwtAuth.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class AuthController : ControllerBase
    {
        private readonly AppDbContext _context;
        private readonly IConfiguration _configuration;

        public AuthController(AppDbContext context, IConfiguration configuration)
        {
            _context = context;
            _configuration = configuration;
        }

        [HttpPost("register")]
        public async Task<IActionResult> Register(RegisterDto dto)
        {
            if (await _context.Users.AnyAsync(u => u.Username == dto.Username))
                return BadRequest("Username already exists.");

            var user = new User
            {
                Username = dto.Username,
                PasswordHash = BCrypt.Net.BCrypt.HashPassword(dto.Password)
            };

            _context.Users.Add(user);
            await _context.SaveChangesAsync();

            return Ok("User registered successfully.");
        }

        [HttpPost("login")]
        public async Task<IActionResult> Login(LoginDto dto)
        {
            var user = await _context.Users.FirstOrDefaultAsync(u => u.Username == dto.Username);
            if (user == null || !BCrypt.Net.BCrypt.Verify(dto.Password, user.PasswordHash))
                return Unauthorized("Invalid username or password.");

            var token = GenerateJwtToken(user);

            return Ok(new { token });
        }

        private string GenerateJwtToken(User user)
        {
            var jwtSettings = _configuration.GetSection("Jwt");
            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings["Key"]));
            var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

            var claims = new[]
            {
                new Claim(JwtRegisteredClaimNames.Sub, user.Username),
                new Claim(ClaimTypes.Role, user.Role),
                new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
            };

            var token = new JwtSecurityToken(
                issuer: jwtSettings["Issuer"],
                audience: jwtSettings["Audience"],
                claims: claims,
                expires: DateTime.UtcNow.AddMinutes(double.Parse(jwtSettings["ExpiresInMinutes"])),
                signingCredentials: creds
            );

            return new JwtSecurityTokenHandler().WriteToken(token);
        }
    }
}

✅ Step 3: Test the Endpoints

Use Postman or curl to test:

Register:

POST http://localhost:5000/api/auth/register
Content-Type: application/json

{
  "username": "djamware",
  "password": "password123"
}

Login:

POST http://localhost:5000/api/auth/login
Content-Type: application/json

{
  "username": "djamware",
  "password": "password123"
}

Expected response:

{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}


7. Setting Up JWT Authorization and Protecting Routes

We already configured JWT authentication in Program.cs, so now we’ll:

  • Apply [Authorize] attributes to secure endpoints

  • Test token-protected access

✅ Step 1: Add Microsoft.AspNetCore.Authorization

Ensure you have the correct using directive in your controller:

using Microsoft.AspNetCore.Authorization;

Step 2: Create a Sample Protected Controller

Let’s add a new controller to test the JWT protection.

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace SecureApiJwtAuth.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class ProtectedController : ControllerBase
    {
        [HttpGet("public")]
        public IActionResult Public()
        {
            return Ok("This is a public endpoint.");
        }

        [Authorize]
        [HttpGet("secure")]
        public IActionResult Secure()
        {
            var username = User.Identity?.Name ?? "Unknown";
            return Ok($"Welcome, {username}. You’ve accessed a protected route!");
        }

        [Authorize(Roles = "Admin")]
        [HttpGet("admin")]
        public IActionResult AdminOnly()
        {
            return Ok("You are an admin!");
        }
    }
}

✅ Step 3: Test with Postman

1. Access Public Route

GET http://localhost:5000/api/protected/public

✅ Should return:

"This is a public endpoint."

2. Access Secure Route without Token

GET http://localhost:5000/api/protected/secure

⛔ Should return:

401 Unauthorized

3. Access Secure Route with JWT Token

  • First, login via /api/auth/login

  • Then, in Postman:

    • Select "Authorization" tab

    • Type: Bearer Token

    • Paste the token

GET http://localhost:5000/api/protected/secure

✅ Should return something like:

"Welcome, djamware. You’ve accessed a protected route!"

🔐 The [Authorize(Roles = "Admin")] works only if a user is created with "Role": "Admin" — useful for role-based access control (RBAC).


8. Testing the REST API with Postman

Now that you’ve built a secure API, it's time to test the entire flow — from registration and login to accessing protected routes using JWT tokens.

✅ Step 1: Register a New User

Request:

POST http://localhost:5000/api/auth/register
Content-Type: application/json

Body:

{
  "username": "djamware",
  "password": "password123"
}

Expected Response:

"User registered successfully."

✅ Step 2: Login to Get a JWT Token

Request:

POST http://localhost:5000/api/auth/login
Content-Type: application/json

Body:

{
  "username": "djamware",
  "password": "password123"
}

Expected Response:

{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

✅ Step 3: Access a Protected Endpoint

Request:

GET http://localhost:5000/api/protected/secure
Authorization: Bearer <paste_token_here>

Expected Response:

"Welcome, djamware. You’ve accessed a protected route!"

If you skip the token or use an invalid one, you’ll get a 401 Unauthorized.

✅ Step 4: Test Admin Route (Optional)

If you’ve created a user with Role = "Admin" manually or via DB seed, test:

GET http://localhost:5000/api/protected/admin
Authorization: Bearer <admin_token>

✅ Step 5: Use Swagger for Visual Testing

Make sure Swagger UI is enabled in Program.cs (already done earlier):

app.UseSwagger();
app.UseSwaggerUI();

Run the app and open:

https://localhost:5001/swagger

🔐 To access protected routes in Swagger, click Authorize (lock icon), paste your Bearer <token> string, and proceed with testing authenticated endpoints.


9. Conclusion and Next Steps

In this tutorial, you’ve built a secure REST API using ASP.NET Core 8, Entity Framework Core, and JWT authentication from the ground up. You learned how to:

  • Set up a new ASP.NET Core Web API project

  • Configure a PostgreSQL database with Entity Framework Core

  • Implement secure user registration and login with hashed passwords

  • Generate and validate JWT tokens

  • Protect API endpoints using role-based authorization

  • Test the API using Postman and Swagger UI

This foundation is production-ready and can be extended to support:

  • Refresh tokens and token expiration handling

  • Role management and permissions

  • Account verification and password reset flows

  • Deployment to cloud platforms like Azure, AWS, or Docker

ASP.NET Core 8 brings modern features, performance, and long-term support, making it an excellent choice for secure backend development.

You can get the full source code on our GitHub.

That's just the basics. If you need more deep learning about ASP.NET Core, Angular, or related, you can take the following cheap course:

Thanks!