paint-brush
Why do you need JWT in your ASP.NET Core project?by@igorlopushko
6,777 reads
6,777 reads

Why do you need JWT in your ASP.NET Core project?

by Igor LopushkoFebruary 13th, 2024
Read on Terminal Reader
Read this story w/o Javascript

Too Long; Didn't Read

The story is about how to create a Web API to generate JWT and then use it for authorization in the CRUD Web API.
featured image - Why do you need JWT in your ASP.NET Core project?
Igor Lopushko HackerNoon profile picture

JWT stands for JSON Web Token, and it is an authorization mechanism, not authentication. So let’s figure out what the difference between those two.


Authentication is the mechanism that allows verifying that the user is exactly the one he claims to be. It is a login process where a user provides a username and password, and the system verifies them. So authentication answers the question: who is the user?


Authorization is the mechanism that allows verification of which access rights the user has to a certain resource. It is a process of granting users some roles and a set of permissions a particular role has. So, authorization answers that question: what rights does the user have in the system?

Authentication vs Authorization


It is important to understand that Authentication always comes first and Authorization is second. In other words, you can’t get permission before you verify your identity. But what are the most popular authorization methods? There are two main approaches to handling authorization for the web application.

Sessions

A traditional approach on the web for authorization users is a cookie-based server-side session. The process starts when a user logs in and a server authenticates him. After that, the server creates a session with a Session ID and stores it somewhere in the server’s memory. The server sends back Session ID to the client and the client stores the Session ID in cookies. For every request, the client sends a Session ID as a part of the request, and the server verifies the Session ID in its memory and the user’s permissions related to this session.

Session-based authorization

Tokens

Another popular approach is using tokens for authorization. The process starts similarly when a user enters login, and passwords and a client sends a login request to a server. Instead of creating a session, the server generates a token signed with the secret token. Then, the server sends the token back to the client, and the client has to store it in a local storage. Similar to the session-based approach, the client has to send a token to the server for every request. However, the server does not store any additional information about the user session. The server has to validate that the token has not changed since it was created and signed with the secret key.

Token-based authorization

Session vs Token

Session-based authorization approach can be vulnerable to an attack known as Cross-Site Request Forgery (CSRF). It is a kind of attack when the attacker points to a site they are logged into to perform actions they didn’t intend to, like submitting a payment or changing a password.


Another thing is that when using a session-based authorization approach creates a stateful session between a client and server. The problem is if a client wants to access different servers in the scope of the same application, those servers have to share a session state. In another case, the client will need to be authorized on each server since the session is going to be different.

Session-based authorization state-sharing


On the other hand, the token-based authorization approach does not require storing session data on the server side and may simplify authorization between multiple servers.


However, tokens can still be stolen by an attacker and it also can be difficult to invalidate tokens. We will see the details and how to handle invalidation further in this article.

JWT

JSON Web Token (JWT) is an open standard that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.

JWT structure

JSON Web Tokens consist of three parts separated by dots .


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

The header usually consists of two parts: the type of token, and the signing algorithm being used.


  • Payload
{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

The payload contains the claims, which are statements about the user. The payload is then Base64Url encoded to form the second part of the JSON Web Token. You can find a description of standard fields that are used as claims here.


  • Signature
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

To create the signature part, you have to take the encoded header, the encoded payload, a secret, and the algorithm specified in the header and sign that.


The token typically looks like the following:

xxxxx.yyyyy.zzzzz


You can navigate to jwt.io and debug a sample token or your own. Just paste your token into the Encoded field and select the Algorithm of the token signature.

jwt.io debugger

.NET project

Now that we have theoretical knowledge of how JWT works, we can apply it to the real-life project. Let’s assume we have a simple API that represents CRUD operations for the coffee entity. We are going to create an ASP.NET Core API project that represents Coffee API. After that, we will create another ASP.NET Core API project that would represent an Identity API that could generate JWT. In real life, you would probably use Identity Server or Okta, or Auth0 for Authentication/Authorization purposes. However, we would create our own Identity API to demonstrate how to generate JWT. When Identity API is done, we can call its controller and generate JWT based on the user’s data. Also, we can protect the Coffee API with an authorization configuration that requires passing JWT with each request.

.NET project landscape

Coffee API

First, we are going to create a simple ASP.NET Core API project that represents Coffee API. Here is the structure of this project:

Coffee API - Project structure


Let’s start with the Coffee.cs in the Model folder. It is a simple entity with an Id and a Name properties.

namespace Hackernoon.Coffee.API.Model;

public class Coffee
{
    public int Id { get; set; }
    public string Name { get; set; }
}


We need to store our entities while working with the API. So, let’s introduce a simple in-memory storage. It is located in the Storage.cs file in the Data folder.

namespace Hackernoon.Coffee.API.Data;

public static class Storage
{
    private static readonly List<Model.Coffee> Data = new();

    public static List<Model.Coffee> GetAll()
    {
        return Data;
    }
    
    public static bool Create(Model.Coffee model)
    {
        if (Data.Any(c => c.Id == model.Id || c.Name == model.Name))
            return false;
        
        Data.Add(new Model.Coffee
        {
            Id = model.Id, 
            Name = model.Name
        });

        return true;
    }
    
    public static bool Delete(int id)
    {
        if (Data.All(c => c.Id != id))
            return false;
        
        Data.Remove(Storage.Data.First(c => c.Id == id));
        return true;
    }
    
    public static bool Update(Model.Coffee model)
    {
        if (Data.All(c => c.Id != model.Id))
            return false;
        
        Data.First(c => c.Id == model.Id).Name = model.Name;
        return true;
    }
}


We need a class that would represent requests to the Coffee API. So, let’s create CoffeeRequest.cs in the Contracts folder.

namespace Hackernoon.Coffee.API.Contracts;

public class CoffeeRequest
{
    public int Id { get; set; }
    public string Name { get; set; }
}


When it is done, we can implement CoffeeController.cs in the Controller folder that represents CRUD operations for the coffee entity.

using Hackernoon.Coffee.API.Contracts;
using Hackernoon.Coffee.API.Data;
using Microsoft.AspNetCore.Mvc;

namespace Hackernoon.Coffee.API.Controllers;

[Route("coffee")]
[ApiController]
public class CoffeeController : ControllerBase
{
    [HttpGet]
    public IList<Model.Coffee> GetAll()
    {
        return Storage.GetAll();
    }
    
    [HttpPost]
    public IActionResult Create([FromBody]CoffeeRequest request)
    {
        var model = new Model.Coffee
        {
            Id = request.Id,
            Name = request.Name
        };

        if (!Storage.Create(model))
            return new BadRequestResult();

        return new OkResult();
    }

    [HttpDelete]
    public IActionResult Delete(int id)
    {
        if (!Storage.Delete(id))
            return new BadRequestResult();

        return new OkResult();
    }

    [HttpPut]
    public IActionResult Update([FromBody] CoffeeRequest request)
    {
        var model = new Model.Coffee()
        {
            Id = request.Id,
            Name = request.Name
        };
        
        if (!Storage.Update(model))
            return new BadRequestResult();

        return new OkResult();
    }
}


Coffee API is done, and we can run the project and see Swagger UI as follows:

Coffee API - Swagger UI

Identity API

Let’s create another ASP.NET Core API project that represents Identity API. Here is the structure of this project:

Identity API - Project structure

Let’s start with the TokenGenerationRequest.cs in Contracts folder, which represents the request for the generation of a new JWT with Email and Password properties.

namespace Hackernoon.Identity.API.Contracts;

public class TokenGenerationRequest
{
    public string Email { get; set; }
    public string Password { get; set; }
}


We need to implement only TokenController.cs that represents the logic of generation JWT. But before we do that Microsoft.AspNetCore.Authentication.JwtBearer NuGet package needs to be installed.

using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Hackernoon.Identity.API.Contracts;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;

namespace Hackernoon.Identity.API.Controllers;

[Route("token")]
public class TokenController : ControllerBase
{
    private const string SecretKey = "VerySecretAndLongKey-NeedMoreSymbolsHere-123";
    private const string Issuer = "IdentityServerIssuer";
    private const string Audience = "IdentityServerClient";
    private static readonly TimeSpan Lifetime = TimeSpan.FromMinutes(20); 
        
    [HttpPost]
    public string Create([FromBody]TokenGenerationRequest request)
    {
        var claims = new List<Claim> {new Claim(ClaimTypes.Email, request.Email) };

        var jwt = new JwtSecurityToken(
            issuer: Issuer,
            audience: Audience,
            claims: claims,
            expires: DateTime.UtcNow.Add(Lifetime),
            signingCredentials: CreateSigningCredentials());
            
        return new JwtSecurityTokenHandler().WriteToken(jwt);
    }

    private static SigningCredentials CreateSigningCredentials()
    {
        return new SigningCredentials(
            new SymmetricSecurityKey(Encoding.UTF8.GetBytes(SecretKey)), 
            SecurityAlgorithms.HmacSha256);
    }
}


Note that sensitive const such as SecretKey, Issuer, and Audience have to be put somewhere in the configuration. They are hardcoded just for simplifying this test project. The Lifetime field is set to 20 minutes, which means that the token will be valid for that time. You also might configure this parameter.


Now we can run the project and see Swagger UI as follows:

Identity API - Swagger UI


Let’s make a call to the /token endpoint and generate a new JWT. Try the following payload:

{
  "email": "[email protected]",
  "password": "password"
}


Identity API will generate the corresponding JWT:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9lbWFpbGFkZHJlc3MiOiJqb2huLmRvZUBnbWFpbC5jb20iLCJJc0dvdXJtZXQiOiJmYWxzZSIsImV4cCI6MTcwNzc4Mzk4MCwiaXNzIjoiSWRlbnRpdHlTZXJ2ZXJJc3N1ZXIiLCJhdWQiOiJJZGVudGl0eVNlcnZlckNsaWVudCJ9.4odXsbWak1C0uK3Ux-n7f58icYQQwlHjM54OjgMCVPM

Enabling Authorization in Coffee API

Now, when Identity API is ready and provides us with tokens, we can guard Coffee API with authorization. Again Microsoft.AspNetCore.Authentication.JwtBearer NuGet package needs to be installed.


We need to register the required services by authentication services. Add the following code to the Program.cs file right after creating a builder.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidIssuer = "IdentityServerIssuer",
            ValidateAudience = true,
            ValidAudience = "IdentityServerClient",
            ValidateLifetime = true,
            IssuerSigningKey = 
                new SymmetricSecurityKey(
                    Encoding.UTF8.GetBytes("VerySecretAndLongKey-NeedMoreSymbolsHere-123")),
            ValidateIssuerSigningKey = true,
        };
    });

builder.Services.AddAuthorization();


It is important to remember that order in middleware is important. We enable authentication by calling AddAuthentication() method and specifying JwtBearerDefaults.AuthenticationScheme as an authentication schema. It is a constant that contains a Bearer value.

namespace Microsoft.AspNetCore.Authentication.JwtBearer
{
  /// <summary>Default values used by bearer authentication.</summary>
  public static class JwtBearerDefaults
  {
    /// <summary>
    /// Default value for AuthenticationScheme property in the JwtBearerAuthenticationOptions
    /// </summary>
    public const string AuthenticationScheme = "Bearer";
  }
}


We need to specify TokenValidationParameters that describes which parameters of JWT will be validated during the authorization. We also specify IssuerSigningKey similar to the signingCredentials in Identity API to verify the JWT signature. Check more details about TokenValidationParameters here.


The next piece of code adds middleware to the builder that enables authentication and authorization capabilities. It should be added between the UseHttpsRedirection() and MapControllers() methods.

app.UseHttpsRedirection();

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

app.MapControllers();


Now, we can use the Authorize attribute over the controller or its actions. By applying this code, now all the actions in CoffeeController are protected with an authorization mechanism, and JWT has to be sent as a part of the request.

[Route("coffee")]
[ApiController]
[Authorize]
public class CoffeeController : ControllerBase
{
  ..


If we make a call to any endpoint of the Coffee API, we can debug HttpContext.User and see that it is populated and has an Identity with claims we have specified in JWT. It is an important thing in understanding how ASP.NET Core handles Authorization under the hood.

Coffee API - Claims are populated from the JWT

Add Authorization to Swagger UI

We did great work to protect Coffee API with the authorization. But if you run the Coffee API project and open Swagger UI, you won’t be able to send JWT as a part of the request. To fix that, we need to update the Program.cs file with the following code:

builder.Services.AddSwaggerGen(option =>
{
    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[]{}
        }
    });
});


After that, we will be able to see the Authorize button at the right top corner:

Coffee API - Authorize button appeared


When you click on the Authorize button you will be able to enter JWT as follows:

Coffee API - Enter JWT value

Use Postman for testing

You can not limit yourself to using Swagger UI and can perform testing of the API through the Postman tool. Let’s call /token endpoint of the Identity API first. We need to specify Content-Type header with the value application/json in the Headers section since we are going to use JSON as a payload.

Identity API - Specify headers


After that, we can call /token endpoint and get a new JWT.

Identity API - Generate JWT


Now, we can copy JWT and use it to call Coffee API. We need to specify Content-Type header similar to the Identity API if we want to test, create, and update endpoints. Authorization header also has to be set with the value Bearer [your JWT value]. After that, just hit Send button and see the result.

Coffee API - Get all entities

Role-based authorization

As you remember, the payload part of JWT is a set of claims with values that are exactly key-value pairs. Role-based authorization allows you to differentiate access to application resources depending on the role to which the user belongs.


If we update the Create() method in the TokenController.cs file in Identity API with the code that adds a new claim for the role; we can handle role-based authentication in the Coffee API. ClaimTypes.Role is a predefined name of the role claim.

var claims = new List<Claim>
{
    new Claim(ClaimTypes.Email, request.Email),
    new Claim(ClaimTypes.Role, "Barista")
};


Update the Authorize attribute in the CoffeeController.cs file specifying the role name:

[Authorize(Roles = "Barista")]


Now, all users who make a call to Coffee API have to have the role claim with the Barista value. Otherwise, they will get 403 Forbidden status code.

Claim-based authorization

An Authorize attribute can easily handle role-based authentication. But what if it is not enough, and we want to differentiate access based on some user properties like age or any other? You have probably already guessed that you can add your claims to JWT and use them to build authorization logic. Role-based authorization itself is a special case of claims-based authorization, just as a role is the same claim object of a predefined type.


Let’s update the Create() method in the TokenController.cs file in Identity API with the code that adds a new claim IsGourmet.

var claims = new List<Claim>
{
    new Claim(ClaimTypes.Email, request.Email),
    new Claim("IsGourmet", "true")
};


In the Program.cs file in Coffee API, we need to create a policy that verifies a claim and can be used in the Authorize attribute. The following code has to be added right after the AddAuthentication() method call.

builder.Services.AddAuthorization(opts => {
    opts.AddPolicy("OnlyForGourmet", policy => {
        policy.RequireClaim("IsGourmet", "true");
    });
});


Update the Authorize attribute in the CoffeeController.cs file specifying the policy name:

[Authorize(Policy = "OnlyForGourmet")]

Summary

Congratulations! You made a great effort in learning JWT in .NET. Now, you have to have a solid understanding of JWT principles and why it is important to use it to perform authorization in .NET applications. But we just scratched the surface in the area of authentication and authorization in ASP.NET Core applications.


I suggest looking into Microsoft documentation regarding the topics we discussed in this article. There are also a lot of built-in capabilities for authorization and role management in the .NET platform. A good addition to this article could be Microsoft documentation about authorization.