Introduction
Authentication is one of the most important parts of any modern web application. If you are building an ASP.NET Core Web API, you need a reliable way to identify users and control which resources they can access.
One of the most common approaches is JWT (JSON Web Token) authentication.
JWT authentication works particularly well with REST APIs because it is stateless. Once a user successfully logs in, the server generates a token. The client then sends that token with subsequent API requests.
In this tutorial, we will build JWT authentication in an ASP.NET Core Web API step by step.
By the end of this article, you will know how to:
- Create an ASP.NET Core Web API
- Configure JWT authentication
- Generate a JWT token during login
- Protect API endpoints
- Use the
[Authorize]attribute - Configure JWT validation
- Test authenticated APIs
- Understand common JWT authentication errors
What Is JWT Authentication?
JWT stands for JSON Web Token.
A JWT is a compact token containing information about the authenticated user. The token is digitally signed so that the API can verify that it has not been modified.
A typical JWT looks like this:
xxxxx.yyyyy.zzzzz
It consists of three parts:
Header.Payload.Signature
1. Header
The header contains information about the token, such as the signing algorithm.
2. Payload
The payload contains claims about the user.
For example:
{
"sub": "123",
"name": "John",
"role": "Admin"
}
3. Signature
The signature is used by the API to verify that the token was issued by a trusted source and has not been changed.
How JWT Authentication Works
The overall flow is relatively simple:
Client
|
| 1. Login (username/password)
v
ASP.NET Core API
|
| 2. Validate credentials
|
| 3. Generate JWT
v
Client
|
| 4. Store token
|
| 5. Send token with API request
v
Protected API
|
| 6. Validate JWT
|
v
Return requested data
The token is normally sent using the HTTP Authorization header:
Authorization: Bearer YOUR_JWT_TOKEN
Prerequisites
Before starting, you should have:
- .NET SDK installed
- Visual Studio, Visual Studio Code, or another suitable IDE
- Basic knowledge of C#
- Basic understanding of ASP.NET Core Web API
- Basic understanding of HTTP requests
This example uses a modern ASP.NET Core Web API project and can be adapted to current .NET versions.
Step 1: Create an ASP.NET Core Web API Project
Create a new Web API project using the .NET CLI:
dotnet new webapi -n JwtAuthenticationDemo
Move into the project:
cd JwtAuthenticationDemo
Run the application:
dotnet run
Your API should now be running locally.
Step 2: Install the JWT Authentication Package
Install the JWT Bearer authentication package:
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
This package provides the middleware required to validate JWT bearer tokens.
Step 3: Add JWT Configuration
Open appsettings.json and add a JWT configuration section.
{
"Jwt": {
"Key": "your-super-secret-development-key-change-this",
"Issuer": "JwtAuthenticationDemo",
"Audience": "JwtAuthenticationDemoUsers",
"ExpirationMinutes": 60
}
}
These settings have the following purpose:
| Setting | Purpose |
|---|---|
Key |
Secret key used to sign the token |
Issuer |
Identifies who issued the token |
Audience |
Identifies who the token is intended for |
ExpirationMinutes |
Token lifetime |
Important security note
Do not store production secrets directly in appsettings.json.
For development, you can use User Secrets. For production, use a secure secret-management solution such as Azure Key Vault, AWS Secrets Manager, or another appropriate secrets store.
Also make sure your signing key is sufficiently long and random.
Step 4: Configure JWT Authentication
Open Program.cs.
Add the required namespaces:
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
Then configure authentication:
var builder = WebApplication.CreateBuilder(args);
var jwtKey = builder.Configuration["Jwt:Key"]
?? throw new InvalidOperationException("JWT key is not configured.");
var jwtIssuer = builder.Configuration["Jwt:Issuer"];
var jwtAudience = builder.Configuration["Jwt:Audience"];
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(jwtKey)
),
ValidateIssuer = true,
ValidIssuer = jwtIssuer,
ValidateAudience = true,
ValidAudience = jwtAudience,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
});
builder.Services.AddAuthorization();
builder.Services.AddControllers();
var app = builder.Build();
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
There are two important middleware calls here:
app.UseAuthentication();
app.UseAuthorization();
The order matters.
Authentication should run before authorization:
UseAuthentication()
↓
UseAuthorization()
↓
MapControllers()
Step 5: Create a Login Request Model
Create a folder called Models.
Inside it, create LoginRequest.cs:
namespace JwtAuthenticationDemo.Models;
public class LoginRequest
{
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
}
This model represents the credentials submitted by the client.
Step 6: Create a Token Service
Instead of generating JWTs directly inside the controller, it is better to keep token generation in a separate service.
Create a folder called Services.
Create IJwtTokenService.cs:
using System.Security.Claims;
namespace JwtAuthenticationDemo.Services;
public interface IJwtTokenService
{
string GenerateToken(string userId, string username, string role);
}
Now create JwtTokenService.cs:
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
namespace JwtAuthenticationDemo.Services;
public class JwtTokenService : IJwtTokenService
{
private readonly IConfiguration _configuration;
public JwtTokenService(IConfiguration configuration)
{
_configuration = configuration;
}
public string GenerateToken(
string userId,
string username,
string role)
{
var key = _configuration["Jwt:Key"]
?? throw new InvalidOperationException("JWT key is not configured.");
var issuer = _configuration["Jwt:Issuer"];
var audience = _configuration["Jwt:Audience"];
var expirationMinutes =
_configuration.GetValue<int>("Jwt:ExpirationMinutes");
var claims = new List<Claim>
{
new(JwtRegisteredClaimNames.Sub, userId),
new(JwtRegisteredClaimNames.UniqueName, username),
new(ClaimTypes.Name, username),
new(ClaimTypes.Role, role)
};
var securityKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(key)
);
var credentials = new SigningCredentials(
securityKey,
SecurityAlgorithms.HmacSha256
);
var token = new JwtSecurityToken(
issuer: issuer,
audience: audience,
claims: claims,
expires: DateTime.UtcNow.AddMinutes(expirationMinutes),
signingCredentials: credentials
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}
Step 7: Register the JWT Service
In Program.cs, register the service:
builder.Services.AddScoped<IJwtTokenService, JwtTokenService>();
So the service configuration becomes:
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(jwtKey)
),
ValidateIssuer = true,
ValidIssuer = jwtIssuer,
ValidateAudience = true,
ValidAudience = jwtAudience,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
});
builder.Services.AddAuthorization();
builder.Services.AddScoped<IJwtTokenService, JwtTokenService>();
Step 8: Create the Login Controller
Now let's create an authentication controller.
Create:
Controllers/AuthController.cs
Add the following code:
using JwtAuthenticationDemo.Models;
using JwtAuthenticationDemo.Services;
using Microsoft.AspNetCore.Mvc;
namespace JwtAuthenticationDemo.Controllers;
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
private readonly IJwtTokenService _jwtTokenService;
public AuthController(IJwtTokenService jwtTokenService)
{
_jwtTokenService = jwtTokenService;
}
[HttpPost("login")]
public IActionResult Login(LoginRequest request)
{
// Demo credentials only.
// In a real application, validate the user
// against your database and use password hashing.
if (request.Username != "admin" ||
request.Password != "Password123!")
{
return Unauthorized(new
{
message = "Invalid username or password."
});
}
var token = _jwtTokenService.GenerateToken(
"1",
request.Username,
"Admin");
return Ok(new
{
accessToken = token
});
}
}
This example uses hard-coded credentials only to keep the tutorial simple.
In a real application, you should validate the credentials against your database.
Step 9: Create a Protected API
Now let's create an endpoint that requires authentication.
Create:
Controllers/ProductsController.cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace JwtAuthenticationDemo.Controllers;
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
[HttpGet]
[Authorize]
public IActionResult GetProducts()
{
return Ok(new[]
{
new { Id = 1, Name = "Laptop", Price = 1200 },
new { Id = 2, Name = "Keyboard", Price = 80 },
new { Id = 3, Name = "Mouse", Price = 40 }
});
}
}
The important part is:
[Authorize]
This tells ASP.NET Core that the endpoint requires an authenticated user.
Step 10: Test the Login API
Start the application and send a POST request to:
POST /api/auth/login
Request body:
{
"username": "admin",
"password": "Password123!"
}
If the credentials are correct, the API will return something similar to:
{
"accessToken": "eyJhbGciOiJIUzI1NiIs..."
}
Copy the access token.
Step 11: Call the Protected API
Now call:
GET /api/products
Add the following HTTP header:
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
If the token is valid, the API will return:
[
{
"id": 1,
"name": "Laptop",
"price": 1200
},
{
"id": 2,
"name": "Keyboard",
"price": 80
},
{
"id": 3,
"name": "Mouse",
"price": 40
}
]
If you call the endpoint without a valid token, ASP.NET Core will reject the request.
Understanding [Authorize]
The [Authorize] attribute can be applied to controllers or individual actions.
For example:
[Authorize]
[HttpGet]
public IActionResult GetProducts()
{
return Ok();
}
You can also protect the entire controller:
[Authorize]
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
}
You can then allow anonymous access to a specific endpoint:
[AllowAnonymous]
[HttpPost("login")]
public IActionResult Login()
{
// Login logic
}
This is particularly useful for authentication endpoints.
Role-Based Authorization
JWT authentication can also be used for role-based authorization.
For example:
[Authorize(Roles = "Admin")]
[HttpDelete("{id}")]
public IActionResult DeleteProduct(int id)
{
return Ok();
}
Only users with the Admin role can access this endpoint.
Remember that the JWT must contain the appropriate role claim:
new(ClaimTypes.Role, role)
For example:
{
"sub": "1",
"unique_name": "admin",
"role": "Admin"
}
Reading User Information from the JWT
Once the user is authenticated, ASP.NET Core makes the claims available through HttpContext.User.
For example:
[Authorize]
[HttpGet("profile")]
public IActionResult GetProfile()
{
var username = User.Identity?.Name;
return Ok(new
{
username
});
}
You can also retrieve individual claims:
var userId = User.FindFirst("sub")?.Value;
Or:
var role = User.FindFirst(
System.Security.Claims.ClaimTypes.Role)?.Value;
This allows your application to determine which user is making the request.
Common JWT Authentication Errors
JWT configuration issues are common when implementing authentication for the first time.
1. Getting 401 Unauthorized
A 401 Unauthorized response usually means that the request does not contain a valid authentication token.
Check that the request contains:
Authorization: Bearer YOUR_TOKEN
Also check:
- Token has not expired
- Signing key is correct
- Issuer matches
- Audience matches
- Authentication middleware is configured
2. Forgetting UseAuthentication()
One common mistake is configuring JWT authentication but forgetting to add:
app.UseAuthentication();
Make sure it appears before:
app.UseAuthorization();
Correct:
app.UseAuthentication();
app.UseAuthorization();
3. Issuer or Audience Doesn't Match
If the token was created with:
Issuer = JwtAuthenticationDemo
but the API expects a different issuer, validation will fail.
The same applies to the audience.
Make sure these values are consistent between token generation and token validation.
4. Token Has Expired
JWT tokens normally have an expiration time.
For example:
expires: DateTime.UtcNow.AddMinutes(60)
After the token expires, the client needs to obtain a new token.
5. Storing the Secret Key in Source Code
Avoid doing this:
var key = "my-secret-key";
Production applications should use secure configuration and secret-management solutions.
JWT Security Best Practices
JWT authentication is straightforward to implement, but security should not be treated as an afterthought.
Use HTTPS
Always use HTTPS in production so that credentials and tokens are encrypted while travelling between the client and server.
Use a Strong Signing Key
Avoid simple secrets such as:
123456
password
secret
Use a strong, randomly generated signing key.
Keep Token Lifetimes Reasonable
Do not make access tokens valid indefinitely.
A relatively short access-token lifetime reduces the impact if a token is compromised.
Never Put Sensitive Information in the JWT
JWT payloads are encoded, not encrypted.
Do not put passwords, credit-card information, or other sensitive data into the token.
Store Secrets Securely
Do not commit production JWT secrets to Git.
Use environment variables, secret stores, or cloud-based secret-management services.
Validate the Token
The API should validate:
- Signature
- Issuer
- Audience
- Expiration
- Other required claims
JWT Authentication vs Session Authentication
Traditional session-based authentication stores authentication state on the server.
JWT authentication generally stores the authentication information in a token that the client sends with each request.
| Feature | JWT | Session |
|---|---|---|
| Server-side session | Not normally required | Required |
| Stateless | Yes | Usually no |
| API friendly | Yes | Yes |
| Scaling | Easier in many architectures | May require shared session storage |
| Token validation | Signature and claims | Session lookup |
| Common use | REST APIs, SPAs, mobile apps | Traditional web applications |
JWT is particularly popular for APIs consumed by Angular, React, mobile applications, and other clients.
JWT Authentication with Angular or React
A common architecture looks like this:
Angular / React / Mobile App
|
| Login
v
ASP.NET Core API
|
| JWT
v
Client receives token
|
| Authorization: Bearer <token>
v
Protected API endpoints
For a frontend application, an HTTP interceptor can automatically attach the JWT to API requests.
For example, conceptually:
Authorization: Bearer <access-token>
The ASP.NET Core API then validates the token before allowing access to protected endpoints.
Should You Use JWT Refresh Tokens?
For applications where users remain logged in for a longer period, you may also want to implement refresh tokens.
A common approach is:
Access Token
Short lifetime
|
| expires
v
Refresh Token
Longer lifetime
|
v
New Access Token
The access token should generally be short-lived, while the refresh-token mechanism can be used to obtain a new access token without forcing the user to log in again.
Refresh tokens require additional security considerations, including secure storage, rotation, revocation, and expiration.
Complete Program.cs Example
For reference, the core configuration looks like this:
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
var jwtKey = builder.Configuration["Jwt:Key"]
?? throw new InvalidOperationException("JWT key is not configured.");
builder.Services.AddAuthentication(
JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters =
new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey =
new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(jwtKey)),
ValidateIssuer = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidateAudience = true,
ValidAudience = builder.Configuration["Jwt:Audience"],
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
});
builder.Services.AddAuthorization();
builder.Services.AddControllers();
var app = builder.Build();
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
Conclusion
JWT authentication is a practical way to secure ASP.NET Core Web APIs. The basic implementation consists of a few important steps:
- Install the JWT Bearer authentication package.
- Configure the JWT signing key, issuer, audience, and expiration.
- Configure JWT authentication in
Program.cs. - Generate a JWT after successfully validating user credentials.
- Send the token using the
Authorization: Bearerheader. - Protect endpoints using
[Authorize]. - Use roles and claims when more granular authorization is required.
The example in this article uses hard-coded credentials to demonstrate the JWT flow. In a real application, authentication should be connected to a user database, passwords should be securely hashed, secrets should be stored outside source control, and refresh-token handling should be designed carefully.
Once you understand this basic flow, you can extend it to support ASP.NET Core Identity, database-backed users, refresh tokens, role-based authorization, Angular/React authentication, and enterprise identity providers.
JWT is only one part of API security, but understanding how it works gives you a solid foundation for building secure ASP.NET Core applications.


Comments (0)