X

JWT Authentication and refresh token in Asp.Net Core Web API

Dung Do Tien Dec 07 2020 1840
JWT authentication is standard for Json Web Token, It is a best solution for login with some stateless application type such as Restful Api. The Jwt uses a bearer token to check and allow users access to the application. In this article I will guide how to implement Jwt authentication and refresh tokens in Asp.net Core Web Api.

To research JWT and implement Jwt authentication in Asp.net core web Api you can refer to the home page of Jwt to get more information here.

1. What is JWT?

JWT is an Internet standard for creating data with optional signature and/or optional encryption whose payload holds JSON that asserts some number of claims. The tokens are signed either using a private secret or a public/private key.

To easily understand Jwt in Asp.net core web api , You can understand that Jwt uses tokens for authentication to the application. For example, an administrator wants to access the cms managed booking system. After login, the server will generate a token key and send it to the client, the client has to save it anywhere. For each after request, the client has to send that token to the server to verify user info, If the token is correct the server will allow access to resources.

1.1 What is the token in JWT?

Jwt bearer token contains three parts, separated by the dot:

- Header: Its content contains two information:  
 "alg": "HS256" : Indicate that what is the algorithm used to encrypt SIGNATURE? 
 "typ": "JWT" : This property indicates what is the type of authentication?

- Payload: Its content information of the user after login, Data formatted in JSON data type. You can store any information of user login here such as full name, email, mobile, roles… but don't store sensitive information here because this is published information.

- Signature: This is the most important part of token JWT, It is used to verify the sender, It helps ensure that the message wasn't changed by the user. The signature contains the secret key, the secret key will be stored in the server so if the user does not have the secret key will never change payload information.

Jwt bearer token will be sent to the server through header request. We will add one more key Authorization with value is “Bearer + token key” to the header of the request.

1.2 What is the secret key in JWT?

A secret key is a private key, this key is stored on the server-side. The secret key is combined with the header and the payload to create a unique hash. You are only able to verify this hash if you have the secret key.

To generate a secret key you can use Guid in c# to generate unique keys.

1.3 What is the claim in JWT?

Json Web Token (JWTs) claims are pieces of information asserted about a subject. As you know Payload stores information of user login with format JSON datatype.

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

Each property is a claim such as sub , name , admin. In a JWT, a claim appears as a name/value pair where the name is always a string and the value can be any JSON value. Generally, when we talk about a claim in the context of a JWT, we are referring to the name (or key).

There are two types of JWT claims:

Reserved: Claims defined by the JWT specification to ensure interoperability with third-party, or external, applications. OIDC standard claims are reserved claims.

Custom: Claims that you define yourself. But the name of the claim can not duplicate with the claim name available of JWT specification.

1.4 What is JWT used for?

- If you have a service provided in many platforms such as web, app, API... you want users to create an account only in a platform but they can access it on all other platforms.
- If you want to sign out a user account on all platforms from only one platform.

For example: Create a web API server to provide data for clients such as web angular, react js, vue js or mobile app...

2. What are Cons and Pros of JWT?

Pros

- Good for Application type No State or State Less
- Support cross-domain
- Easy scale-up
- No need database

Cons

- Cannot manage client from the server
- Have to protect the secret key
- Data overhead: If you combine much information inside the payload, the token will be large.

For cookie-based authentication, we can include Asp.net Core Identity, Owin, Membership… And for token-based authentication include JWT, Identity Server 4, Oauth 2, Openid...

See the above image you can see they are the same about the flow of action. But they have something different. You can see compare in the table below:

  Cookie Token
State Stateful server Stateless server
Scale Hard to achieve Easy to scale
Controlled by Server Client
Cross-domain No Yes
Performance Bad Good
Contain data No Yes
Size Tiny Can be as large as your data grow
Mobile ready No Yes

Based on the above table you can see that Jwt has many advantages but if you only have a web application you no need to use Jwt for authentication. In this case, using a cookie-based is the best option.

JWT's popularity comparison

I compare the JWT, Identity Server 4, Oauth 2, OpenID to see about popular them:

Search number display in Google

Compare in Google Trend

4. Implement JWT Authentication in Asp.net Core Web Api.

To implement JWT authentication in Asp.net core web api, I will guide step by step code. Note that I will not use a database for store Jwt token key or Jwt refresh token key. All token keys will be managed by the client.

STEP 1: Install JWT package

We have to install Microsoft.AspNetCore.Authentication.JwtBearer package. You can install it by using the Nuget package or Package Manager Console. Because I used .net core version 3.1 so I installed version 3.1.10, if you use another .net core version please choose an equivalent version.

PM> Install-Package Microsoft.AspNetCore.Authentication.JwtBearer -Version 3.1.10

STEP 2: Register JWT to service container.

Inside ConfigureServices() method of Startup.cs you need to add authentication to config JWT as below:

using Microsoft.IdentityModel.Tokens;
using Microsoft.AspNetCore.Authentication.JwtBearer;
……………….

public void ConfigureServices(IServiceCollection services)
{
    var secretKey = Encoding.ASCII.GetBytes(“9135176d-94830-456b-aff8-3dsf7b85b05f26”);
    services.AddAuthentication(auth =>
    {
        auth.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        auth.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    }).AddJwtBearer(token =>
    {
        token.RequireHttpsMetadata = false;
        token.SaveToken = true;
        token.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(secretKey),
            ValidateIssuer = true,
            ValidIssuer = “https://quizdeveloper.com”,
            ValidateAudience = true,
            ValidAudience = “https://quizdeveloper.com”,
            RequireExpirationTime = true,
            ValidateLifetime = true,
            ClockSkew = TimeSpan.Zero
        };
    });
}

STEP 3: Add JWToken Authentication service to middleware pipeline

Inside Configure() method of Startup.cs you need to add middleware to listening authentication and authorization for each request and response.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
  ------------------
  app.UseAuthentication();
  app.UseAuthorization();
  ------------------
}

Notice that: That two middleware have to be added below UseRouting() middleware.

STEP 4: Create JWTHelper class file.

JWTHelper will contain some common methods such as generate a token key, extract data from the payload of the token, validate token …

using System.IdentityModel.Tokens.Jwt;
using Microsoft.IdentityModel.Tokens;
public static class JwtHelper
{
     // Create some method here
}

- The first, create GenTokenkey() method, this method will help generate a token key.
To generate a token key we need to make claim data for payload, I will create some custom claim as below :

public class QDMClaimType
{
    public const string UserId = "userId"; 
    public const string DisplayName = "displayName";
    public const string ExpiredTime = "expiredTime";
    public const string EmailAddress = "emailAddress";
    public const string GuidId = "guidId";
}

public class AuthenticationResponse
{
    public AuthenticationResponse()
    {

    }
    public AuthenticationResponse(int id, string userName, string fullName, string email, DateTime? expriedTime, string token, string refreshToken)
    {
        Id = id;
        UserName = userName;
        FullName = fullName;
        Token = token;
        Email = email;
        ExpiredTime = expriedTime;
        RefreshToken = refreshToken;
        GuidId = System.Guid.NewGuid().ToString();
    }
    public int Id { get; set; }
    public string UserName { get; set; }
    public string FullName { get; set; }
    public string Token { get; set; }
    public string RefreshToken { get; set; }
    public string Email { get; set; }
    public string GuidId { get; set; }
    public DateTime? ExpiredTime { get; set; }
}

Next, I create a private method to help return a list of claims, it does contain all information the user will display in the payload of the token key.

private static IEnumerable<Claim> GetAdminClaims(AuthenticationResponse model)
{
    IEnumerable<Claim> claims = new Claim[]
            {
                new Claim(QDMClaimType.UserId, model.Id.ToString()),
                //new Claim(ClaimTypes.Name, model.UserName),
                new Claim(QDMClaimType.DisplayName, model.FullName),
                new Claim(QDMClaimType.EmailAddress, model.Email),
                new Claim(QDMClaimType.GuidId, model.GuidId),
                new Claim(QDMClaimType.ExpiredTime, model.ExpiredTime?.ToString("dd/MM/yyyy HH:mm:ss.fff"))
            };
    return claims;
}

Now I will create GenTokenkey() method as below:

public static string GenTokenkey(AuthenticationResponse model, int? expiredTimeInminutes = 0)
{
    try
    {
        if (model == null) return null;

        // Get secret key
        var key = Encoding.ASCII.GetBytes("9135176d-94830-456b-aff8-3dsf7b85b05f26");

        // Get expires time
        expiredTimeInminutes = !expiredTimeInminutes.HasValue || expiredTimeInminutes == 0 ? 120: expiredTimeInminutes; // 120 = 2h
        DateTime expireTime = DateTime.Now.AddMinutes(expiredTimeInminutes.Value);
        model.ExpiredTime = expireTime;
       
        // Generate new guid string help determine user login
        if(string.IsNullOrEmpty(model.GuidId)) model.GuidId = System.Guid.NewGuid().ToString();
       
        //Generate Token for user 
        var JWToken = new JwtSecurityToken(
            issuer: "https://quizdeveloper.com",
            audience: "https://quizdeveloper.com",
            claims: GetAdminClaims(model),
            notBefore: new DateTimeOffset(DateTime.Now).DateTime,
            expires: new DateTimeOffset(expireTime).DateTime ,
            //Using HS256 Algorithm to encrypt Token  
            signingCredentials: new SigningCredentials
            (new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256)
        );
        var token = new JwtSecurityTokenHandler().WriteToken(JWToken);
        return token;
    }
    catch (Exception)
    {
        return null;
    }
}

In that:

notBefore: Token will invalid if used before this time.
expires: Time expires of the token key.
signingCredentials: Define algorithm will be used to encrypt the secret key.

Say about the claim, you will see I defined two special properties are GuidId and ExpiredTime. They help execute some different business:

GuidId : Help all tokens are different. It helps one user login on to many devices when logout from a device, all other devices will not logout. On the contrary, if you want log out, all devices from one device you no need to use this property.

ExpiredTime : This property serves for refresh tokens, we can call refresh tokens by client or server. We will based on the value of this property to check if the token prepared expires. We can call refresh tokens to help create a new token.

- Secondly, we need to create the ExtracToken() method to help extract data from the token.

public static AuthenticationResponse ExtracToken(string token)
{
    try
    {
        if (string.IsNullOrEmpty(token)) return null;
        var key = Encoding.ASCII.GetBytes("9135176d-94830-456b-aff8-3dsf7b85b05f26");
        var tokenHandler = new JwtSecurityTokenHandler();
        tokenHandler.ValidateToken(token, new TokenValidationParameters
        {
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(key),
            ValidateIssuer = true,
            ValidIssuer = "https://quizdeveloper.com",
            ValidateAudience = true,
            ValidAudience = "https://quizdeveloper.com",
            RequireExpirationTime = true,
            ValidateLifetime = true,
            ClockSkew = TimeSpan.Zero
        }, out SecurityToken validatedToken);

        var jwtToken = (JwtSecurityToken)validatedToken;
        int adminId = int.Parse(jwtToken.Claims.First(x => x.Type.Contains("userId")).Value);
        //string userName = jwtToken.Claims.First(x => x.Type.Contains("name")).Value;
        string displayName = jwtToken.Claims.First(x => x.Type.Contains("displayName")).Value;
        string emailaddress = jwtToken.Claims.First(x => x.Type.Contains("emailAddress")).Value;
        string guidId = jwtToken.Claims.First(x => x.Type.Contains("guidId")).Value;
        DateTime expiredTime = (jwtToken.Claims.First(x => x.Type.Contains("expiredTime")).Value).AsDateTimeExac(DateTime.Now);

        return new AuthenticationResponse()
        {
            Id = adminId,
            //UserName = userName,
            FullName = displayName,
            Email = emailaddress,
            ExpiredTime = expiredTime,
            Token = token,
            GuidId = guidId,
        };
    }
    catch (Exception)
    {
        return null;
    }
}

In that, I have been defined AsDateTimeExac() extension method, this method helps format DateTime same with format datetime in the claim.

public static DateTime AsDateTimeExac(this object obj, DateTime defaultValue = default(DateTime))
{
    if (obj == null || string.IsNullOrEmpty(obj.ToString()))
        return defaultValue;

    DateTime result;
    if (!DateTime.TryParseExact(obj.ToString(), "dd/MM/yyyy HH:mm:ss.fff", CultureInfo.InvariantCulture, DateTimeStyles.None, out result))
        return defaultValue;

    return result;
}

- Finally, I create more IsTokenValid() method to help validate token sends from clients.
If we can extract data from a token, we can understand that token is valid.

public static bool IsTokenValid(string token)
{
    if (string.IsNullOrEmpty(token)) return false;
    AuthenticationResponse payload = ExtracToken(token);
    if (payload == null) return false;
    return true;
}

STEP 5:  Create a service for business help authentication.

I will create IAdminBo interface with two methods as below:

public interface IAdminBo
{
    // Help check authentication
    Task<AuthenticationResponse> Authentication(string username, string password);

    // Help refresh token
    ResponseApi RefeshToken(string refreshToken);
}

And then I will create  AdminBo class to implement IAdminBo interface:

public class AdminBo : IAdminBo
{

    /// <summary>
    /// Authentication with JWT
    /// </summary>
    /// <param name="username"></param>
    /// <param name="password"></param>
    /// <param name="token"></param>
    /// <returns></returns>
    public async Task<AuthenticationResponse> Authentication(string username, string password)
    {
        try
        {
            var adminInfo = await _adminDal.GetLogin(username, password);
            if (adminInfo == null) return null;

            var responAdmin = new AuthenticationResponse() {
             Id = adminInfo.Id,
             FullName = adminInfo.FullName,
             Email = adminInfo.Email,
             UserName = adminInfo.UserName
            };

            string token = JwtHelper.GenTokenkey(responAdmin);

            responAdmin.GuidId = System.Guid.NewGuid().ToString();
            string newRefreshToken = JwtHelper.GenTokenkey(responAdmin, 10800);

            return new AuthenticationResponse(adminInfo.Id, adminInfo.UserName, adminInfo.FullName, adminInfo.Email, responAdmin.ExpiredTime, token, newRefreshToken);
        }
        catch (Exception ex)
        {
            return null;
        }
    }

    /// <summary>
    /// Refresh token for user
    /// </summary>
    /// <param name="refreshToken">Current refresh token</param>
    /// <returns></returns>
    public ResponseApi RefeshToken(string refreshToken)
    {
       var response = new ResponseApi();
        try
        {
            //Step 1: Check null & validate token 
            if (string.IsNullOrEmpty(refreshToken))
            {
                response.IsError = true;
                response.Message = "Token does not exist.";
                return response;
            }

            bool isTokenValid = JwtHelper.IsTokenValid(refreshToken);
            if (!isTokenValid)
            {
                response.IsError = true;
                response.Message = "Token invalid.";
                return response;
            }

            // Step 2: Create new token & return
            AuthenticationResponse payload = JwtHelper.ExtracToken(refreshToken);
            string newToken = JwtHelper.GenTokenkey(payload);
            string newRefreshToken = JwtHelper.GenTokenkey(payload, 10800);
            payload.Token = newToken;
            payload.RefreshToken = newRefreshToken;

            response.IsError = false;
            response.Data = payload;
            return response;
        }
        catch(Exception)
        {
            response.IsError = true;
            response.Message = "Application error.";
            return response;
        }
    }

}

I have been created the ResponseApi class help return information to the client:

public class ResponseApi
{
    public ResponseApi()
    {
        StatusCode = 200;
        IsError = false;
        Message = "";
        Data = null;
    }
    public int StatusCode { get; set; }
    public bool IsError { get; set; }
    public string Message { get; set; }
    public object Data { get; set; }
}

STEP 6: Create API action controller help call from the client.

In this layer, I will create a base controller, it will contain many things common need use for all other controllers:

public class BaseApiController : ControllerBase
{
    public ResponseApi ResponseResult { get; set; }
}

Okay now I will create Authentication controller with two actions help login and refresh token:

[ApiController]
[Route("api/authetication")]
public class AuthenticationController : BaseApiController
{
    private readonly ILogger<AuthenticationController> _logger;
    private readonly IAdminBo _adminBo;
    public AuthenticationController(ILogger<AuthenticationController> logger, IAdminBo adminBo)
    {
        _logger = logger;
        _adminBo = adminBo;
    }
    
    /// <summary>
    /// Login to Cms
    /// </summary>
    /// <param name="model">Username and password</param>
    /// <returns></returns>
    [HttpPost]
    public async Task<IActionResult> Authentication([FromBody]AuthenticationRequest model)
    {

        ResponseResult = new Model.Common.ResponseApi();
        try
        {
            // Step 1: validate form data
            if(model == null || string.IsNullOrEmpty(model.Username) || string.IsNullOrEmpty(model.Password))
            {
                ResponseResult.IsError = true;
                ResponseResult.Message = "Username and password are require.";
                return Ok(ResponseResult);
            }

            // Step 2: Check exist account 
            var adminObj = await _adminBo.Authentication(model.Username, model.Password);

            if(adminObj == null)
            {
                ResponseResult.IsError = true;
                ResponseResult.Message = "Account does not exist.";
                return Ok(ResponseResult);
            }

            // Step 3: return token
            ResponseResult.IsError = false;
            ResponseResult.Message = string.Empty;
            return Ok(adminObj);


        }
        catch(Exception ex)
        {
            _logger.LogError(ex.Message);
            ResponseResult.IsError = true;
            ResponseResult.Message = "Some error occurs";
            return Ok(ResponseResult);
        }
    }

    /// <summary>
    /// Refresh token key
    /// </summary>
    /// <param name="model"></param>
    /// <returns></returns>
    [HttpPost]
    [Route("refresh-token")]
    public IActionResult RefreshToken([FromBody]RefreshTokeRequest model)
    {
        ResponseResult = new Model.Common.ResponseApi();
        try
        {
            if (model == null)
            {
                ResponseResult.IsError = true;
                ResponseResult.Message = StaticVariable.API.JWT.TOKEN_NOT_EXIST;
                return Ok(ResponseResult);
            }

            var response = _adminBo.RefeshToken(model.RefreshToken);
            return Ok(response);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex.Message);
            ResponseResult.IsError = true;
            ResponseResult.Message = StaticVariable.API.Common.APPLICATION_ERROR;
            return Ok(ResponseResult);
        }
    }

}

public class AuthenticationRequest
{
    [Required]
    public string Username { get; set; }

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

public class RefreshTokeRequest
{
    [Required]
    public string RefreshToken { get; set; }
}

STEP 7: Create middleware to listen to all requests.

We need to create a middleware to check all requests from the application and get the token key from header request, the middleware will validate the token and if the token correct it will be added to HttpContent of request.

public class JwtAuthorizationMid
{
    private readonly RequestDelegate _next;

    public JwtAuthorizationMid(RequestDelegate next)
    {
        _next = next;
    }

    public Task Invoke(HttpContext httpContext)
    {
        var token = httpContext.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last();
        if (token != null)
        {
            // Extract value from token and validate 
            var extractTokenObjet = JwtHelper.ExtracToken(token);

            if (extractTokenObjet != null && extractTokenObjet.ExpiredTime > DateTime.Now)
            {
                httpContext.Items["Token"] = token;
            }
        }

        return _next(httpContext);
    }
}

// Extension method used to add the middleware to the HTTP request pipeline.
public static class JwtAuthorizationMidExtensions
{
    public static IApplicationBuilder UseJwtAuthorizationMid(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<JwtAuthorizationMid>();
    }
}

This is a custom middleware, if you still don’t know how to create a custom middleware you can refer to the article Custom middleware in Asp.net Core

And then we need to add this middleware to the pipeline by add line code below inside Configure() method of Startup.cs:

app.UseJwtAuthorizationMid();

Please add this line code before middleware app.UseAuthentication()

STEP 8: Custom Authorize attribute filter.

[Authorize] filter help auto check if the user is authenticated or not. If not it will redirect to the login page.

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AuthorizeAttribute : Attribute, IAuthorizationFilter
{
    public void OnAuthorization(AuthorizationFilterContext context)
    {
        var user = context.HttpContext.Items["Token"];
        if (user == null)
        {
            context.Result = new JsonResult(new { message = "Unauthorized" }) { StatusCode = StatusCodes.Status401Unauthorized };
        }
    }
}

Okay, it’s almost done. Any controller or action you want to the user has to log in before access it, you can add [Authorize] before of it. See an example below:

[Authorize]
[ApiController]
[Route("api/news")]
public class NewsController : BaseApiController
{
    private readonly ILogger<NewsController> _logger;
    public NewsController(ILogger<NewsController> logger)
    {
        _logger = logger;
    }

    [HttpGet]
    public async Task<IActionResult> Index()
    {
        try
        {
            ResponseResult = new Model.Common.ResponseApi();
            ResponseResult.Data = new List<NewsModel>() { 
              new NewsModel() { Id = 1, Title ="Article 1"},
              new NewsModel() { Id = 2, Title ="Article 2"},
              new NewsModel() { Id = 3, Title ="Article 3"},
              new NewsModel() { Id = 4, Title ="Article 4"}
            };
            return Ok(ResponseResult);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex.Message);
            ResponseResult.IsError = true;
            ResponseResult.Message = "Some error occurs";
            return Ok(ResponseResult);
        }
    }
}

STEP 9: See the result.

Okay, now we can run the project and test all API by using the PostMan application.

This is the result when login success:

This is decode token key:

This is the refresh token result:

This is the result when user access to function not authentication:

After authentication:

 

5. Summary.

Notice that if you want to get user information after login from any actions, you can get a token key from http context and using ExtracToken() method of JwtHelper. You can create a command method in BaseController to easily use any other controller.

And you can see in this example I didn’t use a database to store token-key. You want to logout users from all devices you have to manage their token key inside the database.

Okay, In this article I only want to show you What is JWT? Why is JWT? And how to implement JWT in Asp.Net Core Web API 3.1

I hope this article brings some helpful knowledge to you.