JWT Authentication in ASP.NET
Tags: programming, security, webdev, dotnet
Published: 2023-06-03 18:17:53

> ℹ️ **Information**
> _The code in the following article was tested with ASP.NET 6 and 7._

When it comes to security in web applications, one of the main challenges is ensuring user authentication and authorization. Among the various technologies available to manage authentication are JWTs.

## What is a JWT?

JWT stands for JSON Web Token and it's an open standard for creating access tokens that allow the secure sharing of information between different parties in a compact, self-contained format.

A JWT consists of three parts:

1. **Header**: It contains metadata that describes the token itself. It usually includes the type of token, which is JWT, and the signing algorithm used, like HMAC SHA256 or RSA.
2. **Payload**: It contains the so-called "claims", which are assertions about the subject (the user, in most cases) and additional information. There are three types of claims: registered, public, and private. Registered claims are a predefined set of claims with specific meanings, such as **iss** (token issuer), **exp** (token expiration date), **sub** (token subject), among others. Public and private claims are user-defined.
3. **Signature**: It is used to verify that the sender of the JWT is whom they claim to be and to ensure that the message has not been altered along the way.

Once created, the JWT is encoded and signed, making it a secure token to send between the client and server.

JWTs are commonly used to implement token-based authentication in [RESTful APIs](https://dev.to/fabriziobagala/rest-api-5ck0). When a user successfully authenticates, the server creates a JWT and sends it to the client. The client can then use the JWT to authenticate subsequent requests. Since the JWT contains information about the user's permissions, the server can use the JWT to authorize the operations requested by the client.

## Pros and cons of using JWT

One of the main advantages of using JWTs is their stateless nature. They contain all the necessary information for authentication, eliminating the need to maintain a server-side session. This saves resources and promotes application scalability. Additionally, JWTs are very compact, making them suitable for sending via URL, POST parameters, or in the HTTP header. This compactness is particularly useful when data needs to be sent across the network. From a security perspective, JWTs can be digitally signed, allowing for the verification of data integrity. If necessary, a JWT can be encrypted to ensure data confidentiality. Finally, a JWT is self-contained as it holds all the necessary information about who the user is and what permissions they have, significantly simplifying authorization decisions.

However, there are also drawbacks to using JWTs. Although JWTs are compact, they tend to be larger than alternatives such as session cookies. If the JWT's payload is too large, it could affect the performance of the application. Implementing JWTs can also be more complex than other solutions, such as session-based authentication. Furthermore, the security of JWTs is tied to the robustness of the encryption algorithm and key used. Since JWTs are stateless, invalidating or updating a JWT without contacting the server can be challenging, which can pose problems in scenarios like user logout or password change. Finally, from a security perspective, if a JWT is stolen, it can be used to impersonate the user and, due to the difficulty of invalidating a JWT, often the solution involves setting short expiration times and frequently renewing tokens.

Therefore, when deciding to use JWTs, it is important to carefully consider these advantages and disadvantages to determine if it suits the specific use case.

## Example of use of JWT

Here is an example of a minimal API in ASP.NET that uses the JWT to authenticate a user.

```csharp
var builder = WebApplication.CreateBuilder(args);

var jwtOptionsSection = builder.Configuration.GetRequiredSection("Jwt");
builder.Services.Configure<JwtOptions>(jwtOptionsSection);

builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(jwtOptions =>
{
    var configKey = jwtOptionsSection["Key"];
    var key = Encoding.UTF8.GetBytes(configKey);
    
    jwtOptions.TokenValidationParameters = new TokenValidationParameters
    {
        ValidIssuer = jwtOptionsSection["Issuer"],
        ValidAudience = jwtOptionsSection["Audience"],
        IssuerSigningKey = new SymmetricSecurityKey(key),
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateLifetime = false,
        ValidateIssuerSigningKey = true
    };
});

builder.Services.AddAuthorization();

builder.Services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.ReportApiVersions = true;
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ApiVersionReader = new UrlSegmentApiVersionReader();
});

builder.Services.AddTransient<IUserService, UserService>();
builder.Services.AddTransient<IJwtService, JwtService>();

var app = builder.Build();

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

var versionSet = app.NewApiVersionSet()
    .HasApiVersion(new ApiVersion(1.0))
    .ReportApiVersions()
    .Build();

app.MapPost("v{version:apiVersion}/token", [AllowAnonymous]([FromBody] UserInfo user,
    [FromServices] IUserService userService, [FromServices] IJwtService jwtService) =>
{
    var storedUser = userService.GetUser(user?.Username);
    if (!userService.IsAuthenticated(user?.Password, storedUser?.PasswordHash))
    {
        return Results.Unauthorized();
    }
    
    var tokenString = jwtService.GenerateToken(storedUser);
    return Results.Ok(new { token = tokenString });
}).WithApiVersionSet(versionSet);

app.MapGet("v{version:apiVersion}/weather", [Authorize]() =>
{
    var data = Enumerable.Range(1, 5)
        .Select(i => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(i),
            TemperatureC = Random.Shared.Next(-20, 55)
        })
        .ToArray();
    
    return Results.Ok(data);
}).WithApiVersionSet(versionSet);

app.Run();
```

Let's analyze this example step by step.

**Step 1: Creating the application**

```csharp
var builder = WebApplication.CreateBuilder(args);
```

We start by creating an instance of the application builder. This builder will be used to configure services and middleware before the application starts.

**Step 2: Configuring JWT options**

We use the [options pattern](https://dev.to/fabriziobagala/options-pattern-in-net-2abg) to read the `Jwt` section from the configuration file and map it onto an instance of the `JwtOptions` class.

```csharp
var jwtOptionsSection = builder.Configuration.GetRequiredSection("Jwt");
builder.Services.Configure<JwtOptions>(jwtOptionsSection);
```

`GetRequiredSection("Jwt")` retrieves the Jwt section from the configuration file. `Configure<JwtOptions>(jwtOptionsSection)` maps the settings of the `Jwt` section onto the `JwtOptions` object, making it available for injection into other classes through the dependency injection system.

The used `appsettings.json` configuration file is:

```json
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "Jwt": {
    "Issuer": "example.com",
    "Audience": "example.com",
    "Key": "This is a secure secret key"
  }
}
```

In this file, the `Jwt` section contains the settings related to the JWT, such as `Issuer`, `Audience`, and `Key`.

The `JwtOptions` class is a POCO (Plain Old CLR Object) class that corresponds to the settings of the `Jwt` section:

```csharp
public class JwtOptions
{
    public string Issuer { get; init; } = string.Empty;
    public string Audience { get; init; } = string.Empty;
    public string Key { get; init; } = string.Empty;
}
```

Each property in this class is initialized with the corresponding value from the `Jwt` section of the configuration file. This class will then be injected into any class that requires these settings.

> ⚠️ **Warning**
> _In a production environment, do not store sensitive data in the code or `appsettings.json`. Rather use [Vault Secrets](https://kubernetes.io/docs/concepts/configuration/secret/)._

**Step 3: Adding authentication**

```csharp
builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(jwtOptions =>
{
    var configKey = jwtOptionsSection["Key"];
    var key = Encoding.UTF8.GetBytes(configKey);
    
    jwtOptions.TokenValidationParameters = new TokenValidationParameters
    {
        ValidIssuer = jwtOptionsSection["Issuer"],
        ValidAudience = jwtOptionsSection["Audience"],
        IssuerSigningKey = new SymmetricSecurityKey(key),
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateLifetime = false,
        ValidateIssuerSigningKey = true
    };
});
```

The authentication service is added and configured to use JWT authentication as the default authentication scheme. The token validation options are set from `jwtOptionsSection`, which was configured in the previous step.

Note that the NuGet package [Microsoft.AspNetCore.Authentication.JwtBearer](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.JwtBearer) needs to be installed.

**Step 4: Adding authorization**

```csharp
builder.Services.AddAuthorization();
```

The authorization service is added, which allows access to controllers and actions to be restricted based on user roles or policies.

**Step 5: Adding API versioning**

```csharp
builder.Services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.ReportApiVersions = true;
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ApiVersionReader = new UrlSegmentApiVersionReader();
});
```

To be able to use API versioning, you need to install the NuGet package [Asp.Versioning.Http](https://www.nuget.org/packages/Asp.Versioning.Http).

In the code, we are setting the default version of the API to version 1.0, assuming this default version when not specified, and reading the API version from the URL segment of the request. We are also choosing to report the API versions in the HTTP responses, which can be useful for your API customers to know about the different versions available.

> 🔎 **Insight**
> _Even though it's just an example, I decided to add versioning because I consider it an important aspect when writing an API. Let's look at some key reasons:_
- _Seamless evolution: APIs change and evolve over time. You might need to add new features or change existing ones. Versioning allows you to make these changes without disrupting applications using previous versions of your API._
- _Controlled deprecation: You might want to deprecate some features or entire versions of your API in the future. Versioning allows you to do this in a controlled manner, providing users with a notice and a transition period before removing the deprecated features or versions._
- _Flexibility for users: Versioning gives your users the flexibility to choose which version of your API to use. For instance, they may choose to stick to a previous version if the changes introduced in a new version are not compatible with their applications._
- _Documentation and testing: Each version of your API can have its own documentation and test suite, which can help both you and your users to understand and verify the functionality of each version._

**Step 6: Services registration**

```csharp
builder.Services.AddTransient<IJwtService, JwtService>();
builder.Services.AddTransient<IUserService, UserService>();
```

In this step, `IJwtService` and `IUserService` are registered as transient services, which means a new instance will be created each time a component requests one of these services. They are responsible for managing the JWT and managing users, respectively.

Let's start with `IJwtService`. This interface defines a `GenerateToken` method, which takes a `UserDto` object as an argument and returns a string. This string represents the JWT token generated for the user.

```csharp
public record UserDto(Guid Id, string Username, string PasswordHash, string Email, string Role);
```

```csharp
public interface IJwtService
{
    string GenerateToken(UserDto? user);
}
```

The `JwtService` class implements `IJwtService`. It has a variable `_options` of type `JwtOptions`. This variable is initialized via the class constructor, which receives an `IOptions<JwtOptions>` object. `IOptions` is a wrapper containing configuration options (in this case `JwtOptions`) from a configuration file (for instance, `appsettings.json`).

The `GenerateToken` method of `JwtService` creates a JWT token for the user passed as an argument. First of all, it ensures the user is not null, otherwise, it throws an `ArgumentNullException`. Then, it creates a symmetric security key using the key present in the JWT options. Subsequently, it creates a security token descriptor. This descriptor contains a number of important information, such as the user's claims, the token's expiration date, the issuer, the audience, and the signing credentials. Finally, it uses `JwtSecurityTokenHandler` to create and write the token.

```csharp
public sealed class JwtService : IJwtService
{
    private readonly JwtOptions _options;

    public JwtService(IOptions<JwtOptions> options)
    {
        _options = options.Value ?? throw new ArgumentNullException(nameof(options));
    }
    
    public string GenerateToken(UserDto? user)
    {
        ArgumentNullException.ThrowIfNull(user);
        
        var key = Encoding.ASCII.GetBytes(_options.Key);
        var securityKey = new SymmetricSecurityKey(key);
        var tokenDescriptor = new SecurityTokenDescriptor
        {
            Subject = new ClaimsIdentity(new[]
            {
                new Claim(ClaimTypes.Name, user.Username),
                new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
                new Claim(ClaimTypes.Email, user.Email),
                new Claim(ClaimTypes.Role, user.Role)
            }),
            Expires = DateTime.UtcNow.AddMinutes(5),
            Issuer = _options.Issuer,
            Audience = _options.Audience,
            SigningCredentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256Signature)
        };
        var tokenHandler = new JwtSecurityTokenHandler();
        var token = tokenHandler.CreateToken(tokenDescriptor);
        return tokenHandler.WriteToken(token);
    }
}
```

The created token can be decoded through [JWT.IO](https://jwt.io/).

![jwt.io](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/flg5tcjl44107234hsdp.png)

The `IUserService` interface defines two methods: `GetUser`, which returns a `UserDto` object based on the provided username, and `IsAuthenticated`, which verifies if a password matches a provided password hash.

```csharp
public interface IUserService
{
    UserDto? GetUser(string? username);
    bool IsAuthenticated(string? password, string? passwordHash);
}
```

The `UserService` class implements `IUserService`. It has a variable `_users`, which is a list of `UserDto` objects. In the constructor of this class, this list is initialized with two example users, whose data are hardcoded.

The `GetUser` method returns a `UserDto` object that has the provided `username`, or `null` if such a user does not exist. The `IsAuthenticated` method checks if the provided password, once hashed, matches the provided password hash. It uses the `Verify` method of the NuGet package [BCrypt.Net-Next](https://www.nuget.org/packages/BCrypt.Net-Next/) to perform this check.

```csharp
public sealed class UserService : IUserService
{
    private readonly List<UserDto> _users;
    
    public UserService()
    {
        _users = new List<UserDto>
        {
            new(Guid.NewGuid(), "user1", BCrypt.Net.BCrypt.HashPassword("password1"), "user1@gmail.com", "Admin"),
            new(Guid.NewGuid(), "user2", BCrypt.Net.BCrypt.HashPassword("password2"), "user2@gmail.com", "User")
        };
    }
    
    public UserDto? GetUser(string? username)
    {
        ArgumentNullException.ThrowIfNull(username);
        
        return _users.SingleOrDefault(u => u.Username == username);
    }
    
    public bool IsAuthenticated(string? password, string? passwordHash)
    {
        ArgumentNullException.ThrowIfNull(password);
        ArgumentNullException.ThrowIfNull(passwordHash);
        
        return BCrypt.Net.BCrypt.Verify(password, passwordHash);
    }
}

```
**Step 7: Building the application**

```csharp
var app = builder.Build();
```

This code builds the web application with all the configurations and services that have been registered.

**Step 8: Middleware configuration**

```csharp
app.UseAuthentication();
app.UseAuthorization();
```

The authentication and authorization middleware are added to the application.

**Step 9: Creating the API version set**

```csharp
var versionSet = app.NewApiVersionSet()
    .HasApiVersion(new ApiVersion(1.0))
    .ReportApiVersions()
    .Build();
```

An API version set is created to be used with the endpoints.

Creating a version set is useful for grouping and managing API versions in a consistent way. The `NewApiVersionSet()` method creates a new version set. A specific API version can be added to this version set using the `HasApiVersion()` method.

Additionally, the `ReportApiVersions()` method enables API version reporting. This setting allows including information about supported versions in HTTP responses. This can be very helpful for developers using your API, as it allows them to quickly see what API versions are available.

Finally, the `Build()` method builds the version set. This version set is then used in combination with API endpoints to manage different API versions.

**Step 10: Defining the endpoints**

Endpoints represent access points to our API. In our example, we are defining two main endpoints: one for token generation and one for accessing protected data.

- **Token Endpoint (POST /token)**: This endpoint is responsible for generating the JWT token. It receives a `UserInfo` object in the request body, which contains user credentials (username and password). These details are then passed to the user service for authentication. If the user is correctly authenticated, the JWT service generates a token that is returned to the client. This endpoint is decorated with the `AllowAnonymous` attribute, meaning that no token needs to be provided to access it.

```csharp
public record UserInfo(string? Username, string? Password);
```

```csharp
app.MapPost("v{version:apiVersion}/token", [AllowAnonymous]([FromBody] UserInfo user,
    [FromServices] IUserService userService, [FromServices] IJwtService jwtService) =>
{
    var storedUser = userService.GetUser(user?.Username);
    if (!userService.IsAuthenticated(user?.Password, storedUser?.PasswordHash))
    {
        return Results.Unauthorized();
    }
    
    var tokenString = jwtService.GenerateToken(storedUser);
    return Results.Ok(new { token = tokenString });
}).WithApiVersionSet(versionSet);
```

- **Protected Endpoint (GET /weather)**: This endpoint is intended to provide protected data, in this case, a series of weather forecasts. The `Authorize` attribute ensures that only authenticated users with a valid token can access this endpoint. If a client tries to access this endpoint without providing a valid token, they will receive an HTTP 401 Unauthorized error.

```csharp
app.MapGet("v{version:apiVersion}/weather", [Authorize]() =>
{
    var data = Enumerable.Range(1, 5)
        .Select(i => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(i),
            TemperatureC = Random.Shared.Next(-20, 55)
        })
        .ToArray();
    
    return Results.Ok(data);
}).WithApiVersionSet(versionSet);
```

The endpoint routes use a version:apiVersion parameter that allows clients to specify the API version they want to use. This is part of our versioning strategy, as discussed in step 5.

**Step 11: Running the application**

```csharp
app.Run();
```

The application is started and begins listening for incoming requests.

## Conclusion

In this article, I addressed the nuances of implementing JSON Web Token (JWT) in ASP.NET for managing user authentication. I outlined the essentials of JWT and why it has become a popular choice in modern Web development. From configuring properties to generating and decoding tokens, I explored each step in the process. I also examined the advantages and potential disadvantages of its use.

In summary, the goal of this guide was to provide a foundation for integrating JWT authentication into ASP.NET applications. Although I have tried to cover the main aspects, I always remember that understanding the basic principles and adhering to security best practices are critical when using new technology.

## References

- [JWT (RFC 7519)](https://tools.ietf.org/html/rfc7519)
- [ASP.NET documentation](https://docs.microsoft.com/en-us/aspnet/core/?view=aspnetcore-6.0)
- [BCrypt.NET](https://github.com/BcryptNet/bcrypt.net)
- [ASP.NET API Versioning](https://github.com/microsoft/aspnet-api-versioning/wiki)