Webapi 2.0 how to implement refresh JWT token when

2020-07-14 09:53发布

问题:

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.

回答1:

I have some remarks:

  1. The access token is meant to be saved by the client and not in a session on the server. The same counts for the refresh token. The reason for that is, that there usually is no session. Smart clients can handle the token without session, MVC websites can use a cookie and the API doesn't know sessions. It is not forbidden, but then again you'll need to worry about session expiration and all users have to login again when you restart your server.

  2. If you want to implement OAuth then read the specification. In there you will find everything you'll need to implement the refresh token.

  3. In TokenController you handle the login. There you should check other conditions as well.

    • grant_type = password
    • Content-Type has to be "application/x-www-form-urlencoded"
    • the request should only be handled if send over a secured line (https).
  4. When the access_token is obtained and only if the refresh_token is requested, you should include the refresh_token in the access_token.

  5. You don't need a refresh token for client applications (grant_type = client_credentials) as those use a clientid / secret to obtain an access token. Extend TokenController to allow the client_credentials flow. Please note: refresh tokens are for users only and should be used only if they can be kept secret. A refresh token is very powerfull, so handle with care.

  6. In order to refresh an access token you'll need to send the refresh token to the endpoint. In your case you can extend the TokenController to allow a refresh_token request. You'll need to check:

    • grant_type = refresh_token
    • Content-Type has to be "application/x-www-form-urlencoded"
  7. There are several scenarios for the refresh token, which you can also combine:

    • Save the refresh token in a database. Each time a refresh token is used you can remove it from the database, then save the new refresh token which is also returned in the new access_token.
    • Set the refresh token to a longer lifetime and do not refresh it when the access token is refreshed. In this case the returned access_token does not include a new refresh token. That way you'll need to login again after the refresh_token expires.
  8. Please note, a refresh token that never expires and cannot be revoked gives a user unlimited access, so be carefull with your implementation.

  9. In my answer here you can see how a refresh token can be handled using Identity 2. You can consider to switch to Identity 2.

I think I've mentioned everything. Please let me know if I missed something or if something isn't clear.



回答2:

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/