JWT Authentication in ASP.NET Core: Complete Step-by-Step Guide

Learn how to implement JWT authentication in ASP.NET Core Web API with C# code, token validation, protected endpoints and role-based authorization

JWT authentication is commonly used to protect ASP.NET Core Web APIs. Instead of maintaining a traditional server-side login session, the API can issue a signed JSON Web Token after a user successfully authenticates. The client then sends that token with requests to protected API endpoints.

In this tutorial, we'll build a simple JWT authentication implementation from scratch using ASP.NET Core. You'll create a login endpoint, generate a JWT, configure JWT Bearer authentication, protect API endpoints with [Authorize], and add role-based authorization.

What you'll build
  • ASP.NET Core Web API
  • JWT Bearer authentication
  • Login endpoint
  • JWT token generation
  • Protected API endpoint
  • Role-based authorization
  • Token testing with Postman

What Is JWT Authentication?

JWT stands for JSON Web Token. A JWT is a compact token that can carry claims about a user or another subject.

A typical authentication flow looks like this:

  1. The user sends credentials to the login endpoint.
  2. The server validates those credentials.
  3. The server generates and signs a JWT.
  4. The API returns the token to the client.
  5. The client sends the token with subsequent API requests.
  6. The API validates the token before allowing access to protected resources.

For an HTTP API, the token is commonly sent using the Authorization header:

Authorization: Bearer YOUR_ACCESS_TOKEN

Understanding the Structure of a JWT

A JWT commonly consists of three Base64URL-encoded sections separated by periods:

header.payload.signature

Header

The header identifies information such as the token type and signing algorithm.

Payload

The payload contains claims. Claims can describe the authenticated subject, roles, issuer, audience, expiration time, and other information.

Signature

The signature allows the receiving application to verify that a signed token has not been modified after it was issued.

A signed JWT is not automatically encrypted. Do not place passwords, secret keys, or other sensitive information in the JWT payload.

Prerequisites

For this tutorial you should have:

  • A supported .NET SDK installed
  • Basic C# knowledge
  • A code editor such as Visual Studio or Visual Studio Code
  • Postman or another HTTP client for testing

You can check your installed .NET SDK version with:

dotnet --version

Step 1: Create an ASP.NET Core Web API

Open a terminal and create a new Web API project:

dotnet new webapi -n JwtAuthDemo
cd JwtAuthDemo

You can then run the project with:

dotnet run

Make sure the application starts successfully before adding authentication.

Step 2: Install the JWT Bearer Authentication Package

From the project directory, install the JWT Bearer authentication package:

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

Use a package version compatible with the target framework of your application.

Step 3: Add JWT Configuration

Open appsettings.json and add a JWT configuration section.

{
  "Jwt": {
    "Key": "DEVELOPMENT_ONLY_CHANGE_THIS_SECRET_KEY",
    "Issuer": "JwtAuthDemo",
    "Audience": "JwtAuthDemoClient",
    "ExpireMinutes": 15
  },

  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },

  "AllowedHosts": "*"
}
Security note: The key above is only an example for local development. Do not commit a production signing key to source control. Use a secure secret-management solution appropriate for your environment.

Step 4: Create a Login Request Model

Create a folder named Models and add LoginRequest.cs.

namespace JwtAuthDemo.Models;

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

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

This model represents the credentials submitted to our login endpoint.

Step 5: Create a JWT Token Service

Create a folder named Services and add TokenService.cs.

using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.IdentityModel.Tokens;

namespace JwtAuthDemo.Services;

public class TokenService
{
    private readonly IConfiguration _configuration;

    public TokenService(IConfiguration configuration)
    {
        _configuration = configuration;
    }

    public string CreateToken(string userId, string email, string role)
    {
        var jwtSettings = _configuration.GetSection("Jwt");

        var keyValue = jwtSettings["Key"]
            ?? throw new InvalidOperationException("JWT Key is missing.");

        var issuer = jwtSettings["Issuer"]
            ?? throw new InvalidOperationException("JWT Issuer is missing.");

        var audience = jwtSettings["Audience"]
            ?? throw new InvalidOperationException("JWT Audience is missing.");

        var expireMinutes = int.TryParse(
            jwtSettings["ExpireMinutes"],
            out var minutes)
            ? minutes
            : 15;

        var claims = new List<Claim>
        {
            new(JwtRegisteredClaimNames.Sub, userId),
            new(JwtRegisteredClaimNames.Email, email),
            new(ClaimTypes.NameIdentifier, userId),
            new(ClaimTypes.Email, email),
            new(ClaimTypes.Role, role),
            new(
                JwtRegisteredClaimNames.Jti,
                Guid.NewGuid().ToString())
        };

        var key = new SymmetricSecurityKey(
            Encoding.UTF8.GetBytes(keyValue));

        var credentials = new SigningCredentials(
            key,
            SecurityAlgorithms.HmacSha256);

        var token = new JwtSecurityToken(
            issuer: issuer,
            audience: audience,
            claims: claims,
            expires: DateTime.UtcNow.AddMinutes(expireMinutes),
            signingCredentials: credentials);

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

The service creates claims for the user, signs the token and sets an expiration time.

Step 6: Configure JWT Authentication in Program.cs

Now configure ASP.NET Core to validate incoming JWTs.

Replace or update your Program.cs with the following configuration:

using System.Text;
using JwtAuthDemo.Services;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

builder.Services.AddAuthorization();

builder.Services.AddScoped<TokenService>();

var jwtSettings = builder.Configuration.GetSection("Jwt");

var jwtKey = jwtSettings["Key"]
    ?? throw new InvalidOperationException("JWT Key is missing.");

var jwtIssuer = jwtSettings["Issuer"]
    ?? throw new InvalidOperationException("JWT Issuer is missing.");

var jwtAudience = jwtSettings["Audience"]
    ?? throw new InvalidOperationException("JWT Audience is missing.");

builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters =
            new TokenValidationParameters
            {
                ValidateIssuer = true,
                ValidateAudience = true,
                ValidateLifetime = true,
                ValidateIssuerSigningKey = true,

                ValidIssuer = jwtIssuer,
                ValidAudience = jwtAudience,

                IssuerSigningKey =
                    new SymmetricSecurityKey(
                        Encoding.UTF8.GetBytes(jwtKey)),

                ClockSkew = TimeSpan.Zero
            };
    });

var app = builder.Build();

app.UseHttpsRedirection();

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

app.MapControllers();

app.Run();

Two lines are particularly important:

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

Authentication establishes the identity represented by a valid token. Authorization then determines whether that identity is permitted to access a resource.

Step 7: Create the Login Endpoint

Create a Controllers folder if your project doesn't already contain one, then create AuthController.cs.

using JwtAuthDemo.Models;
using JwtAuthDemo.Services;
using Microsoft.AspNetCore.Mvc;

namespace JwtAuthDemo.Controllers;

[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
    private readonly TokenService _tokenService;

    public AuthController(TokenService tokenService)
    {
        _tokenService = tokenService;
    }

    [HttpPost("login")]
    public IActionResult Login(LoginRequest request)
    {
        // Demo only.
        // Replace this with your real user store,
        // ASP.NET Core Identity or another
        // authentication system.

        if (request.Email != "admin@example.com" ||
            request.Password != "DemoPassword123!")
        {
            return Unauthorized(new
            {
                message = "Invalid email or password."
            });
        }

        var token = _tokenService.CreateToken(
            userId: "1",
            email: request.Email,
            role: "Admin");

        return Ok(new
        {
            accessToken = token
        });
    }
}
Important: The hard-coded username and password above exist only to keep this authentication example easy to reproduce. Never implement production authentication using hard-coded credentials. Use a proper identity/user store and securely hashed passwords.

Step 8: Create a Protected API

Now let's create an endpoint that can only be accessed with a valid JWT.

Create SecureController.cs:

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

namespace JwtAuthDemo.Controllers;

[ApiController]
[Route("api/[controller]")]
public class SecureController : ControllerBase
{
    [Authorize]
    [HttpGet]
    public IActionResult GetSecureData()
    {
        return Ok(new
        {
            message = "You successfully accessed a protected API."
        });
    }
}

The [Authorize] attribute tells ASP.NET Core that the endpoint requires an authenticated user.

Step 9: Test the Login API

Run the application:

dotnet run

Using Postman or another HTTP client, send a POST request to:

https://localhost:YOUR_PORT/api/auth/login

Use a JSON request body:

{
  "email": "admin@example.com",
  "password": "DemoPassword123!"
}

If the credentials are valid, the API returns a response similar to:

{
  "accessToken": "eyJhbGciOi..."
}

Copy the value of accessToken.

Step 10: Call the Protected Endpoint

Send a GET request to:

https://localhost:YOUR_PORT/api/secure

Add this request header:

Authorization: Bearer YOUR_ACCESS_TOKEN

With a valid token, the endpoint should return:

{
  "message": "You successfully accessed a protected API."
}

If the token is missing or invalid, the API should respond with 401 Unauthorized.

Adding Role-Based Authorization

Our token contains an ASP.NET Core role claim:

new(ClaimTypes.Role, role)

That means we can restrict an endpoint to a particular role.

[Authorize(Roles = "Admin")]
[HttpGet("admin")]
public IActionResult AdminOnly()
{
    return Ok(new
    {
        message = "Only an Admin can access this endpoint."
    });
}

A successfully authenticated user who does not satisfy the required authorization policy will normally receive 403 Forbidden.

Common JWT Claims

Claim Purpose
sub Identifies the subject of the token
exp Token expiration time
iss Identifies the token issuer
aud Identifies the intended audience
jti Unique identifier for a token

JWT Authentication Best Practices

The example above is intentionally small so that the authentication flow is easy to understand. A production implementation requires additional security considerations.

  • Always use HTTPS. Access tokens should not be transmitted over an unencrypted HTTP connection.
  • Protect signing keys. Do not store production secrets directly in source code or commit them to Git.
  • Use sufficiently strong keys. Generate cryptographically strong secrets appropriate for your signing algorithm.
  • Keep access tokens reasonably short-lived. The appropriate lifetime depends on the application's security and usability requirements.
  • Validate issuer, audience, signature and lifetime. Don't disable validation simply to make an authentication problem disappear.
  • Don't put secrets in JWT payloads. A signed token does not mean that its payload is confidential.
  • Use a real user-management system. Production applications should securely manage users, credentials and password hashing rather than using the demonstration credentials shown in this tutorial.
  • Design refresh-token handling carefully. If your application uses refresh tokens, treat them as sensitive credentials and plan for rotation and revocation.

What About Refresh Tokens?

Short-lived access tokens create an important usability question: what happens when the access token expires?

One common approach is to use a refresh token. The refresh token can be presented to a dedicated endpoint to obtain a new access token without asking the user to enter credentials every time an access token expires.

Refresh tokens should not simply be treated as long-lived access tokens. They require secure storage and a strategy for expiration, rotation and revocation.

For a beginner tutorial, it is better to first understand the access-token flow shown above before adding a refresh-token implementation.

Common JWT Authentication Problems

401 Unauthorized

A 401 usually means the API could not establish a valid authenticated identity from the request.

Check:

  • Is the Authorization header present?
  • Does it start with Bearer?
  • Has the token expired?
  • Does the signing key match?
  • Are the issuer and audience correct?
  • Is JWT Bearer authentication configured?

403 Forbidden

A 403 usually means authentication succeeded but the authenticated user does not meet the authorization requirement.

For example, an endpoint requiring the Admin role will reject an authenticated user who does not have that role.

Invalid Signature

Verify that the API is validating the token using the expected signing key and algorithm. A token signed with a different key should fail validation.

Token Expired

Check the token's expiration and the server clock. In this example we set:

ClockSkew = TimeSpan.Zero

That removes the default clock-skew allowance, so expiration is enforced without that additional tolerance.

Authentication Works Until [Authorize] Is Added

Check the middleware configuration and make sure authentication is executed before authorization:

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

JWT vs Cookie Authentication

JWT Bearer authentication and cookie authentication solve related problems, but neither one is automatically better for every application.

Scenario JWT Bearer Cookies
Web APIs Common choice Also possible
Traditional server-rendered web apps Possible Very common
Automatically sent by browser Depends on storage/client design Yes, according to cookie rules
Server-side session required Not inherently Not inherently
Native/mobile API clients Common Less typical

Choose an authentication approach based on the architecture and security requirements of your application rather than assuming JWT is always the better option.

Frequently Asked Questions

What is JWT authentication in ASP.NET Core?

JWT authentication allows ASP.NET Core to authenticate requests using a signed token supplied by the client, commonly through the HTTP Authorization header using the Bearer authentication scheme.

Does a JWT encrypt my data?

Not necessarily. A standard signed JWT protects integrity but its payload can generally be decoded by anyone who has the token. Sensitive information should not be placed in the payload merely because the token is signed.

Where should I store a JWT?

There is no single storage strategy that is correct for every application. The appropriate choice depends on whether the client is a browser, mobile app, server application or another type of client, as well as the threats you need to protect against.

In browser applications, consider the security implications of both JavaScript-accessible storage and cookies before choosing an approach.

Why am I getting 401 Unauthorized?

Common causes include an expired token, incorrect signing key, invalid issuer or audience, a malformed Authorization header, or incorrect JWT Bearer configuration.

What is the difference between 401 and 403?

In simple terms, 401 Unauthorized generally means the request could not be authenticated successfully. 403 Forbidden generally means the authenticated identity does not have permission to perform the requested action.

Can JWT be used with ASP.NET Core Identity?

Yes. ASP.NET Core Identity can manage users and credentials while your API authentication architecture can issue and validate tokens as appropriate. The exact implementation depends on the application's authentication design.

Can a JWT be revoked?

A self-contained access token does not automatically contact a central session store on every request. If immediate revocation is required, your architecture needs an appropriate revocation strategy. Short token lifetimes and carefully designed refresh-token management are common parts of such a design.

How long should a JWT access token last?

There is no universal lifetime that is correct for every application. Shorter lifetimes reduce the period during which a stolen access token can be used, while very short lifetimes can increase refresh activity. Choose the lifetime based on the application's risk profile and authentication design.

Complete Authentication Flow

At this point our example works as follows:

  1. The client sends an email and password to /api/auth/login.
  2. The demo endpoint validates the credentials.
  3. TokenService creates and signs a JWT.
  4. The API returns the access token.
  5. The client sends the token in the Authorization header.
  6. JWT Bearer middleware validates the token.
  7. [Authorize] allows authenticated requests to the protected endpoint.
  8. Role requirements can further restrict access to specific users.

Conclusion

You have now built a basic JWT authentication flow in ASP.NET Core. We created an API, configured JWT Bearer authentication, generated a signed access token, protected an endpoint with [Authorize], and added role-based authorization.

The example intentionally keeps user validation simple so the JWT flow is easy to understand. For a production application, the next step is to integrate a proper identity system, secure key management, appropriate token lifetimes, refresh-token handling where required, and application-specific authorization policies.

When troubleshooting JWT authentication, avoid weakening validation just to make a token work. Check the signing key, issuer, audience, lifetime, authentication scheme and middleware configuration individually.

Hello! My name is Aniket Shahane and I am a senior software consultant. I hold a post-graduate degree (MTech) in Computer Science and Engineering, and I have a passion for using my technical expertise to solve complex problems. I am excited to be here and eager to share my knowledge and experience with you.

Post a Comment