Back to: ASP.NET Core Web API Tutorials
JWT Authentication in ASP.NET Core Web API
In this article, I will discuss how to implement Token-Based Authentication using JWT in ASP.NET Core Web API Application. Please read our previous article discussing CORS in ASP.NET Core Web API. In this article and our few upcoming articles, we will discuss Token-Based JWT Authentication in ASP.NET Core Web API according to industry standards. The articles will be lengthy as I try to explain the concepts in depth. Please read the articles in the same sequence as I have provided here to get clarity. I have divided these JWT concepts into five articles. They are as follows:
- Part-1: Implementing Authentication Server with the Token-Based JWT Authentication. We will discuss this concept in this article.
- Part-2: Creating Resource Server and Client Application.
- Part-3: Implementing Refresh Token with JWT Authentication.
- Part-4: Providing Logout Endpoint to Revoke the Refresh Token.
- Part-5: Implementing Role-Based Authentication using JWT Token.
Why Do We Need Token-Based Authentication in ASP.NET Core Web API?
ASP.NET Core Web API is a modern framework provided by Microsoft for building HTTP-based services on the .NET Core platform. These services are widely used by various clients, such as:
- Web Browsers
- Mobile Applications
- Desktop Applications
- IoT Devices, etc.
Nowadays, the use of Web API is increasing rapidly. So, we should know how to develop Web APIs. Only developing Web APIs is not enough if there is no security. So, it is also essential for us to implement security in our Web API services, which ensures that only authenticated and authorized users or applications can access protected resources. Without proper security measures, sensitive data can be exposed, and unauthorized users may gain access. One of the most preferred approaches for securing Web APIs is Token-Based Authentication, where:
- The user authenticates with the server and receives a signed token.
- This token contains enough information to identify the user and must sent with every subsequent request to access secured endpoints.
What is JWT-Based Token Authentication?
JSON Web Token (JWT) is a widely used standard for token-based authentication. It is commonly used in modern authentication systems because it is compact, self-contained, and easy to verify. Please visit the following URL to see what the JWT token looks like:
Once you visit the above page, you will see the following.
As you can see on the above page, a JWT typically contains three parts: Header, Payload, and Signature. Before implementing them in our ASP.NET Core Web API Application, let us understand these three parts in detail.
JWT Header:
The JWT Header contains metadata about the type of token and the cryptographic algorithm used to sign the token. It usually includes two fields:
- alg: Specifies the signing algorithm (e.g., HMACSHA256, RSA, etc.).
- typ: Specifies the token type, which is usually “JWT”.
Note:
- HMAC (Symmetric Key): Uses the same secret key for signing and verification.
- RSA (Asymmetric Key): A private key is used for signing, and a public key is used for verification.
Example JWT Header:
{ "alg": "HS256", "typ": "JWT" }
This JSON object is then Base64Url encoded to form the first part of the JWT. The Base64Url encoded string is something like this: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
JWT Payload:
The payload of JWT (JSON Web Token) is the very important part where the data is stored. The payload consists of a set of claims, which are statements about an entity (usually the user) and additional data. The payload is represented as a JSON object and can include different claims:
- iss (Issuer): Identifies the principal that issued the JWT (e.g., the domain name of the authentication server).
- sub (Subject): The user ID or username that the token represents.
- aud (Audience): The intended recipient(s) of the token. It could be the domain name of the client application.
- exp (Expiration Time): The time at which the token will expire. It’s usually represented in Unix time (also known as Epoch time).
- iat (Issued At): The time at which the token was issued, also in Unix time.
- jti (JWT ID): A unique identifier for the token.
Example of a JWT Payload:
{ "iss": "http://example.com", "sub": "user12345", "aud": "http://exampleapp.com", "exp": 1700001234, "iat": 1699990200, "jti": "uniqueid123", "role": "admin", "nickname": "johnny123", "profileId": "PID123" }
This JSON object is also Base64Url encoded to form the second part of the JWT. The Base64Url encoded string is something like this: eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
JWT Signature:
The JWT signature is used to verify that the token’s payload has not been altered. To generate the signature, the server combines the encoded header and payload with a secret key and applies the signing algorithm specified in the header. For example, if you are using the HMAC SHA256 algorithm, the signature will be created like this:
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secretKey)
The resulting signature is Base64Url encoded to form the third part of the JWT. The Base64Url encoded string is something like this: SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Final JWT Token:
The output is three Base64-URL strings separated by dots. The structure looks like this:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
- The first part is the Base64Url encoded Header.
- The second part is the Base64Url encoded Payload.
- The third part is the Base64Url encoded Signature.
This JWT Token format is easily transmitted via URLs, HTTP headers, or HTTP bodies.
How JWT Authentication Works in ASP.NET Core Web API:
JWT Authentication in ASP.NET Core Web API typically involves three main components:
- Client: The entity (such as a browser, mobile app, or IoT device) that requests access to the protected resources.
- Authorization Server: The server responsible for authenticating the client and issuing the JWT.
- Resource Server: The server that hosts the protected resources and verifies the JWT provided by the client.
For a better understanding of how it works in ASP.NET Core Web API, please have a look at the following diagram:
Let us understand how JWT Authentication works in a typical client-server application:
Credential Submission:
The process begins when the User (typically through a browser, mobile app, or another client) submits their credentials (e.g., username and password) to the Client. The Client receives the credentials from the User and sends them to the Authorization Server for verification.
Authorization Server Issues Access and Refresh Tokens:
The Authorization Server validates the credentials received from the Client by verifying them against the Database. If the credentials are valid, the Authorization Server generates two types of tokens and sent back to the client:
- Access Token: This is a short-lived token used to authenticate the user for requesting access to secured resources in the Resource Server. It typically expires within a short period (e.g., 15 minutes to 1 hour).
- Refresh Token: This is a long-lived (e.g., valid for several days) token that refreshes the Access Token when it expires. The Refresh Token allows users to continue using the application without logging in again.
Client Receives Tokens:
The Client receives the Access Token and Refresh Token from the Authorization Server. These tokens are typically stored securely by the client (in local storage, session storage, or cookies) for use in subsequent requests.
Client Makes Requests to Resource Server:
For each subsequent request to access protected resources on the Resource Server, the Client must include the Access Token in the HTTP Authorization header of the request. The token typically follows the Bearer schema: Authorization: Bearer <access_token>. This allows the Client to authenticate itself without sending the username and password again.
Resource Server Validates the Token:
The Resource Server receives the request and validates it with the Access Token. This validation includes checking the token’s signature, expiration time, issuer, and audience claims to ensure it is still valid. If the Access Token is valid, the Resource Server processes the request and responds to the client with the requested resource (e.g., user profile, orders, etc.).
Access Denied or Token Renewal:
If the Access Token is expired or invalid, the Resource Server will deny access to the requested resource, typically returning a 401 Unauthorized response. In this case, the Client can send the Refresh Token to the Authorization Server to obtain a new Access Token (potentially a new Refresh Token). The client uses the new access token for future requests.
Example to Understand JWT-Based Token Authentication in ASP.NET Core:
We will develop three applications to understand JWT-based token authentication in ASP.NET Core Web API. They are as follows:
- Authentication Server Application: This will be an ASP.NET Core Web API Project, providing an endpoint for validating the Client Credential and Generating the Access Token and Refresh Token.
- Resource Server Application: This will also be an ASP.NET Core Web API Project, exposing secured endpoints to be consumed by clients.
- Client Application: This will be a .NET Core Console Application. This application will first generate the access token using the endpoint provided by the Authentication Server application. Then, using the access token it will access the secure endpoints from the Resource Server Application.
Let us proceed with implementing these applications step by step.
Creating the Authentication Server Application
The Authentication Server Application will handle user authentication and token generation. So, create a new ASP.NET Core Web API Project named JWTAuthServer. Once you create the Project, then please install the following Packages, which are required for Password Hashing, JWT, and Entity Framework Core:
- Microsoft.EntityFrameworkCore.SqlServer
- Microsoft.EntityFrameworkCore.Tools
- Microsoft.AspNetCore.Authentication.JwtBearer
- BCrypt.Net-Next
You can install the Package using NuGet Package Manager for Solution or by executing the following command in the Visual Studio Package Manager Console:
- Install-Package Microsoft.EntityFrameworkCore.SqlServer
- Install-Package Microsoft.EntityFrameworkCore.Tools
- Install-Package Microsoft.AspNetCore.Authentication.JwtBearer
- Install-Package BCrypt.Net-Next
Define the Entities
We will create three primary entities: User, Role, UserRole, SigningKey, and Client. Additionally, we will manage the many-to-many relationship between User and Role using a join entity UserRole. First, create a folder named Models in the project root directory, where we will create all our entities.
Creating Role Entity:
Create a class file named Role.cs within the Models folder and copy and paste the following code. The User model will hold the user information. The Role entity will hold the application roles and be connected to the User entity through the UserRole join table.
using System.ComponentModel.DataAnnotations; namespace JWTAuthServer.Models { public class Role { // Primary key for the Role entity. [Key] public int Id { get; set; } // Name of the role (e.g., Admin, User). [Required] [MaxLength(50)] public string Name { get; set; } //Role Description public string? Description { get; set; } // Navigation property for the relationship with UserRole. public ICollection<UserRole> UserRoles { get; set; } } }
Creating User Entity:
Create a class file named User.cs within the Models folder and copy and paste the following code. The User model will hold the user information.
using Microsoft.EntityFrameworkCore; using System.ComponentModel.DataAnnotations; namespace JWTAuthServer.Models { [Index(nameof(Email), Name = "IX_Unique_Email", IsUnique = true)] public class User { [Key] public int Id { get; set; } [Required] public string Email { get; set; } [Required] public string Firstname { get; set; } public string? Lastname { get; set; } [Required] [StringLength(100)] public string Password { get; set; } public ICollection<UserRole> UserRoles { get; set; } // Navigation property for many-to-many relationship with Role } }
Creating Client Entity
Create a class file named Client.cs within the Models folder and copy and paste the following code. This class represents a client that can request tokens from the auth server, typically identified by the ClientId.
using System.ComponentModel.DataAnnotations; namespace JWTAuthServer.Models { public class Client { [Key] public int Id { get; set; } // Unique identifier for the client application. [Required] [MaxLength(100)] public string ClientId { get; set; } // Name of the client application. [Required] [MaxLength(100)] public string Name { get; set; } // URL for the client application. [Required] [MaxLength(200)] public string ClientURL { get; set; } } }
UserRole Entity (Join Table)
To manage the many-to-many relationship between User and Role, we will create a join entity, UserRole. So, create a class file named UserRole.cs in the Models folder and then copy and paste the following code:
namespace JWTAuthServer.Models { public class UserRole { // Foreign key referencing User. public int UserId { get; set; } // Navigation property to User. public User User { get; set; } // Foreign key referencing Role. public int RoleId { get; set; } // Navigation property to Role. public Role Role { get; set; } } }
SigningKey Entity:
We will use the SigningKey entity that holds the key information to manage RSA keys dynamically. So, create a new class file named SigningKey.cs within the Models folder and copy and paste the following code.
using System.ComponentModel.DataAnnotations; namespace JWTAuthServer.Models { public class SigningKey { [Key] public int Id { get; set; } // Unique identifier for the key (Key ID). [Required] [MaxLength(100)] public string KeyId { get; set; } // The RSA private key. [Required] public string PrivateKey { get; set; } // The RSA public key in XML or PEM format. [Required] public string PublicKey { get; set; } // Indicates if the key is active. [Required] public bool IsActive { get; set; } // Date when the key was created. [Required] public DateTime CreatedAt { get; set; } // Date when the key is set to expire. [Required] public DateTime ExpiresAt { get; set; } } }
Create Data Transfer Objects (DTO)
Data Transfer Objects (DTOs) are used to encapsulate data sent between the client and server, ensuring that only necessary information is exposed. So, first, create a folder named DTOs in the project root directory where we will create all our Data Transfer Objects.
Create RegisterDTO
This DTO captures the necessary information for user registration. So, create a class file named RegisterDTO.cs within the DTOs folder and copy and paste the following code.
using System.ComponentModel.DataAnnotations; namespace JWTAuthServer.DTOs { public class RegisterDTO { [Required(ErrorMessage = "First name is required.")] [MaxLength(50, ErrorMessage = "First name must be less than or equal to 50 characters.")] public string Firstname { get; set; } [MaxLength(50, ErrorMessage = "Last name must be less than or equal to 50 characters.")] public string Lastname { get; set; } [Required(ErrorMessage = "Email is required.")] [EmailAddress(ErrorMessage = "Invalid email address.")] [MaxLength(100, ErrorMessage = "Email must be less than or equal to 100 characters.")] public string Email { get; set; } [Required(ErrorMessage = "Password is required.")] [MinLength(6, ErrorMessage = "Password must be at least 6 characters long.")] [MaxLength(100, ErrorMessage = "Password must be less than or equal to 100 characters.")] public string Password { get; set; } } }
Create UpdateProfileDTO
This DTO allows users to update their profile information. So, create a class file named UpdateProfileDTO.cs within the DTOs folder and copy and paste the following code.
using System.ComponentModel.DataAnnotations; namespace JWTAuthServer.DTOs { public class UpdateProfileDTO { [MaxLength(50, ErrorMessage = "First name must be less than or equal to 50 characters.")] public string Firstname { get; set; } [MaxLength(50, ErrorMessage = "Last name must be less than or equal to 50 characters.")] public string Lastname { get; set; } [EmailAddress(ErrorMessage = "Invalid email address.")] [MaxLength(100, ErrorMessage = "Email must be less than or equal to 100 characters.")] public string Email { get; set; } [MinLength(6, ErrorMessage = "Password must be at least 6 characters long.")] [MaxLength(100, ErrorMessage = "Password must be less than or equal to 100 characters.")] public string Password { get; set; } } }
Create ProfileDTO
This DTO represents the user’s profile information returned by the Get Profile endpoint. So, create a class file named ProfileDTO.cs within the DTOs folder and copy and paste the following code.
namespace JWTAuthServer.DTOs { public class ProfileDTO { public int Id { get; set; } public string Email { get; set; } public string Firstname { get; set; } public string? Lastname { get; set; } public List<string> Roles { get; set; } } }
Creating LoginDTO:
When a client wants to request a token, they will send client credentials (so we know which application is requesting the token) and user credentials (so we know which user is logging in). So, create a class file named LoginDTO.cs within the DTOs folder and then copy and paste the following code.
using System.ComponentModel.DataAnnotations; namespace JWTAuthServer.DTOs { public class LoginDTO { // Email input from the user during login. [EmailAddress] [Required(ErrorMessage = "Email is required.")] [MaxLength(100, ErrorMessage = "Email must be less than or equal to 100 characters.")] public string Email { get; set; } // Password input from the user during login. [Required(ErrorMessage = "Password is required.")] [MinLength(6, ErrorMessage = "Password must be at least 6 characters long.")] [MaxLength(100, ErrorMessage = "Password must be less than or equal to 100 characters.")] public string Password { get; set; } [Required(ErrorMessage = "ClientId is required.")] public string ClientId { get; set; } } }
Create a Database Context
The ApplicationDbContext class manages the database connections and configurations for EF Core. First, create a folder named Data in the project root directory and then add a new class file named ApplicationDbContext.cs within the Data folder and then copy and paste the following code.
using JWTAuthServer.Models; using Microsoft.EntityFrameworkCore; namespace JWTAuthServer.Data { public class ApplicationDbContext : DbContext { // Constructor accepting DbContextOptions and passing them to the base class. public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { } // Override OnModelCreating to configure entity properties and relationships. protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); // Configure the UserRole entity as a join table for User and Role. modelBuilder.Entity<UserRole>() .HasKey(ur => new { ur.UserId, ur.RoleId }); // Composite primary key. //Defines the many-to-many relationship between User and Role. modelBuilder.Entity<UserRole>() .HasOne(ur => ur.User) .WithMany(u => u.UserRoles) .HasForeignKey(ur => ur.UserId); modelBuilder.Entity<UserRole>() .HasOne(ur => ur.Role) .WithMany(r => r.UserRoles) .HasForeignKey(ur => ur.RoleId); // Seed initial data for Roles, Users, Clients, and UserRoles. modelBuilder.Entity<Role>().HasData( new Role { Id = 1, Name = "Admin", Description = "Admin Role" }, new Role { Id = 2, Name = "Editor", Description = " Editor Role" }, new Role { Id = 3, Name = "User", Description = "User Role" } ); modelBuilder.Entity<Client>().HasData( new Client { Id = 1, ClientId = "Client1", Name = "Client Application 1", ClientURL = "https://client1.com" }, new Client { Id = 2, ClientId = "Client2", Name = "Client Application 2", ClientURL = "https://client2.com" } ); } // DbSet representing the Users table. public DbSet<User> Users { get; set; } // DbSet representing the Roles table. public DbSet<Role> Roles { get; set; } // DbSet representing the Clients table. public DbSet<Client> Clients { get; set; } // DbSet representing the UserRoles join table. public DbSet<UserRole> UserRoles { get; set; } // DbSet representing the SigningKeys table. public DbSet<SigningKey> SigningKeys { get; set; } } }
Background Services in ASP.NET Core
Background Services in ASP.NET Core are long-running services that execute tasks in the background, independent of incoming HTTP requests. They are implemented by creating classes inherited from the BackgroundService base class or implementing the IHostedService interface.
Let us create a background service to manage the key Rotation. The Purpose of the Key Rotation Background Service is as follows:
- Regularly generates new RSA key pairs and deactivates old ones to enhance security.
- Ensures that the JWKS endpoint always has up-to-date public keys for resource servers to validate tokens.
- Minimizes the risk associated with key compromise by limiting the lifespan of each key.
Creating Key Rotation Background Service
So, first, create a folder named Services in the project root directory. Then, create a class file named KeyRotationService.cs within the Services folder and copy and paste the following code. As you can see, this class is inherited from the BackgroundService class. The following code is self-explained. So, please read the comment lines for a better understanding:
using JWTAuthServer.Data; using JWTAuthServer.Models; using Microsoft.EntityFrameworkCore; using System.Security.Cryptography; namespace JWTAuthServer.Services { // This class defines a background service that periodically rotates cryptographic keys. public class KeyRotationService : BackgroundService { // Service provider is used to create a scoped service lifetime. private readonly IServiceProvider _serviceProvider; // Sets how frequently keys should be rotated; here it’s every 7 days. private readonly TimeSpan _rotationInterval = TimeSpan.FromDays(7); // Constructor that accepts a service provider for dependency injection. public KeyRotationService(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } // This method is executed when the background service starts. protected override async Task ExecuteAsync(CancellationToken stoppingToken) { // Loop that runs until the service is stopped. while (!stoppingToken.IsCancellationRequested) { // Perform the key rotation logic. await RotateKeysAsync(); // Wait for the configured rotation interval before running again. await Task.Delay(_rotationInterval, stoppingToken); } } // This method handles the actual key rotation logic. private async Task RotateKeysAsync() { // Create a new service scope for dependency injection. using var scope = _serviceProvider.CreateScope(); // Retrieve the database context from the service provider. var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>(); // Query the database for the currently active signing key. var activeKey = await context.SigningKeys.FirstOrDefaultAsync(k => k.IsActive); // Check if there’s no active key or if the active key is about to expire. if (activeKey == null || activeKey.ExpiresAt <= DateTime.UtcNow.AddDays(10)) { // If there's an active key, mark it as inactive. if (activeKey != null) { // Mark the current key as inactive since it’s about to be replaced. activeKey.IsActive = false; // Update the current key in the database. context.SigningKeys.Update(activeKey); } // Generate a new RSA key pair. using var rsa = RSA.Create(2048); // Export the private key as a Base64-encoded string. var privateKey = Convert.ToBase64String(rsa.ExportRSAPrivateKey()); // Export the public key as a Base64-encoded string. var publicKey = Convert.ToBase64String(rsa.ExportRSAPublicKey()); // Generate a unique identifier for the new key. var newKeyId = Guid.NewGuid().ToString(); // Create a new SigningKey entity with the new RSA key details. var newKey = new SigningKey { KeyId = newKeyId, PrivateKey = privateKey, PublicKey = publicKey, IsActive = true, CreatedAt = DateTime.UtcNow, ExpiresAt = DateTime.UtcNow.AddYears(1) // Set the new key to expire in one year. }; // Add the new key to the database. await context.SigningKeys.AddAsync(newKey); // Save the changes to the database. await context.SaveChangesAsync(); } } } }
How Does the KeyRotationService Work?
As long as the service is registered using AddHostedService, ASP.NET Core will handle its instantiation and execution without manual intervention. The KeyRotationService will work as follows:
- ExecuteAsync Method: This is the core method where the background task logic resides. It’s overridden from the BackgroundService base class.
- Infinite Loop with Delays: Typically, ExecuteAsync contains a loop that runs indefinitely (or until the application stops), performing periodic tasks with delays in between to prevent tight looping.
Key Rotation Logic:
- Check for Expiring Keys: The service periodically checks the database for active signing keys nearing expiration (e.g., within 10 days).
- Deactivate Old Keys: If an active key is about to expire, it’s marked as inactive.
- Generate New Keys: A new RSA key pair is generated, stored in the database, and marked active.
Create Users Controller
We will create a new controller named UsersController to handle user-related operations. So, create a new API Empty Controller named UsersController within the Controllers folder and then copy and paste the following code:
using JWTAuthServer.Data; using JWTAuthServer.DTOs; using JWTAuthServer.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; namespace JWTAuthServer.Controllers { [Route("api/[controller]")] [ApiController] public class UsersController : ControllerBase { private readonly ApplicationDbContext _context; // Constructor injecting the ApplicationDbContext public UsersController(ApplicationDbContext context) { _context = context; } // Registers a new user. [HttpPost("Register")] public async Task<IActionResult> Register([FromBody] RegisterDTO registerDto) { // Validate the incoming model. if (!ModelState.IsValid) { return BadRequest(ModelState); } // Check if the email already exists. var existingUser = await _context.Users .FirstOrDefaultAsync(u => u.Email.ToLower() == registerDto.Email.ToLower()); if (existingUser != null) { return Conflict(new { message = "Email is already registered." }); } // Hash the password using BCrypt. string hashedPassword = BCrypt.Net.BCrypt.HashPassword(registerDto.Password); // Create a new user entity. var newUser = new User { Firstname = registerDto.Firstname, Lastname = registerDto.Lastname, Email = registerDto.Email, Password = hashedPassword }; // Add the new user to the database. _context.Users.Add(newUser); await _context.SaveChangesAsync(); // Optionally, assign a default role to the new user. // For example, assign the "User" role. var userRole = await _context.Roles.FirstOrDefaultAsync(r => r.Name == "User"); if (userRole != null) { var newUserRole = new UserRole { UserId = newUser.Id, RoleId = userRole.Id }; _context.UserRoles.Add(newUserRole); await _context.SaveChangesAsync(); } return CreatedAtAction(nameof(GetProfile), new { id = newUser.Id }, new { message = "User registered successfully." }); } // Retrieves the authenticated user's profile. [HttpGet("GetProfile")] [Authorize] public async Task<IActionResult> GetProfile() { // Extract the user's email from the JWT token claims. var emailClaim = User.Claims.FirstOrDefault(c => c.Type == System.Security.Claims.ClaimTypes.Email); if (emailClaim == null) { return Unauthorized(new { message = "Invalid token: Email claim missing." }); } string userEmail = emailClaim.Value; // Retrieve the user from the database, including roles. var user = await _context.Users .Include(u => u.UserRoles) .ThenInclude(ur => ur.Role) .FirstOrDefaultAsync(u => u.Email.ToLower() == userEmail.ToLower()); if (user == null) { return NotFound(new { message = "User not found." }); } // Map the user entity to ProfileDTO. var profile = new ProfileDTO { Id = user.Id, Email = user.Email, Firstname = user.Firstname, Lastname = user.Lastname, Roles = user.UserRoles.Select(ur => ur.Role.Name).ToList() }; return Ok(profile); } // Updates the authenticated user's profile. [HttpPut("UpdateProfile")] [Authorize] public async Task<IActionResult> UpdateProfile([FromBody] UpdateProfileDTO updateDto) { // Validate the incoming model. if (!ModelState.IsValid) { return BadRequest(ModelState); } // Extract the user's email from the JWT token claims. var emailClaim = User.Claims.FirstOrDefault(c => c.Type == System.Security.Claims.ClaimTypes.Email); if (emailClaim == null) { return Unauthorized(new { message = "Invalid token: Email claim missing." }); } string userEmail = emailClaim.Value; // Retrieve the user from the database. var user = await _context.Users .FirstOrDefaultAsync(u => u.Email.ToLower() == userEmail.ToLower()); if (user == null) { return NotFound(new { message = "User not found." }); } // Update fields if provided. if (!string.IsNullOrEmpty(updateDto.Firstname)) { user.Firstname = updateDto.Firstname; } if (!string.IsNullOrEmpty(updateDto.Lastname)) { user.Lastname = updateDto.Lastname; } if (!string.IsNullOrEmpty(updateDto.Email)) { // Check if the new email is already taken by another user. var emailExists = await _context.Users .AnyAsync(u => u.Email.ToLower() == updateDto.Email.ToLower() && u.Id != user.Id); if (emailExists) { return Conflict(new { message = "Email is already in use by another account." }); } user.Email = updateDto.Email; } if (!string.IsNullOrEmpty(updateDto.Password)) { // Hash the new password before storing. string hashedPassword = BCrypt.Net.BCrypt.HashPassword(updateDto.Password); user.Password = hashedPassword; } // Save the changes to the database. _context.Users.Update(user); await _context.SaveChangesAsync(); return Ok(new { message = "Profile updated successfully." }); } } }
Creating Authentication Controller:
Create an API controller to authenticate a user and issue a JWT token. So, create an API Empty controller named AuthController within the Controllers folder and copy and paste the following code.
using JWTAuthServer.Data; using JWTAuthServer.DTOs; using JWTAuthServer.Models; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Security.Cryptography; namespace JWTAuthServer.Controllers { [Route("api/[controller]")] [ApiController] public class AuthController : ControllerBase { // Private fields to hold the configuration and database context // Holds configuration settings from appsettings.json or environment variables private readonly IConfiguration _configuration; // Database context for interacting with the database private readonly ApplicationDbContext _context; // Constructor that injects IConfiguration and ApplicationDbContext via dependency injection public AuthController(IConfiguration configuration, ApplicationDbContext context) { // Assign the injected IConfiguration to the private field _configuration = configuration; // Assign the injected ApplicationDbContext to the private field _context = context; } // Define the Login endpoint that responds to POST requests at 'api/Auth/Login' [HttpPost("Login")] public async Task<IActionResult> Login([FromBody] LoginDTO loginDto) { // Validate the incoming model based on data annotations in LoginDTO if (!ModelState.IsValid) { // If the model is invalid, return a 400 Bad Request with validation errors return BadRequest(ModelState); } // Query the Clients table to verify if the provided ClientId exists var client = _context.Clients .FirstOrDefault(c => c.ClientId == loginDto.ClientId); // If the client does not exist, return a 401 Unauthorized response if (client == null) { return Unauthorized("Invalid client credentials."); } // Retrieve the user from the Users table by matching the email (case-insensitive) // Also include the UserRoles and associated Roles for later use var user = await _context.Users .Include(u => u.UserRoles) // Include the UserRoles navigation property .ThenInclude(ur => ur.Role) // Then include the Role within each UserRole .FirstOrDefaultAsync(u => u.Email.ToLower() == loginDto.Email.ToLower()); // If the user does not exist, return a 401 Unauthorized response if (user == null) { // For security reasons, avoid specifying whether the client or user was invalid return Unauthorized("Invalid credentials."); } // Verify the provided password against the stored hashed password using BCrypt bool isPasswordValid = BCrypt.Net.BCrypt.Verify(loginDto.Password, user.Password); // If the password is invalid, return a 401 Unauthorized response if (!isPasswordValid) { // Again, avoid specifying whether the client or user was invalid return Unauthorized("Invalid credentials."); } // At this point, authentication is successful. Proceed to generate a JWT token. var token = GenerateJwtToken(user, client); // Return the generated token in a 200 OK response return Ok(new { Token = token }); } // Private method responsible for generating a JWT token for an authenticated user private string GenerateJwtToken(User user, Client client) { // Retrieve the active signing key from the SigningKeys table var signingKey = _context.SigningKeys.FirstOrDefault(k => k.IsActive); // If no active signing key is found, throw an exception if (signingKey == null) { throw new Exception("No active signing key available."); } // Convert the Base64-encoded private key string back to a byte array var privateKeyBytes = Convert.FromBase64String(signingKey.PrivateKey); // Create a new RSA instance for cryptographic operations var rsa = RSA.Create(); // Import the RSA private key into the RSA instance rsa.ImportRSAPrivateKey(privateKeyBytes, out _); // Create a new RsaSecurityKey using the RSA instance var rsaSecurityKey = new RsaSecurityKey(rsa) { // Assign the Key ID to link the JWT with the correct public key KeyId = signingKey.KeyId }; // Define the signing credentials using the RSA security key and specifying the algorithm var creds = new SigningCredentials(rsaSecurityKey, SecurityAlgorithms.RsaSha256); // Initialize a list of claims to include in the JWT var claims = new List<Claim> { // Subject (sub) claim with the user's ID new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()), // JWT ID (jti) claim with a unique identifier for the token new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), // Name claim with the user's first name new Claim(ClaimTypes.Name, user.Firstname), // NameIdentifier claim with the user's email new Claim(ClaimTypes.NameIdentifier, user.Email), // Email claim with the user's email new Claim(ClaimTypes.Email, user.Email) }; // Iterate through the user's roles and add each as a Role claim foreach (var userRole in user.UserRoles) { claims.Add(new Claim(ClaimTypes.Role, userRole.Role.Name)); } // Define the JWT token's properties, including issuer, audience, claims, expiration, and signing credentials var tokenDescriptor = new JwtSecurityToken( issuer: _configuration["Jwt:Issuer"], // The token issuer, typically your application's URL audience: client.ClientURL, // The intended recipient of the token, typically the client's URL claims: claims, // The list of claims to include in the token expires: DateTime.UtcNow.AddHours(1), // Token expiration time set to 1 hour from now signingCredentials: creds // The credentials used to sign the token ); // Create a JWT token handler to serialize the token var tokenHandler = new JwtSecurityTokenHandler(); // Serialize the token to a string var token = tokenHandler.WriteToken(tokenDescriptor); // Return the serialized JWT token return token; } } }
What is JWKS?
JSON Web Key Set (JWKS) is a JSON structure that represents a set of public keys used to verify the signatures of JWTs (JSON Web Tokens). It allows resource servers to dynamically retrieve the public keys necessary for validating tokens without hardcoding them. The following are the Key Components of JWKS:
- Keys: Each key in the JWKS is a JSON object that contains key parameters such as kty (key type), use (usage), kid (key ID), n (modulus), and e (exponent) for RSA keys.
- Endpoint: The JWKS is typically exposed via a standardized endpoint (e.g., /.well-known/jwks.json) that resource servers can query to obtain the current set of public keys. If you want, then you can change the endpoint.
Implementing the JWKS Controller
Create an endpoint that exposes the public keys in JWKS format. Resource servers will use this endpoint to retrieve the necessary keys for token validation. So, create a new API Empty controller named JWKSController within the Controllers folder and then copy and paste the following code:
using JWTAuthServer.Data; using Microsoft.AspNetCore.Mvc; using Microsoft.IdentityModel.Tokens; using System.Security.Cryptography; namespace JWTAuthServer.Controllers { [Route(".well-known")] [ApiController] public class JWKSController : ControllerBase { // Private field to hold the database context private readonly ApplicationDbContext _context; // Constructor that injects the ApplicationDbContext via dependency injection public JWKSController(ApplicationDbContext context) { // Assign the injected ApplicationDbContext to the private field _context = context; } // Define the JWKS endpoint that responds to GET requests at '/.well-known/jwks.json' [HttpGet("jwks.json")] public IActionResult GetJWKS() { // Retrieve all active signing keys from the database var keys = _context.SigningKeys.Where(k => k.IsActive).ToList(); // Construct the JWKS (JSON Web Key Set) object var jwks = new { //kty, use, kid, alg, n, e keys = keys.Select(k => new { kty = "RSA", // Key type (RSA) use = "sig", // Usage (sig for signature) kid = k.KeyId, // Key ID to identify the key alg = "RS256", // Algorithm (RS256 for RSA SHA-256) n = Base64UrlEncoder.Encode(GetModulus(k.PublicKey)), // Modulus (Base64URL-encoded) e = Base64UrlEncoder.Encode(GetExponent(k.PublicKey)) // Exponent (Base64URL-encoded) }) }; // Return the JWKS object as a JSON response with status code 200 OK return Ok(jwks); } // Helper method to extract the modulus component from a Base64-encoded public key private byte[] GetModulus(string publicKey) { // Create a new RSA instance for cryptographic operations var rsa = RSA.Create(); // Import the RSA public key from its Base64-encoded representation rsa.ImportRSAPublicKey(Convert.FromBase64String(publicKey), out _); // Export the RSA parameters without including the private key var parameters = rsa.ExportParameters(false); // Dispose of the RSA instance to free up resources and prevent memory leaks rsa.Dispose(); if (parameters.Modulus == null) { throw new InvalidOperationException("RSA parameters are not valid."); } // Return the modulus component of the RSA key return parameters.Modulus; } // Helper method to extract the exponent component from a Base64-encoded public key private byte[] GetExponent(string publicKey) { // Create a new RSA instance for cryptographic operations var rsa = RSA.Create(); // Import the RSA public key from its Base64-encoded representation rsa.ImportRSAPublicKey(Convert.FromBase64String(publicKey), out _); // Export the RSA parameters without including the private key var parameters = rsa.ExportParameters(false); // Dispose of the RSA instance to free up resources and prevent memory leaks rsa.Dispose(); if (parameters.Exponent == null) { throw new InvalidOperationException("RSA parameters are not valid."); } // Return the exponent component of the RSA key return parameters.Exponent; } } }
JSON Web Key Sets (JWKS) Components:
The following components are commonly used in JSON Web Keys (JWKs) to represent cryptographic keys in a structured, interoperable format. Each has a specific meaning:
kty (Key Type): This indicates the type of cryptographic key. For example: “RSA” for RSA public keys. This field lets consumers of the JWK know what kind of algorithm the key is associated with and how it can be used.
use (Public Key Use): This indicates the intended use of the key. For example: “sig”: The key is used for signing and verifying digital signatures. By specifying use, the consumer of the JWK knows how the key should be applied.
kid (Key ID): This is a unique identifier for the key. It helps clients or servers distinguish between multiple keys in a JWK set (JWKS). Typically, kid is used to match the key in the JWKS with the key specified in a JSON Web Token’s (JWT’s) header. Having a kid allows the consumer to pick the correct public key for signature validation when multiple keys are available.
alg (Algorithm): This specifies the algorithm intended for use with the key. For example:
- “RS256”: RSA signature using SHA-256.
- “RS512”: RSA signature using SHA-512.
This tells consumers exactly which cryptographic algorithm they should use with the key.
n (Modulus): This is the modulus of the RSA public key, represented as a Base64 URL-encoded value. In RSA, the public key consists of a modulus (n) and an exponent (e). n is a large integer that forms part of the mathematical structure of the RSA key. It’s used in both encryption (when the public key is used) and signature verification.
e (Exponent): This is the public exponent of the RSA public key, represented as a Base64 URL-encoded value. In most cases, the exponent (e) is a small value. It works together with the modulus to define the public key used for RSA encryption or signature verification.
Configure the Database Connection
Set up the connection string in appsettings.json to connect to your SQL Server database. So please modify the appsettings.json file as follows:
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*", "ConnectionStrings": { "EFCoreDBConnection": "Server=LAPTOP-6P5NK25R\\SQLSERVER2022DEV;Database=JWTAuthDb;Trusted_Connection=True;TrustServerCertificate=True;" }, "Jwt": { "Issuer": "https://localhost:7022" } }
Modifying the Program Class:
Please modify the Program class as follows. When your application starts, ASP.NET Core initializes all registered services, including hosted services like KeyRotationService. Here, we have also written the logic to validate the token.
using JWTAuthServer.Data; using Microsoft.EntityFrameworkCore; using JWTAuthServer.Services; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; namespace JWTAuthServer { public class Program { public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); // Add controller services to the container and configure JSON serialization options builder.Services.AddControllers() .AddJsonOptions(options => { // Preserve property names as defined in the C# models (disable camelCase naming) options.JsonSerializerOptions.PropertyNamingPolicy = null; }); // Add services for generating Swagger/OpenAPI documentation builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); // Configure Entity Framework Core with SQL Server using the connection string from configuration builder.Services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("EFCoreDBConnection"))); // Register the KeyRotationService as a hosted (background) service // This service handles periodic rotation of signing keys to enhance security builder.Services.AddHostedService<KeyRotationService>(); // Configure Authentication using JWT Bearer tokens builder.Services.AddAuthentication(options => { // This indicates the authentication scheme that will be used by default when the app attempts to authenticate a user. // Which authentication handler to use for verifying who the user is by default. options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; // This indicates the authentication scheme that will be used by default when the app encounters an authentication challenge. // Which authentication handler to use for responding to failed authentication or authorization attempts. options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(options => { // Define token validation parameters to ensure tokens are valid and trustworthy options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, // Ensure the token was issued by a trusted issuer ValidIssuer = builder.Configuration["Jwt:Issuer"], // The expected issuer value from configuration ValidateAudience = false, // Disable audience validation (can be enabled as needed) ValidateLifetime = true, // Ensure the token has not expired ValidateIssuerSigningKey = true, // Ensure the token's signing key is valid // Define a custom IssuerSigningKeyResolver to dynamically retrieve signing keys from the JWKS endpoint IssuerSigningKeyResolver = (token, securityToken, kid, parameters) => { //Console.WriteLine($"Received Token: {token}"); //Console.WriteLine($"Token Issuer: {securityToken.Issuer}"); //Console.WriteLine($"Key ID: {kid}"); //Console.WriteLine($"Validate Lifetime: {parameters.ValidateLifetime}"); // Initialize an HttpClient instance for fetching the JWKS var httpClient = new HttpClient(); // Synchronously fetch the JWKS (JSON Web Key Set) from the specified URL var jwks = httpClient.GetStringAsync($"{builder.Configuration["Jwt:Issuer"]}/.well-known/jwks.json").Result; // Parse the fetched JWKS into a JsonWebKeySet object var keys = new JsonWebKeySet(jwks); // Return the collection of JsonWebKey objects for token validation return keys.Keys; } }; }); // Build the WebApplication instance based on the configured services and middleware var app = builder.Build(); // Enable Swagger middleware only in the development environment for API documentation and testing if (app.Environment.IsDevelopment()) { app.UseSwagger(); // Generates the Swagger JSON document app.UseSwaggerUI(); // Enables the Swagger UI for interactive API exploration } // Enforce HTTPS redirection to ensure secure communication app.UseHttpsRedirection(); // Enable Authentication middleware to process and validate incoming JWT tokens app.UseAuthentication(); // Enable Authorization middleware to enforce access policies based on user roles and claims app.UseAuthorization(); app.MapControllers(); app.Run(); } } }
IssuerSigningKeyResolver:
The IssuerSigningKeyResolver is a delegate used in the JWT Bearer authentication setup. Its primary purpose is to resolve and retrieve the appropriate signing keys used to validate incoming JWTs’ signatures. This is especially useful in scenarios where keys are rotated or multiple keys are available (e.g., during key rotation), as it allows the application to dynamically fetch and select the correct key based on the token’s metadata. The following is the IssuerSigningKeyResolver delegate signature:
IssuerSigningKeyResolver = (token, securityToken, kid, parameters) => { // Implementation to resolve and return signing keys };
Understanding the Parameters:
- token (String): Represents the raw JWT (JSON Web Token) as a string that is being validated. This is the complete token received from the client, typically in the Authorization header of an HTTP request.
- securityToken (SecurityToken): An instance of SecurityToken that represents the JWT in a parsed and tokenized form. This object provides access to various token properties, such as its header, claims, issuer, etc.
- kid (Key ID): Represents the Key ID (kid) claim extracted from the token’s header. The kid is a unique identifier that indicates which key was used to sign the token. This will determine which public key from the JWKS (JSON Web Key Set) should be used to validate the token’s signature.
- Parameters (TokenValidationParameters): The parameters and settings used to validate the token. This includes settings like issuer validation, audience validation, lifetime validation, and more.
Database Migration
Next, we need to generate the Migration and update the database schema. So, open the Package Manager Console and Execute the Add-Migration and Update-Database commands as follows.
With this, our Database with the required database tables should be created under the JWTAuthDb database, as shown in the image below:
Testing JWT Authentication API Using Postman
You can test the API endpoints and their JWT functionality to ensure each service works as expected using Postman, Swagger, Fiddler, etc. The suggested testing sequence is:
Register a New User
This endpoint allows a new user to create an account.
HTTP Method: POST
Endpoint: /api/Users/Register
Headers:
Content-Type: application/json
Body (Raw JSON):
{ "Firstname": "John", "Lastname": "Doe", "Email": "john.doe@example.com", "Password": "Password@123" }
Expected Response:
You will get a response indicating that the user was successfully registered, as shown below. You will get 201 HTTP Status code and the Location indicating the URI to access the newly created user. Let us assume the user is created with the Id 1.
{ "message": "User registered successfully." }
Login to Obtain a JWT Token
Authenticates the user and issues a JWT token upon successful login. Please ensure that ClientId corresponds to a valid client in your database (e.g., Client1).
HTTP Method: POST
Endpoint: /api/Auth/Login
Headers:
Content-Type: application/json
Body (Raw JSON):
{ "Email": "john.doe@example.com", "Password": "Password@123", "ClientId": "Client1" }
Expected Response:
You will get a response containing a valid JWT in the token field, as shown below if the provided credentials are valid. You will need this token to authenticate requests.
{ "Token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEyMzQ1NiJ9..." }
Understanding the Generated Token:
To understand the different parts of the generated token, please visit the https://jwt.io/ website. Replace your generated token in the Encoded section, and you should see the different parts in different colors in the Decoded section, you will see the details of the token.
Access Protected Endpoints
Now that you have a JWT token, you can access protected endpoints like Get Profile and Update Profile.
Get Profile
This endpoint retrieves the authenticated user’s profile information.
HTTP Method: GET
Endpoint: /api/Users/GetProfile
Headers:
Authorization: Bearer {JWT Token}}
Note: You need to replace {JWT Token} with the actual token you received when you call the Login endpoint.
Expected Response:
You will get a response containing the user’s profile information, including roles, as shown below. Please ensure that the data matches what was registered.
{ "Id": 1, "Email": "john.doe@example.com", "Firstname": "John", "Lastname": "Doe", "Roles": [ "User" ] }
Update Profile
This endpoint enables authenticated users to update their profile information.
HTTP Method: PUT
Endpoint: /api/Users/UpdateProfile
Headers:
Authorization: Bearer {JWT Token}}
Body (Raw JSON):
{ "Firstname": "Johnny", "Lastname": "Doe-Smith", "Email": "johnny.doe@example.com", "Password": "NewPassword@123" }
Note: You need to replace {JWT Token} with the actual token you received when you call the Login endpoint.
Expected Response:
You will get a response confirming that the profile has been updated. The user’s email and other details should now reflect the changes.
{ "message": "Profile updated successfully." }
Token-based authentication, specifically JWT, is a secure, stateless, and scalable approach for managing user authentication and authorization in ASP.NET Core Web API. It’s ideal for scenarios where the client and server are decoupled or distributed, and it provides a robust method to validate user identity and control access to protected resources. Understanding JWT structure, validation, and its role in authentication and authorization is essential for building secure APIs.
In the next article, I will discuss how to implement Resource Server and Client Applications with JWT Authentication. Here, in this article, I explain Token-Based Authentication using JWT in ASP.NET Core Web API Application with an Example. I hope you enjoy this JWT Authentication in ASP.NET Core Web API article.
is there a podcast or youtube tutorial of this article?