With the surge in APIs and their consumption globally, API security is extremely important these days. JWT authentication is a standard way for protecting APIs - it's adept at verifying the data that's transmitted over the wire between APIs and the clients that consume the APIs. You can even safely pass claims between the communicating parties as well.

JSON Web Tokens (commonly known as JWT) is an open standard to pass data between client and server, and enables you to transmit data back and forth between the server and the consumers in a secure manner. This article talks about how you can take advantage of JWTs to protect APIs.

You can download the source code for this article posted in GitHub: https://github.com/joydipkanjilal/jwt-aspnetcore

Prerequisites

If you're to work with the code examples discussed in this article, you should have the following installed in your system:

  • Visual Studio 2019 (an earlier version will also work but Visual Studio 2019 is preferred)
  • .NET 5.0
  • ASP.NET 5.0 Runtime

You can download Visual Studio 2019 from here: https://visualstudio.microsoft.com/downloads/. You can download .NET 5.0 and ASP.NET 5.0 runtime from here: https://dotnet.microsoft.com/download/dotnet/5.0.

What Are JSON Web Tokens (JWT)?

JSON Web Token is an open standard (RFC 7519) that defines a safe, compact, and self-contained, secured way for transmission of information between a sender and a receiver through a URL, a POST parameter, or inside the HTTP Header. It should be noted that the information to be transmitted securely between two parties is represented in JSON format and it is cryptographically signed to verify its authenticity. JWT is typically used for implementing authentication and authorization in Web applications. Because JWT is a standard, all JWTs are tokens but the reverse is not true. You can work with JSON Web Tokens in .NET, Python, Node.js, Java, PHP, Ruby, Go, JavaScript, etc.

Figure 1 illustrates how a typical JWT authentication works.

Figure 1: JWT authentication at work
Figure 1: JWT authentication at work

JWT is represented as a combination of three base64url encoded parts concatenated with period ('.') characters and comprises the following three sections:

  • Header
  • Payload
  • Signature

Header Section

This section provides metadata about the type of data and the algorithm to be used to encrypt the data that is to be transferred. The JWT header comprises three sections - these include: the metadata for the token, the type of signature, and the encryption algorithm. It comprises two properties, i.e., “alg” and “typ”. Although the former relates to the cryptography algorithm used, i.e., HS256 in this case, the latter is used to specify the type of the token, i.e., JWT in this case.

{
  "typ": "JWT",
  "alg": "HS256"
}

Payload

The payload represents the actual information in JSON format that is to be transmitted over the wire. The code snippet given below illustrates a simple payload.

{
  "sub": "1234567890",
  "name": "Joydip Kanjilal",
  "admin": true,
  "jti": "cdafc246-109d-4ac9-9aa1-eb689fad9357",
  "iat": 1611497332,
  "exp": 1611500932
}

The payload typically contains claims, the identity information of the user, the allowed permissions, etc. You can use claims to transmit additional information. These are also called JWT claims and are of two types: Reserved and Custom. Here's a list of the reserved claims:

  • iss: This represents the issuer of the token.
  • sub: This is the subject of the token.
  • aud: This represents the audience of the token.
  • exp: This is used to define token expiration.
  • nbf: This is used to specify the time before which the token must not be processed.
  • iat: This represents the time when the token was issued.
  • jti: This represents a unique identifier for the token.

You can also use custom claims, which can be added to the token using a rule.

Signature

The signature adheres to the JSON Web Signature (JWS) specification and is used to verify the integrity of the data transferred over the wire. It comprises a hash of the header, the payload, and the secret, and is used to ensure that the message was not changed while being transmitted. The final signed token is created by adhering to the JSON Web Signature (JWS) specification. The encoded JWT header and as well as the encoded JWT payload is combined and then it's signed using a strong encryption algorithm such as HMAC SHA 256.

Getting Started

First, create a new ASP.NET Core MVC 5 project in Visual Studio 2019. You can create a project in Visual Studio 2019 in several ways. When you launch Visual Studio 2019, you'll see the Start window. You can choose “Continue without code” to launch the main screen of the Visual Studio 2019 IDE. In the menu of the main screen, you can select File > New > Project to launch the screen shown in Figure 2.

Figure 2: Select the project template and specify authentication and the target framework.
Figure 2: Select the project template and specify authentication and the target framework.

Next, follow the sequence of steps in Visual Studio 2019 to create a new ASP.NET Core MVC 5 project. I'll use this project throughout this article.

Install NuGet Package

So far so good. The next step is to install the necessary NuGet Package(s). A NuGet package is represented as a file that has a .nupkg extension and is comprised of compiled code (also called DLLs), other related files, and a manifest that provides information related to the package such as version number, etc.

To install the required packages into your project, execute the following commands at the NuGet Package Manager Console.

  • dotnet add package Microsoft.AspNetCore.Authentication
  • dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

Implementing JWT in ASP.NET Core 5 MVC

In this section, I'll examine how you can implement JWT authentication in ASP.NET Core MVC 5 application. In this example, you'll be using the following classes and interfaces:

  • HomeController: This is the controller class that contains all the action methods.
  • ITokenService: This is an interface that contains the declaration of two methods, i.e., BuildToken and IsTokenValid. The former is used to build the token and the latter is used to check whether a given token is valid.
  • TokenService: This class extends the ITokenService interface and implements its methods.
  • IUserRepository: This interface contains the declaration of the GetUser method that's used to get an UserDTO instance based on the username from an instance of UserModel class.
  • UserRepository: The UserRepository class extends the IUserRepository interface and implements the GetUser method. It also contains the sample data used by the application as a list of the UserDTO class.

Creating the Model Classes

There are two entities you can use in this application: the UserModel and the UserDTO classes. The following code snippet shows how the UserModel class looks:

public class UserModel
{
    [Required]
    public string UserName { get; set; }
    
    [Required]
    public string Password { get; set; }
}

Next, create a class named UserDTO with the following content:

public class UserDTO
{
    public string UserName { get; set; }
    public string Password { get; set; }
    public string Role { get; set; }
}

The UserDTO represents the user data transfer object and contains three string properties: UserName, Password, and Role. You'll use this class at several places in your application.

Configuring JWT in the AppSettings File

Create a section in the appsettings.cs file called Jwt with the following content inside:

"Jwt": {
    "Key": "This is where you should specify your secret key, which is used to sign and verify Jwt tokens.",
    "Issuer": "www.joydipkanjilal.net"
}

Replace the text mentioned in “Key” above with the actual key you would like to use as a secret. After you've added the new section your appsettings.cs file would look like this:

{
    "Logging": {
        "LogLevel": {
            "Default": "Information",
            "Microsoft": "Warning",
            "Microsoft.Hosting.Lifetime": "Information"
        }
    },
    "Jwt": {
        "Key": "ThisismySecretKey",
        "Issuer": www.joydipkanjilal.net
        "Audience": "http://localhost:36145/"
    },
    "AllowedHosts": "*"
}

Configure Authentication with Bearer and JWT

In the ConfigureServices method of the Startup class, I should mention that you'll use the AddAuthentication feature as well as JwtBearer using the AddJwtBearer method, as shown in the code snippet below.

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>
{
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,
        ValidIssuer = Configuration["Jwt:Issuer"],
        ValidAudience = Configuration["Jwt:Issuer"],
        IssuerSigningKey = new
        SymmetricSecurityKey
        (Encoding.UTF8.GetBytes
        (Configuration["Jwt:Key"]))
    };
});

The following code can be used in the ConfigureServices method of the Startup class to add a transient service of type IUserRepository and IITokenService respectively.

services.AddTransient<IUserRepository, UserRepository>();
services.AddTransient<ITokenService, TokenService>();

Once these instances have been added, you can take advantage of dependency injection in the constructor of the HomeController class to retrieve these instances from the container.

The complete code of the ConfigureServices method is given in Listing 1.

Listing 1: The ConfigureServices Method

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.AddTransient<IUserRepository, UserRepository>();
    services.AddTransient<ITokenService, TokenService>();
    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>
    {        
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = Configuration["Jwt:Issuer"],
            ValidAudience = Configuration["Jwt:Issuer"],
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"]))
        };
    });
}

You'll take advantage of session state in this example to store the generated token. You should make a call to the UseSession extension method in the Configure method of the Startup class to enable session state for your application. The code snippet below shows how you can retrieve the generated token from the session and then add it as a bearer token in the request header.

app.Use(async (context, next) =>
{
    var token = context.Session.GetString("Token");
    if (!string.IsNullOrEmpty(token))
    {
        context.Request.Headers.Add("Authorization", "Bearer " + token);
    }
    await next();
});

Listing 2 illustrates the complete source code of the Configure method - note how you can specify that session state, authentication, and routing that will be used.

Listing 2: The Configure Method

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }          
    else            
    {
        app.UseExceptionHandler("/Home/Error");
    }
    app.UseSession();
    app.Use(async (context, next) =>    
    {
        var token = context.Session.GetString("Token"); 
        if (!string.IsNullOrEmpty(token))  
        {
            context.Request.Headers.Add("Authorization", "Bearer " + token);
        }
        await next();
    });
    app.UseStaticFiles();
    app.UseRouting();
    app.UseAuthentication();
    app.UseEndpoints(endpoints =>  
    {
        endpoints.MapControllerRoute(name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); 
    });
}

Create the Views

Replace the source code of the Index.cshtml with the code in Listing 3.

Listing 3: The Login View

@model CodeMagazineMVCDemo.Models.UserModel
@{
    ViewData["Title"] = "Index";
}

<hr />
<div class="row">  
    <div class="col-md-4">
        <form asp-action="Login"> 
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="UserName" class="control- label"></label> 
                <input asp-for="UserName" class="form-control" /> 
                <span asp-validation-for= "UserName" class="text-danger"></span>        
            </div>
            <div class="form-group">
                <label asp-for="Password" class="control-label"></label>    
                <input asp-for="Password" type="password" class="form-control"/>    
                <span asp-validation-for="Password" class="text-danger"></span> 
            </div>
            <div class="form-group">                
                <input type="submit" value="Login" class="btn btn-primary" />
            </div>
        </form>
    </div>

</div>

@section Scripts {@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}}

Next, create a view named MainWindow.cshtml and write the following code in there:

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>CodeMag JWT Demo</title>
</head>

<body>
  @{
      ViewBag.Title = "Demonstrating JWTs in ASP.NET Core MVC 5";
  }

  <p>You're logged in as:&nbsp;@User.Identity.Name</p>
  <p>@ViewBag.Message</p>
</body>
</html>

Lastly, create a view named Error.cshtml and write the following code in there:

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Error...</title>
</head>

<body>
  <p>@ViewBag.Message</p>
</body>
</html>

Create the UserRepository Class

A repository class is an implementation of the Repository design pattern and is one that manages data access. The application takes advantage of the repository instance to perform CRUD operations against the database. In this example, the HomeController interacts with the UserRepository to retrieve a user based on the username and password.

Create a file named IUserRepository.cs with the following content inside:

public interface IUserRepository
{
    UserDTO GetUser(UserModel userMode);
}

The UserRepository class extends the IUserRepository interface and implements the GetUser method, as can be seen in Listing 4. It also builds a list of UserDTO objects. Note that the password here has been hardcoded for simplicity.

Listing 4: The UserRepository Class

public class UserRepository : IUserRepository    
{
    private readonly List<UserDTO> users = new List<UserDTO>();

    public UserRepository()
    {
        users.Add(new UserDTO 
        {
            UserName = "joydipkanjilal",    
            Password = "joydip123", 
            Role = "manager" 
        });    
        users.Add(new UserDTO 
        { 
            UserName = "michaelsanders",    
            Password = "michael321", 
            Role = "developer" 
        });
        users.Add(new UserDTO 
        { 
            UserName = "stephensmith",
            Password = "stephen123",
            Role = "tester" 
        });
        users.Add(new UserDTO 
        { 
            UserName = "rodpaddock",
            Password = "rod123", 
            Role = "admin" 
        });
        users.Add(new UserDTO
        { 
            UserName = "rexwills",
            Password = "rex321", 
            Role = "admin" 
        });
    }
    public UserDTO GetUser(UserModel userModel)
    {
        return users.Where(x => x.UserName.ToLower() == userModel.UserName.ToLower()               
            && x.Password == userModel.Password).FirstOrDefault(); 
    }
}

Create the TokenService Class

Create an interface called ITokenService with the following content: cs

public interface ITokenService
{
    string BuildToken(string key, string issuer, UserDTO user);
    bool ValidateToken(string key, string issuer, string audience, string token);
}

The TokenService class extends the ITokenService interface and implements its methods as shown in Listing 5.

Listing 5: The TokenService Class

public class TokenService : ITokenService
{
    private const double EXPIRY_DURATION_MINUTES = 30;

    public string BuildToken(string key, string issuer, UserDTO user)
    {
        var claims = new[] {    
            new Claim(ClaimTypes.Name, user.UserName),
            new Claim(ClaimTypes.Role, user.Role),
            new Claim(ClaimTypes.NameIdentifier,
            Guid.NewGuid().ToString())
        };

        var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key));        
        var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256Signature);           
        var tokenDescriptor = new JwtSecurityToken(issuer, issuer, claims, 
            expires: DateTime.Now.AddMinutes(EXPIRY_DURATION_MINUTES), signingCredentials: credentials);        
        return new JwtSecurityTokenHandler().WriteToken(tokenDescriptor);  
    }
    public bool IsTokenValid(string key, string issuer, string token)
    {
        var mySecret = Encoding.UTF8.GetBytes(key);           
        var mySecurityKey = new SymmetricSecurityKey(mySecret);
        var tokenHandler = new JwtSecurityTokenHandler(); 
        try 
        {
            tokenHandler.ValidateToken(token, 
            new TokenValidationParameters   
            {
                ValidateIssuerSigningKey = true,
                ValidateIssuer = true, 
                ValidateAudience = true,    
                ValidIssuer = issuer,
                ValidAudience = issuer, 
                IssuerSigningKey = mySecurityKey,
            }, out SecurityToken validatedToken);            
        }
        catch
        {
            return false;
        }
        return true;    
    }
}

Create the HomeController Class

Finally, we come to the controller class. In the HomeController class, you'll take advantage of dependency injection to be able to use instances of the Configuration, TokenService, and UserRepository classes. Create the following read-only instances for each of the three interfaces as shown below:

private readonly IConfiguration_config;
private readonly ITokenService_tokenService;
private readonly IUserRepository_userRepository;

The ITokenService and IUserRepository instances are created in the ConfigureServices method of the Startup class, as shown in the code snippet given below:

services.AddScoped<ITokenService, TokenService>();
services.AddScoped<IUserRepository, UserRepository>();

The IConfiguration interface is declared in the Startup class as a read-only property, as shown below:

public IConfiguration Configuration { get; }

Here's how constructor injection is used in the HomeController class for each of the instances discussed earlier.

public HomeController (IConfiguration config, ITokenService tokenService, IUserRepository userRepository)
{
    _tokenService = tokenService;
    _userRepository = userRepository;
    _config = config;
}

The complete source code of the HomeController class is given in Listing 6.

Listing 6: The HomeController Class

public class HomeController : Controller    
{
    private readonly IConfiguration _config;        
    private readonly IUserRepository _userRepository;       
    private readonly ITokenService _tokenService;  
    private string generatedToken = null;  
    
    public HomeController (IConfiguration config, ITokenService tokenService, IUserRepository userRepository)
    {
        _config = config;
        _tokenService = tokenService;
        _userRepository = userRepository;
    }
    
    public IActionResult Index()
    {
        return View(); 
    }

    [AllowAnonymous]    
    [Route("login")]    
    [HttpPost] 
    public IActionResult Login(UserModel userModel)
    {
        if (string.IsNullOrEmpty(userModel.UserName) || string.IsNullOrEmpty(userModel.Password))  
        {
            return (RedirectToAction("Error"));
        }
        IActionResult response = Unauthorized();        
        var validUser = GetUser(userModel);
    
        if (validUser != null)  
        {
            generatedToken = _tokenService.BuildToken(_config["Jwt:Key"].ToString(), _config["Jwt:Issuer"].ToString(), validUser);
            if (generatedToken != null) 
            {
                HttpContext.Session.SetString("Token", generatedToken);
                return RedirectToAction("MainWindow"); 
            }
            else
            {
                return (RedirectToAction("Error")); 
            }
        }
        else
        {
            return (RedirectToAction("Error"));
        }
    }

    private UserDTO GetUser(UserModel userModel)        
    {
        // Write your code here to authenticate the user     
        return _userRepository.GetUser(userModel);
    }

    [Authorize]
    [Route("mainwindow")]
    [HttpGet]
    public IActionResult MainWindow() 
    {
        string token = HttpContext.Session.GetString("Token");
        if (token == null)
        {
            return (RedirectToAction("Index"));
        }
        if (!_tokenService.IsTokenValid(_config["Jwt:Key"].ToString(), _config["Jwt:Issuer"].ToString(), token)) 
        {
            return (RedirectToAction("Index")); 
        }
        ViewBag.Message = BuildMessage(token, 50);      
        return View(); 
    }

    public IActionResult Error()
    {
        ViewBag.Message = "An error occured...";
        return View();  
    }

    private string BuildMessage(string stringToSplit, int chunkSize)
    {
        var data = Enumerable.Range(0, stringToSplit.Length / chunkSize).Select(i => stringToSplit.Substring (i * chunkSize, chunkSize));
        string result = "The generated token is:";
        foreach (string str in data)            
        {
            result += Environment.NewLine + str;
        }
        return result;      
        }
    }    

Refer to the code listing of the HomeController class in Listing 6. The BuildMessage method is used to split the generated token in multiple lines. The GetUser method of the HomeController class calls the GetUser method of the UserRepository class to retrieve an instance of the UserDTO class based on the user credentials entered in the Login screen shown in Figure 3.

Figure 3: The Login Screen as seen in the Web Browser
Figure 3: The Login Screen as seen in the Web Browser

Run the Application

Now run the application by pressing on Ctrl + F5 or just F5. Figure 3 shows how the output looks in the Web browser.

Once you specify the user's credentials and click on Login, you'll be redirected to another Web page that shows the name of the logged in user and the generated token, as shown in Figure 4.

Figure 4: Displaying the Generated Token
Figure 4: Displaying the Generated Token

Summary

JSON Web Token (JWT) is an open standard (RFC 7519) that defines how you can securely transfer information between two parties. You must use SSL/TLS together with JSON Web Tokens (JWT) to combat man-in-the-middle attacks. In most cases, this should be sufficient to encrypt the payload before it is transferred over the wire. You can take advantage of JWT as an additional layer of security as well.