Webapi 2.0 how to implement refresh JWT token when access token expired

16,331

This can be done with a separate persisting refresh token. A nice tutorial at http://www.c-sharpcorner.com/article/handle-refresh-token-using-asp-net-core-2-0-and-json-web-token/

Share:
16,331
Neeraj Kumar Gupta
Author by

Neeraj Kumar Gupta

Updated on June 04, 2022

Comments

  • Neeraj Kumar Gupta
    Neeraj Kumar Gupta almost 2 years

    I am quite new in web API implementation, I have created a web API service to use it with ASP.net web form applications as well as some stand alone applications(C# Console/Windows application) using HttpClient object.

    I have implemented a basic JWT access token authentication with expiration time limit in web api, this authentication technique is working fine until token not expired, when token get expired web api does not accept request as token has expired! which is fine as per authentication implementation, but I want to implement refresh token logic in web api so token can renew/refersh and client should be able to use the web api resource.

    I googled a lot but unable to find the proper implementation of refresh token logic. Please help me if any one has right approach to handle the expired access token.

    Following are the steps that I have followed to use the web api in asp.net application.

    1. In ASP.net web form login page I called the web API "TokenController" this controller take two arguments loginID and password and return the JWT token that I stored in session object.

    2. Now whenever my client application need too use the web api resource has to send the access token in request header while making call to web api using httpclient.

    3. But when token get expired client unable use the web api resource he has to login again and renew the token! this I don't want, user should not prompt to be login again as application session out time not elapsed yet.

    How do I refresh the token without forcing user to login again.

    If my given below JWT access token implementation logic is not suitable or it is incorrect, please let me know the correct way.

    Following is the code.

    WebAPI

    AuthHandler.cs

      public class AuthHandler : DelegatingHandler
        {
    
        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
                    CancellationToken cancellationToken)
        {
            HttpResponseMessage errorResponse = null;           
            try
            {
                IEnumerable<string> authHeaderValues;
                request.Headers.TryGetValues("Authorization", out authHeaderValues);
    
                if (authHeaderValues == null)
                    return base.SendAsync(request, cancellationToken);
    
                var requestToken = authHeaderValues.ElementAt(0);
    
                var token = "";
    
                if (requestToken.StartsWith("Bearer ", StringComparison.CurrentCultureIgnoreCase))
                {
                    token = requestToken.Substring("Bearer ".Length);
                }
    
                var secret = "w$e$#*az";
    
                ClaimsPrincipal cp = ValidateToken(token, secret, true);
    
    
                Thread.CurrentPrincipal = cp;
    
                if (HttpContext.Current != null)
                {
                    Thread.CurrentPrincipal = cp;
                    HttpContext.Current.User = cp;
                }
            }
            catch (SignatureVerificationException ex)
            {
                errorResponse = request.CreateErrorResponse(HttpStatusCode.Unauthorized, ex.Message);
            }
            catch (Exception ex)
            {
                errorResponse = request.CreateErrorResponse(HttpStatusCode.InternalServerError, ex.Message);
            }
    
    
            return errorResponse != null
                ? Task.FromResult(errorResponse)
                : base.SendAsync(request, cancellationToken);
        }
    
        private static ClaimsPrincipal ValidateToken(string token, string secret, bool checkExpiration)
        {
            var jsonSerializer = new JavaScriptSerializer();
            string payloadJson = string.Empty;
    
            try
            {
                payloadJson = JsonWebToken.Decode(token, secret);
            }
            catch (Exception)
            {
                throw new SignatureVerificationException("Unauthorized access!");
            }
    
            var payloadData = jsonSerializer.Deserialize<Dictionary<string, object>>(payloadJson);
    
    
            object exp;
            if (payloadData != null && (checkExpiration && payloadData.TryGetValue("exp", out exp)))
            {
                var validTo = AuthFactory.FromUnixTime(long.Parse(exp.ToString()));
                if (DateTime.Compare(validTo, DateTime.UtcNow) <= 0)
                {
                    throw new SignatureVerificationException("Token is expired!");
                }
            }
    
            var clmsIdentity = new ClaimsIdentity("Federation", ClaimTypes.Name, ClaimTypes.Role);
    
            var claims = new List<Claim>();
    
            if (payloadData != null)
                foreach (var pair in payloadData)
                {
                    var claimType = pair.Key;
    
                    var source = pair.Value as ArrayList;
    
                    if (source != null)
                    {
                        claims.AddRange(from object item in source
                                        select new Claim(claimType, item.ToString(), ClaimValueTypes.String));
    
                        continue;
                    }
    
                    switch (pair.Key.ToUpper())
                    {
                        case "USERNAME":
                            claims.Add(new Claim(ClaimTypes.Name, pair.Value.ToString(), ClaimValueTypes.String));
                            break;
                        case "EMAILID":
                            claims.Add(new Claim(ClaimTypes.Email, pair.Value.ToString(), ClaimValueTypes.Email));
                            break;
                        case "USERID":
                            claims.Add(new Claim(ClaimTypes.UserData, pair.Value.ToString(), ClaimValueTypes.Integer));
                            break;
                        default:
                            claims.Add(new Claim(claimType, pair.Value.ToString(), ClaimValueTypes.String));
                            break;
                    }
                }
    
            clmsIdentity.AddClaims(claims);
    
            ClaimsPrincipal cp = new ClaimsPrincipal(clmsIdentity);
    
            return cp;
        }
    
    
    }
    

    AuthFactory.cs

    public static class AuthFactory
    {
    internal static DateTime FromUnixTime(double unixTime)
    {
        var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
        return epoch.AddSeconds(unixTime);
    }
    
    
    internal static string CreateToken(User user, string loginID, out double issuedAt, out double expiryAt)
    {
    
        var unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
        expiryAt = Math.Round((DateTime.UtcNow.AddMinutes(TokenLifeDuration) - unixEpoch).TotalSeconds);
        issuedAt = Math.Round((DateTime.UtcNow - unixEpoch).TotalSeconds);
    
        var payload = new Dictionary<string, object>
            {
                {enmUserIdentity.UserName.ToString(), user.Name},
                {enmUserIdentity.EmailID.ToString(), user.Email},
                {enmUserIdentity.UserID.ToString(), user.UserID},
                {enmUserIdentity.LoginID.ToString(), loginID}
                ,{"iat", issuedAt}
                ,{"exp", expiryAt}
            };
    
        var secret = "w$e$#*az";
    
        var token = JsonWebToken.Encode(payload, secret, JwtHashAlgorithm.HS256);
    
        return token;
    }
    
    public static int TokenLifeDuration
    {
        get
        {
            int tokenLifeDuration = 20; // in minuets
            return tokenLifeDuration;
        }
    }
    
    internal static string CreateMasterToken(int userID, string loginID)
    {
    
        var payload = new Dictionary<string, object>
            {
                {enmUserIdentity.LoginID.ToString(), loginID},
                {enmUserIdentity.UserID.ToString(), userID},
                {"instanceid", DateTime.Now.ToFileTime()}
            };
    
        var secret = "w$e$#*az";
    
        var token = JsonWebToken.Encode(payload, secret, JwtHashAlgorithm.HS256);
    
        return token;
    }
    

    }

    WebApiConfig.cs

    public static class WebApiConfig
    {
    
        public static void Register(HttpConfiguration config)
        {
            var cors = new EnableCorsAttribute("*", "*", "*");
            config.EnableCors(cors);
    
            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{action}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
    
            config.Formatters.Remove(config.Formatters.XmlFormatter);
    
            config.MessageHandlers.Add(new AuthHandler());
        }
    }
    

    TokenController .cs

    public class TokenController : ApiController
    {
        [AllowAnonymous]
        [Route("signin")]
        [HttpPost]
        public HttpResponseMessage Login(Login model)
        {
            HttpResponseMessage response = null;
            DataTable dtblLogin = null;
            double issuedAt;
            double expiryAt;
    
            if (ModelState.IsValid)
            {
                dtblLogin = LoginManager.GetUserLoginDetails(model.LoginID, model.Password, true);
    
                if (dtblLogin == null || dtblLogin.Rows.Count == 0)
                {
                    response = Request.CreateResponse(HttpStatusCode.NotFound);
                }
                else
                {
                    User loggedInUser = new User();
                    loggedInUser.UserID = Convert.ToInt32(dtblLogin.Rows[0]["UserID"]);
                    loggedInUser.Email = Convert.ToString(dtblLogin.Rows[0]["UserEmailID"]);
                    loggedInUser.Name = Convert.ToString(dtblLogin.Rows[0]["LastName"]) + " " + Convert.ToString(dtblLogin.Rows[0]["FirstName"]);
    
                    string token = AuthFactory.CreateToken(loggedInUser, model.LoginID, out issuedAt, out expiryAt);
                    loggedInUser.Token = token;
    
                    response = Request.CreateResponse(loggedInUser);
    
                }
            }
            else
            {
                response = Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState);
            }
            return response;
        }
    
        protected override void Dispose(bool disposing)
        {
            base.Dispose(disposing);
        }
    
    }
    

    PremiumCalculatorController.cs

    PremiumCalculatorController : ApiController
    {
        [HttpPost]
        public IHttpActionResult CalculatAnnualPremium(PremiumFactorInfo premiumFactDetails)
        {
          PremiumInfo result;
          result = AnnualPremium.GetPremium(premiumFactDetails);
          return Ok(result);
        }
    }
    

    Web Form Application

    Login.aspx.cs

    public class Login
    {
        protected void imgbtnLogin_Click(object sender, System.EventArgs s)
        {
    
        UserInfo loggedinUser = LoginManager.ValidateUser(txtUserID.text.trim(), txtPassword.text);
    
        if (loggedinUser != null)
        {
    
            byte[] password = LoginManager.EncryptPassword(txtPassword.text);
    
            APIToken tokenInfo = ApiLoginManager.Login(txtUserID.text.trim(), password);
    
            loggedinUser.AccessToken = tokenInfo.Token;
    
            Session.Add("LoggedInUser", loggedinUser);
    
            Response.Redirect("Home.aspx");
    
        }
        else
        {
            msg.Show("Logn ID or Password is invalid.");
        }
    
    
        }
    }
    

    ApiLoginManager.cs

    public class ApiLoginManager
    {
        public UserDetails Login(string userName, byte[] password)
        {
            APIToken result = null;
            UserLogin objLoginInfo;
            string webAPIBaseURL = "http://localhost/polwebapiService/"
            try
            {
                using (var client = new HttpClient())
                {
                    result = new UserDetails();
                    client.BaseAddress = new Uri(webAPIBaseURL);
                    objLoginInfo = new UserLogin { LoginID = userName, Password = password };
    
                    var response = client.PostAsJsonAsync("api/token/Login", objLoginInfo);
    
                    if (response.Result.IsSuccessStatusCode)
                    {
                        string jsonResponce = response.Result.Content.ReadAsStringAsync().Result;
                        result = JsonConvert.DeserializeObject<APIToken>(jsonResponce);
                    }
    
                    response = null;
                }
    
                return result;
            }
            catch (Exception ex)
            {
                throw ex;
            }
    
        }
    
    }
    

    AnnualPremiumCalculator.aspx.cs

    public class AnnualPremiumCalculator
    {
        protected void imgbtnCalculatePremium_Click(object sender, System.EventArgs s)
        { 
           string token = ((UserInfo)Session["LoggedInUser"]).AccessToken;
           PremiumFactors premiumFacts = CollectUserInputPremiumFactors();
           PremiumInfo premiumDet = CalculatePremium(premiumFacts, token);
           txtAnnulPremium.text = premiumDet.Premium;
           //other details so on 
        }
    
        public PremiumInfo CalculatePremium(PremiumFactors premiumFacts, string accessToken)
        {
            PremiumInfo result = null;
            string webAPIBaseURL = "http://localhost/polwebapiService/";
            try
            {
                using (var client = new HttpClient())
                {
                    client.BaseAddress = new Uri(webAPIBaseURL);
    
                    StringContent content = new StringContent(JsonConvert.SerializeObject(premiumFacts), Encoding.UTF8, "application/json");
    
                    client.DefaultRequestHeaders.Accept.Clear();
                    client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
                    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
    
                    var response = client.PostAsync("api/calculators/PremiumCalculator", content);
    
                    if (response.Result.IsSuccessStatusCode)
                    {
                        string jsonResponce = response.Result.Content.ReadAsStringAsync().Result;
                        result = JsonConvert.DeserializeObject<PremiumInfo>(jsonResponce);
                    }
    
                    response = null;
    
                }
    
                return result;
            }
            finally
            {
    
            }
    
        }
    
    }
    

    above is a sample code to illustrate the issue, it may have some typo.