Skip to content
Rajasekar Su
Go back

JWT authentication in ASP.NET Core Web API

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:

Create a minimal API project in Visual Studio Code.

Please follow the instructions here 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.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1700978232338/073345dc-fd7f-49ce-8c86-507e18f74684.png align=“center”)

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.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1700978985561/572cec7b-adc5-489d-8134-c22a2af7a65a.png align=“center”)

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.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1700979206274/ddec9821-b22e-4780-a100-74a1b767b316.png align=“center”)

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.

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.

 "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

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.

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 admin roles.

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.

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

How to Create a JWT Token: Generate a Login Endpoint to Authenticate Users

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

record Login(string Email, string Password);

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

//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.

//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().

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.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1700990104259/0a54805a-a0dc-4507-968c-3334bf2ace8b.png align=“center”)

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

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

The result will look like below.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1700990672720/b7512cc6-510f-4632-a26f-7309b47bf26c.png align=“center”)

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.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1700990787255/65bb3a24-d475-4650-bb4c-d67a5474a15c.png align=“center”)

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

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1700990962484/ec3f3475-bdd0-4c92-8738-08b048a476a6.png align=“center”)

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

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1700991098076/5a92bfe4-78eb-43e9-a3fa-3731cd1e6747.png align=“center”)

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.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1700991225830/af74edf9-13cd-4c73-8bb5-2832b30e0466.png align=“center”)

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

{
  "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.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1700991742366/f6232df7-ad4c-4572-a15e-267586047af7.png align=“center”)

Complete source code

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

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

Conclusion

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

References:

JSON Web Token Introduction - jwt.io

How to implement JWT authentication in ASP.NET Core 6 | InfoWorld

Authentication and authorization in minimal APIs | Microsoft Learn

Role based JWT Tokens in ASP.NET Core APIs - Rick Strahl’s Web Log (west-wind.com)


Share this post on:

Previous Post
API Development with C#: A Comprehensive Guide
Next Post
VS Code setup for C# development