.NET Core  

JWT Session 11: Replace Hardcoded Login with EF Core Database Authentication in ASP.NET Core Web API

In the previous articles, we built a complete JWT authentication flow. Our SecureShop API can now:

  • Generate JWTs after successful login

  • Validate incoming JWTs

  • Protect endpoints using [Authorize]

  • Implement Role-Based and Policy-Based Authorization

  • Create Custom Authorization Handlers

However, one important part of our application is still unrealistic.

Our login endpoint still uses hardcoded credentials like this:

if (request.Email != "[email protected]" ||
    request.Password != "Admin@123")
{
    return Unauthorized();
}

This approach is useful for learning, but no real application stores users in source code.

In this article, we'll replace the hardcoded login with Entity Framework Core so that users can authenticate using records stored in a SQL Server database.

Replace Hardcoded Login with EF Core Database Authentication in ASP.NET Core Web API

Why Replace Hardcoded Login?

Imagine the SecureShop application has thousands of customers.

Every day:

  • New users register.

  • Existing users update their profile.

  • Administrators manage customer accounts.

If user information is written directly inside the code, every new customer would require modifying the application and redeploying it.

Instead, user information should be stored in a database.

The authentication flow becomes much more practical.

Login Request
      │
      ▼
ASP.NET Core API
      │
      ▼
SQL Server
      │
      ▼
Find User
      │
      ▼
Generate JWT

This allows the application to authenticate any registered user without changing the code.

How Database Authentication Works

The overall login process remains almost the same.

The only difference is where the user information comes from.

Client
   │
   │ Email + Password
   ▼
Login API
   │
   ▼
Entity Framework Core
   │
   ▼
SQL Server
   │
   ▼
User Found?
      │
 Yes  │ No
      │
      ▼
Generate JWT

Notice that JWT generation hasn't changed.

Only the user lookup process is different.

Step 1: Install Entity Framework Core Packages

Install the SQL Server provider.

dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools

These packages allow Entity Framework Core to communicate with SQL Server and create database migrations.

Step 2: Create the User Entity

Replace our simple model with an Entity Framework entity.

namespace SecureShop.Api.Models;

public class ApplicationUser
{
    public int Id { get; set; }

    public string Name { get; set; } = string.Empty;

    public string Email { get; set; } = string.Empty;

    public string Password { get; set; } = string.Empty;

    public string Role { get; set; } = "Customer";
}

For now, the password is stored as plain text only to simplify the learning process.

Step 3: Create the Database Context

Create ApplicationDbContext.cs

using Microsoft.EntityFrameworkCore;
using SecureShop.Api.Models;

public class ApplicationDbContext : DbContext
{
    public ApplicationDbContext(
        DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }

    public DbSet<ApplicationUser> Users => Set<ApplicationUser>();
}

The DbSet<ApplicationUser> represents the Users table in SQL Server.

Step 4: Configure SQL Server

Add the connection string to appsettings.json.

{
  "ConnectionStrings": {
    "DefaultConnection":
      "Server=.;Database=SecureShopDb;Trusted_Connection=True;TrustServerCertificate=True;"
  }
}

Now register the database context.

builder.Services.AddDbContext<ApplicationDbContext>(options =>
{
    options.UseSqlServer(
        builder.Configuration.GetConnectionString("DefaultConnection"));
});

ASP.NET Core can now create and use the database context through Dependency Injection.

Step 5: Create the Database

Create the first migration.

dotnet ef migrations add InitialCreate

Update the database.

dotnet ef database update

Entity Framework Core creates the Users table automatically.

Step 6: Insert Sample Data

For testing, insert a sample user.

IdNameEmailPasswordRole
1John[email protected]Password@123Customer

Now our application has a real user stored in SQL Server.

Step 7: Update the Login Endpoint

Instead of checking hardcoded values, search the database.

[HttpPost("login")]
public IActionResult Login(LoginRequest request)
{
    var user = _context.Users.FirstOrDefault(x =>
        x.Email == request.Email &&
        x.Password == request.Password);

    if (user is null)
    {
        return Unauthorized();
    }

    var token = _tokenService.GenerateToken(user);

    return Ok(new LoginResponse(
        token,
        DateTime.UtcNow.AddMinutes(30)));
}

Notice that the login flow hasn't changed.

The only difference is that the user now comes from SQL Server instead of source code.

Internal Authentication Flow

Let's see what happens internally.

Client
   │
   │ Login Request
   ▼
AuthController
   │
   ▼
ApplicationDbContext
   │
   ▼
SQL Server
   │
   ▼
Find Matching User
   │
      ├── Not Found → 401 Unauthorized
      │
      ▼
Generate JWT
   │
   ▼
Return Access Token

This is much closer to how authentication works in real applications.

Practical Example

Let's continue with our SecureShop API.

The database contains:

Name  : John
 Email : [email protected]
 Password : Password@123
 Role : Customer

The client sends:

POST /api/auth/login
{
    "email": "[email protected]",
    "password": "Password@123"
}

The API queries SQL Server.

User found.

JWT generated.

Response:

{
    "accessToken": "eyJhbGciOiJIUzI1NiIs..."
}

If the email or password doesn't match any record, the API returns:

401 Unauthorized

Limitations of This Approach

Although database authentication is much better than hardcoded login, there is still one major problem.

We're comparing plain-text passwords.

user.Password == request.Password

This is not secure.

If someone gains access to the database, every user's password becomes visible.

Production applications never store passwords in plain text.

Instead, passwords are stored as secure hashes.

We'll fix this in the next article.

Common Mistakes

Mistake 1: Keeping Hardcoded Credentials

Hardcoded usernames and passwords should only be used for learning or quick demos.

Always authenticate users from a database in real applications.

Mistake 2: Returning the Entire User Entity

After authentication, return only the information the client needs, such as the JWT and its expiration time.

Avoid returning sensitive fields like the password.

Mistake 3: Using Plain-Text Passwords in Production

Comparing plain-text passwords is insecure.

Always store hashed passwords instead of the original password.

Mistake 4: Querying More Data Than Necessary

During login, retrieve only the user record required for authentication.

Avoid loading unnecessary related data that isn't needed to generate the JWT.

Key Takeaways

  • Hardcoded credentials are useful for learning but not for real applications.

  • Entity Framework Core allows user authentication using data stored in SQL Server.

  • The JWT generation process remains unchanged after moving to a database.

  • ApplicationDbContext provides access to the Users table through Entity Framework Core.

  • If a matching user is found, the API generates a JWT; otherwise, it returns 401 Unauthorized.

  • Storing plain-text passwords is still insecure and should be replaced with password hashing.

Our SecureShop API now authenticates users from a real SQL Server database instead of hardcoded values. In the next article, we'll improve security by implementing password hashing and verification, ensuring that user passwords are never stored or compared in plain text.