.NET 6.0 JWT Token Authentication C# API Tutorial

In this tutorial, we will be implementing JWT (JSON Web Token) tokens to authenticate users in a C# API built in .NET 6.0 and ASP.NET Core. We will be implementing users and roles using ASP.NET Core Identity.

Our API will compromise of an endpoint that issues a JWT token when a successful email address and password combination are sent. We will have another endpoint that returns data if the user is authorized to do so.

By the end of this tutorial, we will have built a secure API that issues a JWT to authenticated users and serves data to authorized users.

Authorization will be conducted via the JWT token and roles will be implemented to ensure users are authorized to access certain endpoints. We will use Postman to ensure our API and JWT token are working as expected.

We will also explore what JWT tokens are and how they are typically used.

Jump to section:

What is a JWT token?

How are JWT tokens secured?

What is a JWT token used for?

Build a .NET 6 Web API with JWT Authentication

Testing JWT token using Postman

Next steps

What is a JWT token?

JWT stands for JSON Web Token and is a compact, URL-safe, open industry standard for securely sharing information or claims between two parties.

A claim is a statement that a person, organisation or object may make about itself. This could be a name, department or role.

How are JWT tokens secured?

When generated, JWT tokens are signed using either a private secret or a public/private key combination. When received, JWT tokens are verified with a key to ensure the token has not been modified in transit.

If using a private secret, this would be used by the issuing computer when generating tokens. This same key would also be used by any recipients of JWT tokens to verify the legitimacy of the JWT token.

Alternatively, a public/private key combination can be used. In this scenario, a public key would be widely distributed but the private key would only be stored with the issuer to create the signature. This is more secure than having a private secret on both parties, as a private key would only need to be in one place.

What is a JWT token used for?

JWT tokens are commonly used by APIs to issue JWT tokens to successfully authenticated users. The JWT token would contain the identity of the authenticated user as well as any roles that they were a part of.

If the claims were to include the identity of the user and the roles they belong to, these could be used for authorization purposes on the client.

Build a .NET 6 Web API with JWT Authentication

Configuring Data Access Layer and ASP.NET Core Identity

We’ll start our API project by first creating the data access layer, that is setting up the DbContext.

In the past, I have installed an instance of SQL Express onto my laptop. However, I’ve now moved into using the LocalDb that comes shipped with Visual Studio. It successfully serves the same purpose and it requires less software to be installed on my laptop.

First, we’ll create a folder titled Data and inside it place the following ApplicationDbContext class:

public class ApplicationDbContext : IdentityDbContext<ApplicationUser, ApplicationRole, Guid>
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);
        }
    }

In another folder titled Models, create the following classes to represent our users and roles:

public class ApplicationUser : IdentityUser<Guid>
    {
    }
public class ApplicationRole : IdentityRole<Guid>
    {
        public ApplicationRole(string name)
        {
            Name = name;
        }
    }

From the very start of a project, I always like to create my own classes that extend from IdentityUser and IdentityRole respectively as it means the classes are already in place should I wish to add my own custom properties to each model.

Next, we have to register our DbContext in program.cs, as well as configure ASP.NET Core Identity. We do this by adding the following lines after CreateBuilder:

var builder = WebApplication.CreateBuilder(args);

<strong>builder.Services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

builder.Services.AddIdentity<ApplicationUser, ApplicationRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders();

builder.Services.Configure<IdentityOptions>(options =>
{
    options.User.RequireUniqueEmail = true;
});</strong>

User registration and seeding data

Now we’ll start building our API endpoints, starting with the Register endpoint. We’ll first need a class to represent the input from the user. I’ve placed the following DTOs in a shared library so that they can be used by a client once one is implemented.

In a new folder titled DTOs, add the following UserRegisterDTO:

public class UserRegisterDTO
    {
        [Required]
        [EmailAddress]
        public string? Email { get; set; }

        [Required]
        public string? Password { get; set; }
    }

Add the following UserRegisterResultDTO:

public class UserRegisterResultDTO
    {
        public bool Succeeded { get; set; }

        public IEnumerable<string> Errors { get; set; }
    }

Next, create a new UserController and add the following API controller with Register method:

[ApiController]
    [Route("[controller]")]
    public class UserController : ControllerBase
    {
        private readonly UserManager<ApplicationUser> _userManager;
        private readonly RoleManager<ApplicationRole> _roleManager;

        public UserController(
            UserManager<ApplicationUser> userManager,
            RoleManager<ApplicationRole> roleManager)
        {
            _userManager = userManager;
            _roleManager = roleManager;
        }

        [HttpPost]
        [Route("register")]
        [ProducesResponseType(StatusCodes.Status201Created)]
        [ProducesResponseType(StatusCodes.Status409Conflict)]
        public async Task<IActionResult> Register([FromBody] UserRegisterDTO userRegisterDTO)
        {
            IdentityResult result;

            ApplicationUser newUser = new()
            {
                Email = userRegisterDTO.Email,
                UserName = userRegisterDTO.Email,
                SecurityStamp = Guid.NewGuid().ToString(),
            };

            result = await _userManager.CreateAsync(newUser, userRegisterDTO.Password);

            if (!result.Succeeded)
                return Conflict(new UserRegisterResultDTO
                {
                    Succeeded = result.Succeeded,
                    Errors = result.Errors.Select(e => e.Description)
                });

            await SeedRoles();
            result = await _userManager.AddToRoleAsync(newUser, UserRoles.User);

            return CreatedAtAction(nameof(Register), new UserRegisterResultDTO { Succeeded = true });
        }
    }

Note that we have defined the possible results of our endpoint using ProducesResponseType. This is also reflected in the Swagger API documentation.

We return a status code 409 if the user tries to register with an email that already exists. Upon successful user creation and before adding our user to a role, a method is called to ensure our user roles exist. Add the following SeedRoles method underneath the Register method.

private async Task SeedRoles()
        {
            if (!await _roleManager.RoleExistsAsync(UserRoles.Admin))
                await _roleManager.CreateAsync(new ApplicationRole(UserRoles.User));

            if (!await _roleManager.RoleExistsAsync(UserRoles.User))
                await _roleManager.CreateAsync(new ApplicationRole(UserRoles.User));
        }

Next, create a Constants folder and add the following UserRoles class:

public static class UserRoles
    {
        public const string Admin = "Admin";
        public const string User = "User";
    }

Issuing JWT token

Now that users are able to register, we will next create the login section of our API. If a user is successfully authenticated, they will be issued a JWT token. This token can then be included with subsequent API calls to ensure that requests made to our API are authenticated.

Let’s first add the appropriate DTOs and the following Login method to our UserController:

public class UserLoginDTO
    {
        [Required]
        [EmailAddress]
        public string? Email { get; set; }

        [Required]
        [DataType(DataType.Password)]
        public string? Password { get; set; }
    }
public class UserLoginResultDTO
    {
        public bool Succeeded { get; set; }

        public string Message { get; set; }

        public TokenDTO Token { get; set; }
    }
public class TokenDTO
    {
        public string Token { get; set; }

        public DateTime Expiration { get; set; }
    }
private readonly UserManager<ApplicationUser> _userManager;
        private readonly RoleManager<ApplicationRole> _roleManager;
        <strong>private readonly IClaimsService _claimsService;
        private readonly IJwtTokenService _jwtTokenService;</strong>

        public UserController(
            UserManager<ApplicationUser> userManager,
            RoleManager<ApplicationRole> roleManager,
            <strong>IClaimsService claimsService,
            IJwtTokenService jwtTokenService</strong>)
        {
            _userManager = userManager;
            _roleManager = roleManager;
            <strong>_claimsService = claimsService;
            _jwtTokenService = jwtTokenService;</strong>
        }

        [HttpPost]
        [Route("login")]
        [ProducesResponseType(StatusCodes.Status200OK)]
        [ProducesResponseType(StatusCodes.Status401Unauthorized)]
        public async Task<IActionResult> Login([FromBody] UserLoginDTO userLoginDTO)
        {
            var user = await _userManager.FindByEmailAsync(userLoginDTO.Email);

            if (user != null && await _userManager.CheckPasswordAsync(user, userLoginDTO.Password))
            {
                var userClaims = await _claimsService.GetUserClaimsAsync(user);

                var token = _jwtTokenService.GetJwtToken(userClaims);

                return Ok(new UserLoginResultDTO
                {
                    Succeeded = true,
                    Token = new TokenDTO
                    {
                        Token = new JwtSecurityTokenHandler().WriteToken(token),
                        Expiration = token.ValidTo
                    }
                });
            }

            return Unauthorized(new UserLoginResultDTO
            {
                Succeeded = false,
                Message = "The email and password combination was invalid."
            });
        }

Note that we are returning an unauthorized 401 status code should the user submit an invalid email and password combination.

You’ll also notice two additional services being injected into our API controller: IClaimsService and IJwtTokenService.

If the email and password combination match, we obtain a list of claims from our IClaimsService. These claims are then passed into the IJwtTokenService which is where our JWT token is created, before being returned along with an Ok 200 status code.

In a new folder titled named Services, create the following ClaimsService:

public interface IClaimsService
    {
        Task<List<Claim>> GetUserClaimsAsync(ApplicationUser user);
    }

    public class ClaimsService : IClaimsService
    {
        private readonly UserManager<ApplicationUser> _userManager;

        public ClaimsService(UserManager<ApplicationUser> userManager)
        {
            _userManager = userManager;
        }

        public async Task<List<Claim>> GetUserClaimsAsync(ApplicationUser user)
        {
            List<Claim> userClaims = new()
            {
                new Claim(ClaimTypes.Name, user.Email),
                new Claim(ClaimTypes.Email, user.Email)
            };

            var userRoles = await _userManager.GetRolesAsync(user);

            foreach (var userRole in userRoles)
            {
                userClaims.Add(new Claim(ClaimTypes.Role, userRole));
            }

            return userClaims;
        }
    }

Here we are creating a claim for Email and a claim for each Role that the user is a part of. You may wish to extend the claims that are being returned based on your own ApplicationUser model or any other relevant data you wish to share with the client.

Now we’ve got our list of claims, we can issue our JWT token. Create another class named JwtTokenService:

public interface IJwtTokenService
    {
        JwtSecurityToken GetJwtToken(List<Claim> userClaims);
    }

    public class JwtTokenService : IJwtTokenService
    {
        private readonly IConfiguration _configuration;

        public JwtTokenService(IConfiguration configuration)
        {
            _configuration = configuration;
        }

        public JwtSecurityToken GetJwtToken(List<Claim> userClaims)
        {
            var authSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Secret"]));
            var expiryInMinutes = Convert.ToInt32(_configuration["Jwt:ExpiryInMinutes"]);

            var token = new JwtSecurityToken(
                issuer: _configuration["Jwt:ValidIssuer"],
                audience: _configuration["Jwt:ValidAudience"],
                claims: userClaims,
                expires: DateTime.Now.AddMinutes(expiryInMinutes),
                signingCredentials: new SigningCredentials(authSigningKey, SecurityAlgorithms.HmacSha256)
            );

            return token;
        }
    }

We will also need a few values in place in our appsettings:

"Jwt": {
    "ValidAudience": "trystanwilcock.com",
    "ValidIssuer": "jwttest.com",
    "Secret": "dCmBUCuVnSrXurUWVbaTbvqcejpEWh",
    "ExpiryInMinutes": 180
  }

We can see that a key is created using the Secret specified in appsettings, this is then used to sign the JWT token. The expiry of the token also derives from appsettings and here is set to 3 hours.

The Issuer denotes the party that created the token and Audience is the intended recipient of the token.

All of these values must match for the token to be valid; this will be demonstrated during the testing part of this tutorial.

Now all that’s left is to register our authentication method in program.cs:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

builder.Services.AddIdentity<ApplicationUser, ApplicationRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders();

builder.Services.Configure<IdentityOptions>(options =>
{
    options.User.RequireUniqueEmail = true;
});

<strong>builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
    options.SaveToken = true;
    options.RequireHttpsMetadata = false;
    options.TokenValidationParameters = new TokenValidationParameters()
    {
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateIssuerSigningKey = true,
        ValidateLifetime = true,
        ValidIssuer = builder.Configuration["Jwt:ValidIssuer"],
        ValidAudience = builder.Configuration["Jwt:ValidAudience"],
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Secret"])),
        ClockSkew = TimeSpan.Zero, // Messes with expiry!
    };
});

builder.Services.AddTransient<IClaimsService, ClaimsService>();
builder.Services.AddTransient<IJwtTokenService, JwtTokenService>();
</strong>
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

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

app.UseHttpsRedirection();

<strong>app.UseAuthentication();
app.UseAuthorization();</strong>

app.MapControllers();

app.Run();

One key thing to note here is how ClockSkew is set to Zero. By default, the clock skew is set to 5 minutes, meaning there’s a 5-minute buffer where the token is still valid after its expiry time. Setting the clock skew to zero eliminates this.

Testing JWT token using Postman

Before we can conduct any testing on our API, we first need a secure endpoint that only authorized users are able to access.

Create the following WeatherForecastController and WeatherForecastDTO:

[Authorize]
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    private static readonly string[] Summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    [HttpGet(Name = "GetWeatherForecast")]
    public IEnumerable<WeatherForecastDTO> Get()
    {
        return Enumerable.Range(1, 5).Select(index => new WeatherForecastDTO
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .ToArray();
    }
}
public class WeatherForecastDTO
    {
        public DateTime Date { get; set; }

        public int TemperatureC { get; set; }

        public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);

        public string? Summary { get; set; }
    }

Note the Authorize tag above our controller; we have already configured our API for JWT so this is the authentication scheme that our API will use.

Let’s first make a call to our API without any JWT token present:

As expected, we are returned a 401 unauthorized status code. Let’s now register a user:

Ensure to make a POST request, that the Body is set to raw and that JSON has been selected.

We get a 201 created status code, meaning the user was created successfully. Let’s make the same request to our API in order to try and register a user that already exists:

Upon trying to register a user that already exists, we get a 409 conflict status code as expected.

Now that we have registered a user, we can log in and obtain our bearer token:

Make sure to copy this token value. Heading back to our original GET request, we can now add our token to the request.

Go to the Authorization tab and select Bearer Token. Paste in the token in the field provided.

Now our request is authenticated and we are presented with data.

You may notice earlier in the SeedRoles method, I created two roles but only added the user to one of them. Let’s update our endpoint to only allow administrator access:

[Authorize(Roles = UserRoles.Admin)]

Let’s perform the same request again, with the same token:

Although we are authenticated as a user, we are unable to perform the request as we are not authorized to do so.

Next steps

In this tutorial, we have created a C# web API in .NET 6.0 and ASP.NET Core. Our API could be used as the backend to a web application to authenticate and authorize users.

We have implemented roles and users using ASP.NET Identity as our user store. We are able to issue expiring JWT tokens to successfully authenticated users.

In a future tutorial, we will be building the frontend client using Blazor. We will build registration and login to our client and consume our API built here.

What this tutorial doesn’t cover is what to do if the JWT token has expired. This is where refresh tokens come in and we’ll be covering this is another future tutorial.

Thanks for hanging around and I hope this tutorial has managed to clear some things when it comes to implementing JWT token authentication in a .NET 6.0 API.

1 thought on “.NET 6.0 JWT Token Authentication C# API Tutorial”

  1. Pingback: .NET 6.0 Blazor WebAssembly JWT Token Authentication From Scratch C# Tutorial – Trystan Wilcock

Comments are closed.

Scroll to Top