Introduction

In order to ensure data security in today’s web applications, with the Token Based Authentication method, which we can accept as one of the most effective and modern user authentication methods, we can achieve the highest efficiency in terms of system and user at the least cost, and we can perform the necessary authorization transactions safely.

In this article, we will examine the Token Based Authentication process in Asp.NET Core applications with JSON Web Token (JWT).

What is JWT?

To put it simply without going into complicated definitions; JSON Web Token (JWT) is a standard that enables data transfer as JSON securely between parties.

It is generally used in user authentication processes. There is no need to go to any different server during this authentication. Likewise, there is no need to query from the database. The token information coming to the server is verified by the code and the requested content is presented to the user. This is how it is used in simple terms.

When should we use JWT?

Information Exchange: JSON Web Token is a good way to securely exchange information between parties. Because JWTs can be signed, for example, using public/private key pairs you can be sure that they are the senders.

Authorization: This is the most common scenario for using JWT. Once the user is logged in, every subsequent request will contain the JWT and the system will allow the user to access the routes, services and methods allowed with this token.

What is JWT Structure?

It consists of 3 parts:

·         Header

·         Payload

·         Signature

These three parts are separated by a dot (.). Actually, its structure is as follows.

JWT Structure: “xxxxxxxxxxxxxx.yyyyyyyyyyyy.zzzzzzz”

Header

It contains two types of information. One of them is the Hashing algorithm. (HS256, RS512, ES356), the other type is JWT.

This JSON object, hashed with the Base64 algorithm, will form the first part of the JWT.

Payload

The payload contains the information of the logged in user such as user id, username and information about whether the user is an administrative user. Since JWT Tokens are not encrypted, they can be decoded with any base64 decoder, so there should be no sensitive information in the payload.

An example payload structure:

Payload JSON information will be encoded with Base64 and will form the second part of the JWT.

What is the JWT Signature?

The signature is responsible for the approval of the JWT. In order for the signature part to be formed, a base64-encoded header, base64-encoded payload and a specified secret key are required. Then, the hashing algorithm defined in the header is used to create the signature.

HMACSHA256(base64UrlEncode(header) + “.” + base64UrlEncode(payload), secret)

A JWT consisting of the encoded version of the previous header and payload information and the structure signed using secret is as follows.

The JWT, which is a combination of Base64 string expressions with a dot, can be easily transferred in HTML and HTTP environments, unlike the more complex XML-based standards.

What is Refresh Token?

It is the token value used to generate a new access token when the access token’s expiration date is nearing or has expired. In addition to the access token value given to the user, the refresh token value will be given, and if the access token expires, a new token can be requested with this refresh token. Thus, if the token expires, the user will be able to obtain a new token without being logged out of the session and continue on his way.

What will we build?

We will not create a new project in this article. We will develop the application that we developed in the previous article How to Build a .NET 6.0 API with PostgreSQL and EF Core. We will add a token authorization with JWT to this application. We will add token authorization based on role and we will provide refresh token value to the user.

Code

There is a library we need to do JWT integration in our Code Application. Microsoft.AspNetCore.Authentication.JwtBearer. Let’s download this library to the ‘User.API’ project via the NuGet package manager.

Nuget-JWT-Builder

Now we will add the following C# code for JWT configuration to ‘Program.cs’ under ‘User.API’.

namespace User.API.Handler
{
    public class TokenCreationHandler
    {
        private readonly IConfiguration configuration;

        public TokenCreationHandler(IConfiguration configuration)
        {
            this.configuration = configuration;
        }

        public Token createAccessToken(UserInfo user)
        {
            Token tokenInstance = new Token();
            var claims = new[] {
                        new Claim(JwtRegisteredClaimNames.Sub, "Subject"),
                        new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
                        new Claim(JwtRegisteredClaimNames.Iat, DateTime.UtcNow.ToString()),
                        new Claim(ClaimTypes.Role, user.Role),
                        new Claim("DisplayName", user.FullName),
                        new Claim("Email", user.Email)
                    };

            tokenInstance.Expiration = DateTime.Now.AddMinutes(10);

            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Jwt:Key"]));
            var signIn = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
            var token = new JwtSecurityToken(
                configuration["Jwt:Issuer"],
                configuration["Jwt:Audience"],
                claims,
                expires: tokenInstance.Expiration,
                signingCredentials: signIn);

            tokenInstance.AccessToken = new JwtSecurityTokenHandler().WriteToken(token);

            createRefreshToken(tokenInstance);

            return tokenInstance;
        }

        private void createRefreshToken(Token tokenInstance)
        {
            byte[] number = new byte[32];
            using (RandomNumberGenerator random = RandomNumberGenerator.Create())
            {
                random.GetBytes(number);
                tokenInstance.RefreshToken = Convert.ToBase64String(number);
            }
        }
    }
}

If you pay attention, the concepts of “Audience”, “Issuer”, “LifeTime”, “SigningKey” and “ClockSkew” are available. If we need to explain what these concepts are;

Audience

This is the field where we determine who/which origins/sites will use the token value to be created.

Issuer

It is the field where we will express who distributes the token value to be created.

LifeTime

It is the verification that will check the duration of the generated token value.

SigningKey

It is the verification of the security key data, which states that the token value to be generated is a value belonging to our application.

ClockSkew

It is the feature that allows the token value to be produced to be extended by the specified value. For example; If the ClockSkew value of the token value, whose usability period is set as 5 minutes, is given 3 minutes, the relevant token will be usable for 5 + 3 = 8 minutes. This is because, thanks to the ClockSkew property, we need to add the time difference to the token so that the common token value obtained on an application broadcasting on servers in different locations with a time difference does not lose its validity earlier on the server with the clock ahead. Thus, the usage period will be extended and the token value will be made fair use on all servers.

We have enabled Audience verification on the token with ValidateAudience.

With ValidateIssuer, we have enabled Issuer verification on the token.

With ValidateLifetime, we have enabled validation of the token value’s expiration date.

With ValidateIssuerSigningKey, we have activated the Security Key verification, which allows us to understand whether the token value belongs to this application.

We determined the Issuer value of the token in the application with ValidIssuer.

With ValidAudience, we determined the Audience value of the token in the app.

With IssuerSigningKey, we specify the current key via the SymmetricSecurityKey object for Security Key validation.

With ClockSkew, we specify the value of TimeSpan.Zero without adding any extra time on top of the token duration.

In addition, the Configuration property is used for the values used in the code block above. This means that we will read the relevant data from the “appsettings.json” file. So, in our example application, we will keep the data in the relevant file as follows.

Now we will update the ‘UserInfo.cs’ class we created earlier in the ‘User.Core’ project as follows.

We can start developing the endpoint where the user will log in. In the ‘User.API’ project, we will create a Web API controller named ‘LoginController’ under the Controllers file. Then we will paste the following codes into this controller.

Then ‘UserInfo.cs.’ We will create a role class that we will hold the roles defined in and write the following.

public class UserInfo
{
    public int Id { get; set; }

    public string FullName { get; set; }

    public int Age { get; set; }

    public string Email { get; set; }

    public string Password { get; set; }

    public string RefreshToken { get; set; }

    public DateTime? RefreshTokenEndDate { get; set; }

    public string Role { get; set; }
}

We can start developing the endpoint where the user will log in. In the ‘User.API’ project, we will create a Web API controller named ‘LoginController’ under the Controllers file. Then we will paste the following codes into this controller.

namespace User.API.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class LoginController : ControllerBase
    {
        private readonly UserInfoService userInfoService;
        private readonly TokenCreationHandler tokenHandler;
        public LoginController(UserInfoService userInfoService, TokenCreationHandler tokenHandler)
        {
            this.userInfoService = userInfoService;
            this.tokenHandler = tokenHandler;
        }
        [HttpPost]
        public async Task<Token> Login([FromForm] UserLogin userLogin)
        {
            UserInfo user =  await userInfoService.getUserInfoByEmail(userLogin.Email);
            if (user != null)
            {
                var token = tokenHandler.createAccessToken(user);

                user.RefreshToken = token.RefreshToken;
                user.RefreshTokenEndDate = token.Expiration.AddMinutes(3);

                await userInfoService.updateUserInfo(user);

                return token;
            }
            return new Token();
        }
    }
}

The ‘LoginController’ we created has a login endpoint of type HttpPost. This endpoint takes a model named ‘UserLogin’ as a parameter. Then, if this endpoint exists in the email database of the user in the model, it generates an access token and a refresh token for the Token model for this user.

Now let’s create the ‘TokenCreationHandler’ , ‘UserLogin’ and ‘Token’ classes that we use in the ‘LoginController’. Next, let’s add the ‘getUserInfoByEmail()’ method to the ‘UserInfoService’ and ‘UserInfoRepository’ classes.

namespace User.Core
{
    public class Token
    {
        public string AccessToken { get; set; }
        public DateTime Expiration { get; set; }
        public string RefreshToken { get; set; }
    }
}

We will add two models named ‘UserLogin’ and ‘Token’ to the ‘User.Core’ project and paste the codes below.

namespace User.Core
{
    public class Role
    {
        public const string SuperAdmin = "SuperAdmin";
        public const string Admin = "Admin";
        public const string BasicUser = "BasicUser";
    }
}
namespace User.Core
{
    public class UserLogin
    {
        [Required(ErrorMessage = "Email is required")]
        public string Email { get; set; }

        [Required(ErrorMessage = "Password is required")]
        public string Password { get; set; }

    }
}

In the ‘User.API’ project we will create a folder named ‘Handler’ and we will create a class named ‘TokenCreationHandler’ in it. After creating the ‘TokenCreationHandler’ class, let’s paste the following codes into this class.

 

namespace User.API.Handler
{
    public class TokenCreationHandler
    {
        private readonly IConfiguration configuration;

        public TokenCreationHandler(IConfiguration configuration)
        {
            this.configuration = configuration;
        }

        public Token createAccessToken(UserInfo user)
        {
            Token tokenInstance = new Token();
            var claims = new[] {
                        new Claim(JwtRegisteredClaimNames.Sub, "Subject"),
                        new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
                        new Claim(JwtRegisteredClaimNames.Iat, DateTime.UtcNow.ToString()),
                        new Claim(ClaimTypes.Role, user.Role),
                        new Claim("DisplayName", user.FullName),
                        new Claim("Email", user.Email)
                    };

            tokenInstance.Expiration = DateTime.Now.AddMinutes(10);

            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Jwt:Key"]));
            var signIn = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
            var token = new JwtSecurityToken(
                configuration["Jwt:Issuer"],
                configuration["Jwt:Audience"],
                claims,
                expires: tokenInstance.Expiration,
                signingCredentials: signIn);

            tokenInstance.AccessToken = new JwtSecurityTokenHandler().WriteToken(token);

            createRefreshToken(tokenInstance);

            return tokenInstance;
        }

        private void createRefreshToken(Token tokenInstance)
        {
            byte[] number = new byte[32];
            using (RandomNumberGenerator random = RandomNumberGenerator.Create())
            {
                random.GetBytes(number);
                tokenInstance.RefreshToken = Convert.ToBase64String(number);
            }
        }
    }
}

If you examine the above code block, we have designed a class that will generate refresh token values together with access token.

Now we will add a method to the ‘UserInfoService’ and ‘User Info Repository’ classes that returns user information by mail.

And we will add the following code to ‘UserInfoDbContext.cs’ class to avoid any runtime exception related to Datetime type in Postgresql.

For now, we have done the transactions that will actually generate tokens for the user. Before testing, we will create a sample controller where we can test role-based authorization operations. This controller will contain three endpoints. These three endpoints will only be accessible by one role.

First of all, let’s create a class named ‘Product.cs’ in the ‘User.Core’ project and paste the following codes.

Now we will create a Web API controller class named ‘ProductController.cs’ in the Controllers folder under the ‘User.API’ project. Then we will write the following codes in the ‘ProductController.cs’ class.

The endpoints we create will work on a role basis. For example, a user whose role is ‘SuperAdmin’ will get an error when trying to access the ‘/getAdminProducts’ endpoint. 

Since we have updated the ‘UserInfo.cs’ class before testing, we will need to migrate and update the database. Therefore, we will select the default project ‘User.Repository’ from the package manager console screen and run the following commands respectively.

Test the API

It’s time to test the application we have developed. First of all, let’s get the application up.

In the previous article, we performed the registration of the user to the system. We will use ‘UserController’ to add users to the system. The Swagger page that opens when the application stands up will be as follows.

JWT-Swagger-Page

First, we will create three different users for three different roles by running the POST ‘/api/user’ endpoint under User.

After creating our test users with different roles, we can see the users we created with GET ‘/api/user’.

JWT-swagger-get-users

We will login with use in the admin role we created and obtain our token information.

JWT-swagger-login-admin-role

After logging in, we see that the ‘accessToken’, ‘expiration’ and ‘refreshToken’ fields have been successfully delivered to us. Now let’s examine our ‘accessToken’ by decoding it from the jwt.io.


First of all, let’s add the ‘accessToken’ we obtained in the Postman application by typing the Authorization key to the Headers and ‘Bearer’ to its value and send a request to the ‘/api/product/admin’ endpoint. The request result is as follows.


We have successfully accessed the endpoint that users in the admin role can access. Now let’s try to send a request without sending Authorization in the Headers field.

 

Now we will perform our final test. With the access token of the user in the Admin role, we will send a request to the endpoint that only users in the Super Admin role can access.


As can be seen from this test, the access barrier to different role-specific endpoints is successfully stolen. This endpoint returns a 403(Forbidden) response.

Conclusion

There are many reasons why .JWT is the most popular token-based authorization system today. For example it works stateless. So there is no Session to control. Information and expiry date are kept neither on the server nor on the client side. Necessary information is kept in the token. Also there is no need to use cookies. It can also be easily used for mobile applications.

Due to the combination of these and many other reasons and the widespread use of JWT, we have successfully integrated JWT (JSON Web Token) into an Asp.Net Core 5 application in this article.

You can find the source code of the application here.