DEV Community

Cover image for How to Secure Your ASP.NET Web API with JWT Bearer Authentication (Step-by-Step Guide)
R M Shaidul Islam shahed
R M Shaidul Islam shahed

Posted on

How to Secure Your ASP.NET Web API with JWT Bearer Authentication (Step-by-Step Guide)

How to Secure Your ASP.NET Web API with JWT Bearer Authentication (Step-by-Step Guide)

Introduction

In today's interconnected digital landscape, APIs serve as the backbone of modern web applications, mobile apps, and microservices architectures. However, with great connectivity comes great responsibility—securing these APIs is no longer optional; it's a critical requirement.

Unsecured APIs expose your application to a multitude of threats including unauthorized access, data breaches, and malicious attacks. Without proper authentication and authorization mechanisms, anyone can potentially access sensitive data or perform unauthorized operations on your system.

This is where JWT (JSON Web Token) authentication comes into play. JWT has become the de facto standard for securing modern Web APIs due to its stateless nature, scalability, and ease of implementation. Unlike traditional session-based authentication, JWT allows you to authenticate users without maintaining server-side session state, making it perfect for distributed systems and microservices architectures.

In this comprehensive guide, we'll walk through implementing JWT Bearer authentication in an ASP.NET Web API from scratch. By the end of this tutorial, you'll have a fully functional, secure API with token-based authentication.

What is JWT (JSON Web Token)?

JWT is an open standard (RFC 7519) that defines a compact and self-contained way of securely transmitting information between parties as a JSON object. This information can be verified and trusted because it's digitally signed using a secret key or a public/private key pair.

Understanding JWT Structure

A JWT token consists of three parts separated by dots (.):

xxxxx.yyyyy.zzzzz
Enter fullscreen mode Exit fullscreen mode

1. Header
The header typically consists of two parts: the token type (JWT) and the signing algorithm being used (such as HMAC SHA256 or RSA).

{
  "alg": "HS256",
  "typ": "JWT"
}
Enter fullscreen mode Exit fullscreen mode

2. Payload
The payload contains the claims—statements about the user and additional metadata. There are three types of claims: registered, public, and private claims.

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true,
  "exp": 1516239022
}
Enter fullscreen mode Exit fullscreen mode

3. Signature
The signature is used to verify that the sender of the JWT is who it says it is and to ensure the message wasn't changed along the way. It's created by encoding the header and payload using Base64Url and signing them with a secret key.

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)
Enter fullscreen mode Exit fullscreen mode

How JWT Works for Authentication

The JWT authentication flow works as follows:

  1. User submits login credentials (username/password) to the authentication endpoint
  2. Server validates credentials and generates a JWT token
  3. Server returns the token to the client
  4. Client stores the token (typically in localStorage or a cookie)
  5. For subsequent requests, the client includes the token in the Authorization header
  6. Server validates the token and grants access to protected resources

The beauty of JWT is that the server doesn't need to store session information—everything needed for authentication is contained within the token itself.

Setting Up the ASP.NET Web API Project

Let's start by creating a new ASP.NET Web API project. You can use either Visual Studio or the .NET CLI.

Using .NET CLI

Open your terminal or command prompt and run:

dotnet new webapi -n JwtAuthenticationDemo
cd JwtAuthenticationDemo
Enter fullscreen mode Exit fullscreen mode

This creates a new Web API project named JwtAuthenticationDemo using the default ASP.NET Core Web API template.

Using Visual Studio

  1. Open Visual Studio
  2. Click on "Create a new project"
  3. Search for "ASP.NET Core Web API"
  4. Select the template and click "Next"
  5. Name your project JwtAuthenticationDemo
  6. Choose .NET 6.0 or later (this guide uses .NET 8.0)
  7. Uncheck "Use controllers" if you prefer minimal APIs, or keep it checked for traditional controller-based approach
  8. Click "Create"

For this tutorial, we'll use .NET 8.0 with the traditional controller-based approach, as it's more explicit and easier to understand for learning purposes.

Installing Required NuGet Packages

To implement JWT authentication, we need to install the JWT Bearer authentication package. Open your terminal in the project directory and run:

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
Enter fullscreen mode Exit fullscreen mode

Alternatively, you can install it via the NuGet Package Manager in Visual Studio:

  1. Right-click on the project in Solution Explorer
  2. Select "Manage NuGet Packages"
  3. Search for Microsoft.AspNetCore.Authentication.JwtBearer
  4. Click "Install"

This package provides middleware and handlers for JWT Bearer token authentication in ASP.NET Core applications.

Configuring JWT Authentication

Now let's configure JWT authentication in our application. We'll work with the Program.cs file (for .NET 6+ minimal hosting model).

Step 1: Add Configuration Settings

First, add JWT configuration settings to your appsettings.json:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "Jwt": {
    "Key": "YourSuperSecretKeyThatIsAtLeast32CharactersLong!",
    "Issuer": "https://yourdomain.com",
    "Audience": "https://yourdomain.com"
  }
}
Enter fullscreen mode Exit fullscreen mode

Important: Never commit real secret keys to version control. We'll discuss best practices for managing secrets later in this guide.

Step 2: Configure Authentication in Program.cs

Open Program.cs and add the following configuration:

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

var builder = WebApplication.CreateBuilder(args);

// Add services to the container
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// Configure JWT Authentication
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 = builder.Configuration["Jwt:Issuer"],
        ValidAudience = builder.Configuration["Jwt:Audience"],
        IssuerSigningKey = new SymmetricSecurityKey(
            Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!))
    };
});

var app = builder.Build();

// Configure the HTTP request pipeline
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

// IMPORTANT: Authentication must come before Authorization
app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

app.Run();
Enter fullscreen mode Exit fullscreen mode

Understanding the Configuration

Let's break down what each part does:

  • DefaultAuthenticateScheme: Specifies the scheme to use for authentication
  • ValidateIssuer: Ensures the token was issued by a trusted source
  • ValidateAudience: Verifies the token is intended for your application
  • ValidateLifetime: Checks if the token has expired
  • ValidateIssuerSigningKey: Validates the signature to ensure token integrity
  • IssuerSigningKey: The secret key used to sign and validate tokens

Generating JWT Tokens in C

Now that authentication is configured, let's create a controller that generates JWT tokens upon successful login.

Step 1: Create a Login Model

First, create a Models folder and add a LoginModel.cs class:

namespace JwtAuthenticationDemo.Models
{
    public class LoginModel
    {
        public string Username { get; set; } = string.Empty;
        public string Password { get; set; } = string.Empty;
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Create the Authentication Controller

Create a new controller named AuthController.cs in the Controllers folder:

using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using JwtAuthenticationDemo.Models;

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

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

        [HttpPost("login")]
        public IActionResult Login([FromBody] LoginModel login)
        {
            // DEMO ONLY: Validate credentials
            // In production, validate against a database with hashed passwords
            if (login.Username == "admin" && login.Password == "password123")
            {
                var token = GenerateJwtToken(login.Username);
                return Ok(new { token });
            }

            return Unauthorized(new { message = "Invalid credentials" });
        }

        private string GenerateJwtToken(string username)
        {
            var securityKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(_configuration["Jwt:Key"]!));
            var credentials = new SigningCredentials(
                securityKey, SecurityAlgorithms.HmacSha256);

            var claims = new[]
            {
                new Claim(JwtRegisteredClaimNames.Sub, username),
                new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
                new Claim(ClaimTypes.Name, username),
                new Claim(ClaimTypes.Role, "Administrator")
            };

            var token = new JwtSecurityToken(
                issuer: _configuration["Jwt:Issuer"],
                audience: _configuration["Jwt:Audience"],
                claims: claims,
                expires: DateTime.Now.AddHours(1),
                signingCredentials: credentials
            );

            return new JwtSecurityTokenHandler().WriteToken(token);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Understanding the Token Generation

The GenerateJwtToken method does the following:

  1. Creates a symmetric security key from the secret in configuration
  2. Sets up signing credentials using HMAC SHA256 algorithm
  3. Defines claims (user information) to include in the token
  4. Creates a JWT token with issuer, audience, claims, expiration, and signing credentials
  5. Returns the serialized token as a string

Important: The hardcoded credentials in the login method are for demonstration only. In production, always validate against a database with properly hashed passwords using libraries like BCrypt or ASP.NET Core Identity.

Protecting API Endpoints

With authentication configured and token generation in place, let's create protected endpoints.

Step 1: Create a Protected Controller

Create a new controller named WeatherController.cs:

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

namespace JwtAuthenticationDemo.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class WeatherController : ControllerBase
    {
        [HttpGet("public")]
        [AllowAnonymous]
        public IActionResult GetPublicData()
        {
            return Ok(new { message = "This is public data, no authentication required" });
        }

        [HttpGet("protected")]
        [Authorize]
        public IActionResult GetProtectedData()
        {
            var username = User.Identity?.Name;
            return Ok(new { 
                message = "This is protected data",
                username = username
            });
        }

        [HttpGet("admin-only")]
        [Authorize(Roles = "Administrator")]
        public IActionResult GetAdminData()
        {
            return Ok(new { message = "This is admin-only data" });
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Understanding Authorization Attributes

  • [AllowAnonymous]: Allows access without authentication
  • [Authorize]: Requires a valid JWT token to access the endpoint
  • [Authorize(Roles = "Administrator")]: Requires both authentication and specific role membership

When a request is made to a protected endpoint without a valid token, the API returns a 401 Unauthorized response. With a valid token that lacks required roles, it returns 403 Forbidden.

Testing the Authentication

Now let's test our JWT authentication implementation using Postman or any API testing tool.

Step 1: Test Token Generation

Request:

  • Method: POST
  • URL: https://localhost:7xxx/api/auth/login
  • Headers: Content-Type: application/json
  • Body:
{
  "username": "admin",
  "password": "password123"
}
Enter fullscreen mode Exit fullscreen mode

Expected Response:

{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Enter fullscreen mode Exit fullscreen mode

Copy the token value from the response.

Step 2: Test Public Endpoint

Request:

  • Method: GET
  • URL: https://localhost:7xxx/api/weather/public

Expected Response:

{
  "message": "This is public data, no authentication required"
}
Enter fullscreen mode Exit fullscreen mode

This should work without any authentication.

Step 3: Test Protected Endpoint Without Token

Request:

  • Method: GET
  • URL: https://localhost:7xxx/api/weather/protected

Expected Response:

  • Status Code: 401 Unauthorized

Step 4: Test Protected Endpoint With Token

Request:

  • Method: GET
  • URL: https://localhost:7xxx/api/weather/protected
  • Headers: Authorization: Bearer [paste_your_token_here]

Expected Response:

{
  "message": "This is protected data",
  "username": "admin"
}
Enter fullscreen mode Exit fullscreen mode

Step 5: Test Role-Based Endpoint

Request:

  • Method: GET
  • URL: https://localhost:7xxx/api/weather/admin-only
  • Headers: Authorization: Bearer [paste_your_token_here]

Expected Response:

{
  "message": "This is admin-only data"
}
Enter fullscreen mode Exit fullscreen mode

Testing with Swagger

If you're using Swagger (Swashbuckle), you can configure it to support JWT authentication:

builder.Services.AddSwaggerGen(options =>
{
    options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
        Name = "Authorization",
        Type = SecuritySchemeType.Http,
        Scheme = "Bearer",
        BearerFormat = "JWT",
        In = ParameterLocation.Header,
        Description = "Enter your JWT token in the text input below."
    });

    options.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
        {
            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference
                {
                    Type = ReferenceType.SecurityScheme,
                    Id = "Bearer"
                }
            },
            new string[] {}
        }
    });
});
Enter fullscreen mode Exit fullscreen mode

Don't forget to add the required using statement:

using Microsoft.OpenApi.Models;
Enter fullscreen mode Exit fullscreen mode

This adds a "Authorize" button to Swagger UI where you can input your JWT token.

Best Practices & Common Mistakes

1. Use Environment Variables for Secrets

Never hardcode secret keys in your source code or commit them to version control. Use environment variables or secure secret management systems.

For Development:
Use User Secrets in .NET:

dotnet user-secrets init
dotnet user-secrets set "Jwt:Key" "YourSuperSecretKey"
Enter fullscreen mode Exit fullscreen mode

For Production:
Use Azure Key Vault, AWS Secrets Manager, or environment variables:

var jwtKey = Environment.GetEnvironmentVariable("JWT_KEY") 
    ?? builder.Configuration["Jwt:Key"];
Enter fullscreen mode Exit fullscreen mode

2. Implement Token Expiration and Refresh Tokens

Always set reasonable expiration times for tokens. For sensitive applications, use short-lived access tokens (15-30 minutes) combined with refresh tokens.

expires: DateTime.Now.AddMinutes(30), // Short-lived access token
Enter fullscreen mode Exit fullscreen mode

Implement a refresh token endpoint that issues new access tokens without requiring the user to log in again.

3. Use Strong Secret Keys

Your JWT secret key should be:

  • At least 32 characters long
  • Randomly generated
  • Stored securely
  • Rotated periodically
// Good: Strong, random key
"Kv8B#Xm2$Qw9@Ln5!Yt7&Zp4*Gh6^Rj3"

// Bad: Weak, predictable key
"mySecretKey123"
Enter fullscreen mode Exit fullscreen mode

4. Validate All Token Parameters

Don't skip token validation parameters. Each serves a security purpose:

ValidateIssuer = true,        // Prevent token reuse from other apps
ValidateAudience = true,       // Ensure token is for your API
ValidateLifetime = true,       // Reject expired tokens
ValidateIssuerSigningKey = true, // Verify token hasn't been tampered with
ClockSkew = TimeSpan.Zero      // Eliminate default 5-minute clock skew
Enter fullscreen mode Exit fullscreen mode

5. Use HTTPS Only

Always use HTTPS in production to prevent token interception:

app.UseHttpsRedirection();
Enter fullscreen mode Exit fullscreen mode

Consider setting the Secure and HttpOnly flags if storing tokens in cookies.

6. Implement Proper Password Security

Never store plain-text passwords. Use ASP.NET Core Identity or implement proper password hashing:

// Use BCrypt, Argon2, or ASP.NET Core Identity
public bool VerifyPassword(string password, string hashedPassword)
{
    return BCrypt.Net.BCrypt.Verify(password, hashedPassword);
}
Enter fullscreen mode Exit fullscreen mode

7. Handle Token Validation Errors Gracefully

Configure custom error responses for authentication failures:

.AddJwtBearer(options =>
{
    options.Events = new JwtBearerEvents
    {
        OnAuthenticationFailed = context =>
        {
            if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
            {
                context.Response.Headers.Add("Token-Expired", "true");
            }
            return Task.CompletedTask;
        }
    };
});
Enter fullscreen mode Exit fullscreen mode

8. Implement Token Blacklisting for Logout

Since JWTs are stateless, they remain valid until expiration. For logout functionality, implement token blacklisting using a cache (Redis) or database.

9. Don't Store Sensitive Data in JWT Payload

Remember that JWT payloads are base64-encoded, not encrypted. Anyone with the token can decode and read the payload. Never include:

  • Passwords
  • Credit card numbers
  • Social security numbers
  • Other sensitive personal information

10. Set Appropriate CORS Policies

If your API will be consumed by web applications from different domains, configure CORS properly:

builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowSpecificOrigin",
        builder => builder
            .WithOrigins("https://yourdomain.com")
            .AllowAnyMethod()
            .AllowAnyHeader());
});
Enter fullscreen mode Exit fullscreen mode

Conclusion

Congratulations! You've successfully implemented JWT Bearer authentication in your ASP.NET Web API. Let's recap what we've accomplished:

✅ Understood what JWT is and how it works

✅ Set up an ASP.NET Web API project with JWT authentication

✅ Configured token validation parameters

✅ Implemented token generation on successful login

✅ Protected API endpoints using [Authorize] attributes

✅ Tested authentication with various scenarios

✅ Learned best practices for production-ready JWT implementation

JWT authentication provides a scalable, stateless solution for securing your APIs, making it ideal for modern web applications, mobile apps, and microservices architectures. By following the best practices outlined in this guide—using environment variables for secrets, implementing token expiration, and validating all token parameters—you'll build secure, production-ready APIs.

The complete code from this tutorial provides a solid foundation, but remember that real-world applications require additional considerations like refresh tokens, token revocation, rate limiting, and comprehensive logging.

Next Steps

To further enhance your API security, consider exploring:

  • Implementing refresh token functionality
  • Integrating with ASP.NET Core Identity
  • Adding role and claim-based authorization
  • Implementing OAuth 2.0 and OpenID Connect
  • Setting up API rate limiting and throttling

Found this guide helpful? Follow me for more ASP.NET, .NET, and web development tutorials. If you have questions or run into issues, drop a comment below—I'm here to help!

GitHub Repository: Consider checking out the complete source code on GitHub (create a repository with the code from this tutorial).

👋Ultimate Collection of .NET Web Apps for Developers and Businesses
🚀 My YouTube Channel
💻 Github

Here are three ways you can help me out:
Please drop me a follow →👍 R M Shahidul Islam Shahed
Receive an e-mail every time I post on Medium → 💌 Click Here
Grab a copy of my E-Book on OOP with C# → 📚 Click Here

Happy coding! 🚀


Tags: #ASPNETCore #WebAPI #JWT #Authentication #CSharp #DotNet #WebDevelopment #APISecurity

Top comments (0)