JWT Authentication In A .NET 5.0 Application

Introduction

This article will give a definition of JSON Web Tokens and shows how they are used in a .Net application. This article will refer to the article Build A ToDo List Application Using .NET 5.0 Web API and Microsoft SQL Server where I showed how to build a backend of a to-do list using .NET 5.0 Web API. The article will describe the structure of a JWT and explain how a JWT is generated in a .NET application.

Table of Contents

  • Tools
  • JWT Definition
  • JWT Structure
  • Generate a JWT in a .NET application
  • Conclusion

Tools

  • Visual Studio 2019

JWT Definition

JSON Web Token (JWT) is an open standard that enables safe transmission of claims in space-constrained environments. This allows JWT to be transmitted through URLs, POST methods, or HTTP headers. In this article, we will send a JWT through a POST method.

JWT is a standard because the JSON Object Signing and Encryption group (JOSE), which was formed in 2011, “standardize the mechanism for integrity protection (signature and MAC) and encryption as well as the format for keys and algorithm identifiers to support interoperability of security services for protocols that use JSON”. The JOSE group established norms to securely transmit claims in JSON format as digitally signed tokens.

Claims are assertion statements about an object. In this article, I will show that a claim called “name” asserts that a login user is “Sakhile-admin”. In this case, our object is the user. There are three types of claims: registered, public, and private claims. In the to-do application, we used registered claims and two private claims that were defined by me.

JWT Structure

JWTs have three different parts, namely, the header, payload, and signature. The header usually contains two claims, the algorithm used to sign the token and the type of the token. However, only the algorithm claim is mandatory. There are different types of algorithms that are used to sign the token, such as, RS256, RS256, etc. If no algorithm is used the assertion of the claim is none and this JWT is unsecure. I will show how to create an unsecured JWT. An unsecured JWT has no signature and some JWT validation libraries can interpret these tokens as valid tokens, which may allow someone to modify the token payload. The TokenValidationParameters, an option of the JwtBearerOptions, does not consider unsigned tokens as valid. I will show that a .NET application considers unsigned tokens as invalid and returns a 401 unauthorized status code.

The payload contains claims about an object. This is where user data is added. One of the registered claims used in the to-do application is expiration time, which shows the exact moment from which the token is considered invalid. The JWT was set to expire in 3 hours. A short-lived token helps to mitigate Cross-Site Request Forgery (CSRF) attacks. However, since the front end of the to-do application uses localStorage to store the token and not Cookies, CSRF is not possible.

The signature is formed using the Base64Url-encoded header and payload, a secrete string that is given by a user and the algorithm described in the header. The signature verifies that the data contained in the token has not been changed when the token was sent from the issuer to the audience and from the audience to the issuer.

Generate a JWT in a .NET application

To generate a JWT, three files must be edited in our simple to-do list application. The startup.cs file, the appsettings.json file, and the AthenticationController.cs file.

The appsettings.cs file contains the JWT credentials. The credentials are stored here for security reasons. The secret string is used to sign the JWT.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "SQLConnection": "Server=.;Database=ToDoDB;Trusted_Connection=True;Integrated Security=true"
  },
  "JWT": {
    "ValidAudience": "http: //localhost:4200",
    "ValidIssuer": "http://localhost:24288",
    "Secret": "MySecretStringMuuustBeVeeeeeeeeeeryLooooooooOng"
  }
}

The startup.cs file. In the ConfigureServices method, the AddAuthentication service is added to validate the token. In the Configure method the app.UseAuthentication() is added to use the authentication in this application.

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ToDoAPI.Authentication;
using ToDoAPI.Models;

namespace ToDoAPI
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {

            services.AddControllers();
            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new OpenApiInfo { Title = "ToDoAPI", Version = "v1" });
            });

            services.AddDbContext<ApplicationDbContext>(options =>
           options.UseSqlServer(Configuration.GetConnectionString("SQLConnection")));

            services.AddIdentity<ApplicationUser, IdentityRole>()
                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddDefaultTokenProviders();

            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,
                    ValidateLifetime = true,
                    ValidAudience = Configuration["JWT:ValidAudience"],
                    ValidIssuer = Configuration["JWT:ValidIssuer"],
                    // set clockskew to zero so tokens expire exactly at token expiration time (instead of 5 minutes later)
                    ClockSkew = TimeSpan.Zero,
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JWT:Secret"]))
                };
            });
            services.AddCors();
        }
        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            app.UseCors(options =>
            options.WithOrigins("http://localhost:4200")
            .AllowAnyMethod()
            .AllowAnyHeader());
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseSwagger();
                app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "ToDoAPI v1"));
            }
            app.UseAuthentication();
            app.UseRouting();
            app.UseAuthorization();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }
}

To generate a secure JWT the Login method must be written as shown below between line 33 and line 70, in the athenticationController.cs file. The code in line 44 and line 50 shows that name and role private claims were defined and used in the JWT. Line 60 shows an authSigningKey secret and SecurityAlgorithms.HmacSha256 algorithm was used to sign the JWT. This produced a signed JWT.

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using ToDoAPI.Authentication;
using ToDoAPI.Models;

namespace ToDoAPI.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class AuthenticationController : ControllerBase
    {
        private readonly UserManager<ApplicationUser> userManager;
        private readonly RoleManager<IdentityRole> roleManager;
        private readonly IConfiguration _configuration;

        public AuthenticationController(UserManager<ApplicationUser> userManager, RoleManager<IdentityRole> roleManager, IConfiguration configuration)
        {
            this.userManager = userManager;
            this.roleManager = roleManager;
            _configuration = configuration;
        }

        [HttpPost]
        [Route("login")]
        public async Task<IActionResult> Login([FromBody] LoginModel model)
        {
            var user = await userManager.FindByNameAsync(model.Username);

            if (user != null && await userManager.CheckPasswordAsync(user, model.Password))
            {
                var userRoles = await userManager.GetRolesAsync(user);
                var authClaims = new List<Claim>
                {
                    new Claim("name", user.UserName),
                    new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
                };

                foreach (var userRole in userRoles)
                {
                    authClaims.Add(new Claim("role", userRole));
                }

                var authSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JWT:Secret"]));

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

                return Ok(new
                {
                    token = new JwtSecurityTokenHandler().WriteToken(token),
                    expiration = token.ValidTo
                });
            }
            return Unauthorized();
        }

        [HttpPost]
        [Route("register")]
        public async Task<IActionResult> Register([FromBody] RegisterModel model)
        {
            var userExists = await userManager.FindByNameAsync(model.Username);

            if (userExists != null)
            {
                return StatusCode(StatusCodes.Status500InternalServerError, new Response { Status = "Error", Message = "User already exists!" });
            };

            ApplicationUser user = new ApplicationUser()
            {
                Email = model.Email,
                SecurityStamp = Guid.NewGuid().ToString(),
                UserName = model.Username
            };

            var result = await userManager.CreateAsync(user, model.Password);

            if (!result.Succeeded)
            {
                return StatusCode(StatusCodes.Status500InternalServerError, new Response { Status = "Error", Message = "User creation failed! Please check user details and try again." });
            }

            return Ok(new Response { Status = "Success", Message = "User created successfully" });
        }

        [HttpPost]
        [Route("register-admin")]
        public async Task<IActionResult> RegisterAdmin([FromBody] RegisterModel model)
        {
            var userExists = await userManager.FindByNameAsync(model.Username);

            if (userExists != null)
            {
                return StatusCode(StatusCodes.Status500InternalServerError, new Response { Status = "Error", Message = "User already exists!" });
            };

            ApplicationUser user = new ApplicationUser()
            {
                Email = model.Email,
                SecurityStamp = Guid.NewGuid().ToString(),
                UserName = model.Username
            };
            var result = await userManager.CreateAsync(user, model.Password);
            if (!result.Succeeded)
            {
                return StatusCode(StatusCodes.Status500InternalServerError, new Response { Status = "Error", Message = "User creation failed! Please check user details and try again." });
            }
            if (!await roleManager.RoleExistsAsync(UserRoles.Admin))
            {
                await roleManager.CreateAsync(new IdentityRole(UserRoles.Admin));
            }
            if (!await roleManager.RoleExistsAsync(UserRoles.User))
            {
                await roleManager.CreateAsync(new IdentityRole(UserRoles.User));
            }
            if (await roleManager.RoleExistsAsync(UserRoles.Admin))
            {
                await userManager.AddToRoleAsync(user, UserRoles.Admin);
            }
            return Ok(new Response { Status = "Success", Message = "User created successfully" });

        }
    }
}

A signed JWT that is generated is shown below. Notice that the token has three encoded parts. The “alg” claim is HS256 that shows the algorithm used to sign this JWT.

To generate an unsecured JWT, the Login method must be changed as shown below.

[HttpPost]
[Route("login")]
public async Task < IActionResult > Login([FromBody] LoginModel model) {
    var user = await userManager.FindByNameAsync(model.Username);
    if (user != null && await userManager.CheckPasswordAsync(user, model.Password)) {
        var userRoles = await userManager.GetRolesAsync(user);
        var authClaims = new List < Claim > {
            new Claim("name", user.UserName),
            new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
        };
        foreach(var userRole in userRoles) {
            authClaims.Add(new Claim("role", userRole));
        }
        var authSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JWT:Secret"]));
        var token = new JwtSecurityToken(issuer: _configuration["JWT:ValidIssuer"], audience: _configuration["JWT:ValidAudience"], expires: DateTime.Now.AddMinutes(120), claims: authClaims);
        return Ok(new {
            token = new JwtSecurityTokenHandler().WriteToken(token),
                expiration = token.ValidTo
        });
    }
    return Unauthorized();
}

An unsigned JWT that is generated is shown below. Notice that the token has only two encoded parts with a dot at the end of the encoded token. Also, the “alg” claim is none.

The unsigned JWT is considered as invalid and does not allow the user to access the to-do item endpoints.

Conclusion

In this article, I described a token and its structure. I showed how to generate a secure and unsecured JWT in a .NET application. I described some having an unsecured token can cause the application to be venerable to Cross-Site Request Forgery (CSRF) attacks. JWT is a standard that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. A signed JWT can be used for authentication and authorization in the application. I have shown that a JWT authentication can be easily implemented in a .NET application and that only signed JWT can be used to access the endpoints of an application. You can find the source code in my GitHub repository.


Similar Articles