JWT authentication in ASP.NET Core Web API
Tags: webdev, dotnet, api, jwt
Published: 2023-11-27 10:50:00

## What is JSON Web Token(JWT)?

JSON Web Token (JWT) is a secure and compact way of transmitting information between parties as a JSON object. It consists of three parts: header, payload, and signature. The header contains the algorithm used to sign the token, the payload contains the claims, and the signature is used to verify the authenticity of the token. JWTs are commonly used for authentication and authorization purposes in web applications.

Compared to other token formats, JWT has several benefits:

- Compactness: JWT is a compact format that can be easily transmitted over the network.
- Self-contained: JWT contains all the necessary information within itself, which means that it does not require any additional lookup to validate the token.
- Security: JWT can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA. This makes JWTs secure and tamper-proof.
- Ease of use: JWT is easy to use and can be integrated with various programming languages and frameworks.

## Create a minimal API project in Visual Studio Code

Please follow the instructions [here](https://rajasekar.dev/vs-code-setup-for-csharp-development) if you do not already have VS Code and the C# Dev kit is installed.

Open Visual Studio Code -> Explorer -> Create .NET Project -> select ASP.NET Core Web API project as shown in the below image and select the project folder location.

![VS Code WebAPI](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/90y3ap3x7tnsleswd0bl.png)

This will create a new API project with the weather data endpoint.

### Test it using Swagger

Click the F5 key, select the C# from the debugger list and default Launch configuration (it will be asked the first time). The Swagger API page will open with endpoints.

![Sample API Code with Swagger](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ixr7xboux9pxdy55swo5.png)

Select the "weatherforecast" endpoint, click "Try it out" and click the "Execute" button. Congratulations, now you can see the result as like below. The URL `http://localhost:5231/weatherforecast` can be tested using Postman or similar tools.

![API response](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/chrn0ib4kuuh058soc96.png)

## Implement the JWT authentication step-by-step

Follow the below steps one by one to implement the JWT authentication and implement the authorization with the "admin" role.

### Add the JWT nuget package

Execute the below command in the terminal to install the JwtBearer package. Make sure you are running the below command in the project folder.

```csharp
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
```

### Add JWT secrets in appsettings.json

Before proceeding, you should first add JWT secure key and other necessary details in the appsettings.json.

```csharp
"Jwt": {
    "Issuer": "https://rajasekar.dev/",
    "Audience": "https://rajasekar.dev/",
    "Key": "This is a secure key, requires a key size of at least '128' bits"
  }
```

### Configure JWT authentication and authorization

Add the below namespaces as we are going to use the same in the project

```csharp
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
```

Add authentication and authorization services as below.

```csharp
builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(o =>
{
    o.TokenValidationParameters = new TokenValidationParameters
    {
        ValidIssuer = builder.Configuration["Jwt:Issuer"],
        ValidAudience = builder.Configuration["Jwt:Audience"],
        IssuerSigningKey = new SymmetricSecurityKey
(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"] ?? string.Empty)),
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,
        ClockSkew = TimeSpan.Zero //required if the expire time is <5 minutes
    };
});
//Adding authorization
// builder.Services.AddAuthorization(); //Used if no authorization policy required
builder.Services.AddAuthorizationBuilder()
  .AddPolicy("admin_policy", policy => policy.RequireRole("admin"));
```

Here we are adding JWT-bearer authentication using the default scheme and configuring `TokenValidationParameters` with a secure key along with other necessary values.

Using `AddAuthorizationBuilder`, we are adding a new policy `admin_policy` to authorize the admin role.

These settings will be used to validate the JWT token when a request comes to API.

Now you should enable the authentication and authorization with the below code.

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

### Create login endpoint to authenticate users and return JWT token generation

First, create a new login model to use in the login endpoint

```csharp
record Login(string Email, string Password);
```

Now we can create a new login endpoint with the below code.

```csharp
//Login endpoint that returns Jwt token on successful authentication
app.MapPost("/login", [AllowAnonymous] (Login user) =>
{
    var normalUser = AuthenticateNormalUser(user);
    var adminUser = AuthenticateAdminUser(user);
    if (!(normalUser || adminUser))
        return Results.Unauthorized();

    var issuer = builder.Configuration["Jwt:Issuer"];
    var audience = builder.Configuration["Jwt:Audience"];
    var key = Encoding.ASCII.GetBytes(builder.Configuration["Jwt:Key"] ?? string.Empty);
    var claims = new List<Claim>()
        {
            new Claim("Id", Guid.NewGuid().ToString()),
            new Claim(JwtRegisteredClaimNames.Sub, user.Email),
            new Claim(JwtRegisteredClaimNames.Email, user.Email),
            new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
        };
    if (adminUser)
    {
        claims.Add(new Claim(ClaimTypes.Role, "admin"));
    }
    var tokenDescriptor = new SecurityTokenDescriptor
    {
        Subject = new ClaimsIdentity(claims),
        Expires = DateTime.UtcNow.AddMinutes(5), //should be at least 5 minutes - https://github.com/IdentityServer/IdentityServer3/issues/1251
        Issuer = issuer,
        Audience = audience,
        SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha512Signature)
    };
    var tokenHandler = new JwtSecurityTokenHandler();
    var token = tokenHandler.CreateToken(tokenDescriptor);
    var stringToken = tokenHandler.WriteToken(token);
    return Results.Ok(stringToken);
});
static bool AuthenticateNormalUser(Login user)
{
    //Check the given user credential is valid - Usually this should be checked from database
    return user.Email == "hello@example.com" && user.Password == "pass123";
}
static bool AuthenticateAdminUser(Login user)
{
    //Check the given user credential is valid - Usually this should be checked from database
    return user.Email == "admin@example.com" && user.Password == "admin123";
}
```

We have created two methods `AuthenticateNormalUser` and `AuthenticateAdminUser` to validate the user's credentials and decide whether the user is a normal or an admin user.

For the sake of simplicity, we are not using the database logic to check the user's credentials.

At least any one of the above methods should return True to authenticate users but for admin access the method `AuthenticateAdminUser` must return True. If both are not true, the unauthorized error will be thrown.

The issuer, audience, and secure key details are used from the `appsettings.json` file to create tokens. We are adding user details to tokens like UserId and email via `Claims`. Additionally, an "admin" role will be added to the claims if the user is an admin.

Finally new `SecurityTokenDescriptor` object will be created with all the above values and the token will be created using `JwtSecurityTokenHandler`.

### Add the endpoints with authorization

Add the below secure endpoints, one for a regular authenticated user and another one for an admin user. Admin endpoint added with `admin_policy` which will be validated before accessing the admin page.

```csharp
//Secure endpoint - All users
app.MapGet("/secure-page", () =>
{
    return "secure page - for all authenticated users 🔐";
})
.RequireAuthorization();

//Secure endpoint - admin user
app.MapGet("/admin-page", () =>
{
    return "Admin page - only for admin users 🔐";
})
.RequireAuthorization("admin_policy");
```

### Adding authorization bearer token support in Swagger UI (optional)

To test the above endpoints using Postman or similar tools, this step is not required.

Since we are using SwaggerUI, you should add the following code for JWT token support just below the `builder.Services.AddEndpointsApiExplorer()`.

```csharp
builder.Services.AddSwaggerGen(option =>
{
    option.SwaggerDoc("v1", new OpenApiInfo { Title = "Web API with Jwt Authentication", Version = "v1" });
    option.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
        In = ParameterLocation.Header,
        Description = "Please enter a valid token",
        Name = "Authorization",
        Type = SecuritySchemeType.Http,
        BearerFormat = "JWT",
        Scheme = "Bearer"
    });
    option.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
        {
            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference
                {
                    Type=ReferenceType.SecurityScheme,
                    Id="Bearer"
                }
            },
            new string[]{}
        }
    });
});
```

### Testing the JWT authentication

You can either run `dotnet run` in the terminal or press the F5 key to run the application.

The application will be running in the localhost with any port that is mentioned in the terminal. Open the localhost link (ex. http://localhost:5033/swagger/index.html) in the browser and it will look like below.

![Swagger API page with endpoints](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2gs82gc383ekkn68xmzf.png)

Click `login` endpoint -> "Try it out", use the below values in the request body and click execute.

```json
{
  "email": "hello@example.com",
  "password": "pass123"
}
```

The result will look like below.

![JWT Token](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vbxjawp88hdimetu15nv.png)

Copy the token, click the "Authorize" button, past it, click the Authorize button and close the popup. Now the page is saved with a valid JWT token.

![Authorize token](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/53psutt0cp2vwbta074q.png)

Now test the `secure-page` and the successful output will look like below.

![secure-page API Response](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8lsltkim9loxffdbksv6.png)

If the user is not authorized or the authorization token is expired, the below 401 error will be shown.

![Unauthorized error](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mp946z8uapiz29eojzg8.png)

Test the admin-page and it will show the 403 error because the above user is authenticated successfully but not authorized to view the admin page.

![Forbidden error](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9ul14o33fy876v3ja8s5.png)

Again, go to the login endpoint and use the below admin user to log in. Save the generated token in the "Authorize" section.

```json
{
  "email": "admin@example.com",
  "password": "admin123"
}
```

Now test the endpoint `admin-page` and it will show the below successful result. Even `secure-page` endpoint will work with the above admin credentials.

![Admin page API result](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4yiz4fi3izebgqu917dl.png)

### Complete source code

Find the complete source code of the Program.cs below.

```csharp
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();

//Adding authorization bearer token support in Swagger UI
builder.Services.AddSwaggerGen(option =>
{
    option.SwaggerDoc("v1", new OpenApiInfo { Title = "Web API with Jwt Authentication", Version = "v1" });
    option.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
        In = ParameterLocation.Header,
        Description = "Please enter a valid token",
        Name = "Authorization",
        Type = SecuritySchemeType.Http,
        BearerFormat = "JWT",
        Scheme = "Bearer"
    });
    option.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
        {
            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference
                {
                    Type=ReferenceType.SecurityScheme,
                    Id="Bearer"
                }
            },
            new string[]{}
        }
    });
});

//Configure Jwt authentication 
builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(o =>
{
    o.TokenValidationParameters = new TokenValidationParameters
    {
        ValidIssuer = builder.Configuration["Jwt:Issuer"],
        ValidAudience = builder.Configuration["Jwt:Audience"],
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"] ?? string.Empty)),
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,
        ClockSkew = TimeSpan.Zero //required if the expire time is <5 minutes
    };
});
//Adding authorization
// builder.Services.AddAuthorization(); //Used when no authorization policy is required, otherwise use below AddAuthorizationBuilder
builder.Services.AddAuthorizationBuilder()
  .AddPolicy("admin_policy", policy => policy.RequireRole("admin"));

var app = builder.Build();

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

app.UseHttpsRedirection();

//Enable Authentication and Authorization
app.UseAuthentication();
app.UseAuthorization();

//Login endpoint that returns Jwt token on successful authentication
app.MapPost("/login", [AllowAnonymous] (Login user) =>
{
    var normalUser = AuthenticateNormalUser(user);
    var adminUser = AuthenticateAdminUser(user);
    if (!(normalUser || adminUser))
        return Results.Unauthorized();

    var issuer = builder.Configuration["Jwt:Issuer"];
    var audience = builder.Configuration["Jwt:Audience"];
    var key = Encoding.ASCII.GetBytes(builder.Configuration["Jwt:Key"] ?? string.Empty);
    var claims = new List<Claim>()
        {
            new Claim("Id", Guid.NewGuid().ToString()),
            new Claim(JwtRegisteredClaimNames.Sub, user.Email),
            new Claim(JwtRegisteredClaimNames.Email, user.Email),
            new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
        };
    if (adminUser)
    {
        claims.Add(new Claim(ClaimTypes.Role, "admin"));
    }
    var tokenDescriptor = new SecurityTokenDescriptor
    {
        Subject = new ClaimsIdentity(claims),
        Expires = DateTime.UtcNow.AddMinutes(5), //should be at least 5 minutes - https://github.com/IdentityServer/IdentityServer3/issues/1251
        Issuer = issuer,
        Audience = audience,
        SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha512Signature)
    };
    var tokenHandler = new JwtSecurityTokenHandler();
    var token = tokenHandler.CreateToken(tokenDescriptor);
    var stringToken = tokenHandler.WriteToken(token);
    return Results.Ok(stringToken);
});
static bool AuthenticateNormalUser(Login user)
{
    //Check the given user credential is valid - Usually this should be checked from database
    return user.Email == "hello@example.com" && user.Password == "pass123";
}
static bool AuthenticateAdminUser(Login user)
{
    //Check the given user credential is valid - Usually this should be checked from database
    return user.Email == "admin@example.com" && user.Password == "admin123";
}

//Secure endpoint - All users
app.MapGet("/secure-page", () =>
{
    return "secure page - for all authenticated users 🔐";
})
.RequireAuthorization();

//Secure endpoint - admin user
app.MapGet("/admin-page", () =>
{
    return "Admin page - only for admin users 🔐";
})
.RequireAuthorization("admin_policy");

app.Run();

//Login user model
record Login(string Email, string Password);
```

### Some key points

No logout option is possible because of no cookie option. You should manually remove the stored JWT token from the client-side application but still, the token is valid until it expires.

Generally the token `Expires` setting should be a minimum of 5 minutes (refer https://github.com/IdentityServer/IdentityServer3/issues/1251).

## Conclusion

You should now understand how to implement the JWT authentication in ASP.NET Core Web API with role-based authorization.

> Visit my blog at [rajasekar.dev](https://rajasekar.dev/) for more articles

References:

[JSON Web Token Introduction - jwt.io](https://jwt.io/introduction/)

[How to implement JWT authentication in ASP.NET Core 6](https://www.infoworld.com/article/3669188/how-to-implement-jwt-authentication-in-aspnet-core-6.html)

[Authentication and authorization in minimal APIs](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/security?view=aspnetcore-8.0)

[Role based JWT Tokens in ASP.NET Core APIs - Rick Strahl's Web Log](https://weblog.west-wind.com/posts/2021/Mar/09/Role-based-JWT-Tokens-in-ASPNET-Core)