JWT refresh Token solution in Asp.Net Core

Keywords: C# .NET

1, Foreword

1. Regarding the expiration of JWT Token, how long is it set to expire?

(1) Some people set the expiration time to be very long, such as one month or even longer. When it expires, they return to the login page, log in again and get the token again. During the login, they also get the token again, and then the expiration time is reset to one month. In this way, once a token is intercepted, it may be used for a long time. If you want to prohibit it, you can only modify the key issued by the token, which will lead to the invalidation of all tokens. Obviously, it is not advisable.

(2) Some people set it for a short time, such as 10 minutes. In the process of use, once it expires, they also return to the login page. In this way, they may often return to the login page during use, and the experience is very bad.

2. Here is a mainstream solution - Double Token mechanism

(1) . access token: accessToken. The access interface needs to be carried, that is, the one we have been using before. The expiration time is generally set to be short. According to the analysis of the actual project, for example, 10 minutes

(2) . refresh token: refreshToken. When the accessToken expires, it is used to obtain a new accessToken. The expiration time is generally set to be long, such as 7 days

3. When obtaining a new accessToken, why do you need to pass in the old accessToken instead of just the refreshToken?

  Take a closer look at the solution below. It is OK to pass in only refreshToken, but the security of passing in double Token is higher.

2, Solution

1. After the login request, save the userId and userAccount in payLoad, set different expiration times, and generate accessToken and refreshToken respectively. The difference keys and expiration times of the two are different, and then save the relevant information of the generated refreshToken in the corresponding table [id, userId, token, expire], A user corresponds to a record (it can also be saved in Redis. Here, for testing, there is a global variable),   Each time you log in, add or update records, and finally return the double token to the front end, which is stored in LocalStorage.

 

2. The front end accesses the GetMsg interface to obtain information. The header needs to carry an accessToken. The server side verifies it through the JwtCheck2 filter. If it passes the verification, it will access normally. If it does not pass, it returns 401 and the reason for failure, and the front end obtains it in Error. Here, the reason for 401 is distinguished.

//Get information interface
        function GetMsg() {
            var accessToken = window.localStorage.getItem("accessToken");
            $.ajax({
                url: "/Home/GetMsg",
                type: "Post",
                data: {},
                datatype: "json",
                beforeSend: function (xhr) {
                    xhr.setRequestHeader("Authorization", "Bearer " + accessToken);
                },
                success: function (data) {
                    if (data.status == "ok") {
                        alert(data.msg);
                    } else {
                        alert(data.msg);
                    }
                },
                //Enter here when the security check fails
                error: function (xhr) {
                    if (xhr.status == 401) {
                        var errorMsg = xhr.responseText;
                        console.log(errorMsg);
                        //alert(errorMsg);
                        if (errorMsg == "expired") {
                            //Indicates that it has expired and needs to be refreshed automatically
                            GetTokenAgain(GetMsg);
                        } else {
                            //Indicates that it is an illegal request. If you give a prompt, you can directly return to the login page
                            alert("Illegal request");
                        }
                    }
                }
            });
        }

3. If the header is empty, verification error, etc., you will be prompted that the request is illegal and return to the login page.

4. If the captured is expired, that is, expired, call GetTokenAgain(func) method, that is, re obtain accessToken and refreshToken. Here func represents passing in a method name, so that the original method can be called again after the call is successful to achieve seamless refresh; Pass the double Token to the server. The verification logic of the server is as follows:

(1) . first verify the physical legitimacy of refreshToken through pure code. If it is illegal, the front end will directly report an error and return to the login page.

(2) . parse userId and other data from the accessToken (even if the accessToken has expired, it can still be parsed)

(3) Take userId, refreshtoken and current time to look up the data in the refreshtoken table. If it cannot be found, directly return to the front end with an error and return to the login page.

(4) . if it can be found, regenerate the accessToken and refreshtoken and write them into the refreshtoken table

(5) . return the double token to the front end, the front end will overwrite and store it, and then automatically call the original method to carry the new accessToken for access, so as to realize the problem of seamlessly refreshing the token.

//Re acquire access token and refresh token
        function GetTokenAgain(func) {
            var model = {
                accessToken: window.localStorage.getItem("accessToken"),
                refreshToken: window.localStorage.getItem("refreshToken")
            };
            $.ajax({
                url: '/Home/UpdateAccessToken',
                type: "POST",
                dataType: "json",
                data: model,
                success: function (data) {
                    if (data.status == "error") {
                        debugger;
                        // Indicates that the token re acquisition failed and can be returned to the login page
                        alert("Failed to retrieve token");

                    } else {
                        window.localStorage.setItem("accessToken", data.data.accessToken);
                        window.localStorage.setItem("refreshToken", data.data.refreshToken);
                        func();
                    }
                }
            });

PS: the above scheme is applicable to sending a single ajax request from a single page. If there are multiple requests, they are sent in order. For example, the first one is sent, and then the second one is sent. This scenario is no problem.

However, in a special case, if multiple ajax on a page come in parallel, if one of the accesstokens has expired, it will follow the mechanism of updating the token. At this time, the refreshToken and accessToken are updated (the refreshToken in the database is also updated), which will lead to the failure of the refreshToken verification of other ajax that came in at the same time, so that the double tokens cannot be refreshed.

For this special case, as a trade-off, the refreshToken is not updated in the method of updating the accessToken, so the refreshToken expires and would have to enter the login page. Therefore, this trade-off is understandable for such cases.

Share the full code below:

Front end code:

@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Index</title>
    <script src="~/lib/jquery/dist/jquery.js"></script>
    <script>
        $(function () {
            $('#btn1').click(function () {
                Login();
            });
            $('#btn2').click(function () {
                GetMsg();
            });
        });

        //Login interface
        function Login() {
            $.ajax({
                url: "/Home/CheckLogin",
                type: "Post",
                data: { userAccount: "admin", userPwd: "123456" },
                datatype: "json",
                success: function (data) {
                    if (data.status == "ok") {
                        alert(data.msg);
                        console.log(data.data.accessToken);
                        console.log(data.data.refreshToken);
                        window.localStorage.setItem("accessToken", data.data.accessToken);
                        window.localStorage.setItem("refreshToken", data.data.refreshToken);

                    } else {
                        alert(data.msg);
                    }
                },
                //Enter here when the security check fails
                error: function (xhr) {
                    if (xhr.status == 401) {
                        console.log(xhr.responseText);
                        alert(xhr.responseText)
                    }
                }
            });

        }

        //Get information interface
        function GetMsg() {
            var accessToken = window.localStorage.getItem("accessToken");
            $.ajax({
                url: "/Home/GetMsg",
                type: "Post",
                data: {},
                datatype: "json",
                beforeSend: function (xhr) {
                    xhr.setRequestHeader("Authorization", "Bearer " + accessToken);
                },
                success: function (data) {
                    if (data.status == "ok") {
                        alert(data.msg);
                    } else {
                        alert(data.msg);
                    }
                },
                //Enter here when the security check fails
                error: function (xhr) {
                    if (xhr.status == 401) {
                        var errorMsg = xhr.responseText;
                        console.log(errorMsg);
                        //alert(errorMsg);
                        if (errorMsg == "expired") {
                            //Indicates that it has expired and needs to be refreshed automatically
                            GetTokenAgain(GetMsg);
                        } else {
                            //Indicates that it is an illegal request. If you give a prompt, you can directly return to the login page
                            alert("Illegal request");
                        }
                    }
                }
            });
        }

        //Re acquire access token and refresh token
        function GetTokenAgain(func) {
            var model = {
                accessToken: window.localStorage.getItem("accessToken"),
                refreshToken: window.localStorage.getItem("refreshToken")
            };
            $.ajax({
                url: '/Home/UpdateAccessToken',
                type: "POST",
                dataType: "json",
                data: model,
                success: function (data) {
                    if (data.status == "error") {
                        debugger;
                        // Indicates that the token re acquisition failed and can be returned to the login page
                        alert("Failed to retrieve token");

                    } else {
                        window.localStorage.setItem("accessToken", data.data.accessToken);
                        window.localStorage.setItem("refreshToken", data.data.refreshToken);
                        func();
                    }
                }
            });
        }

    </script>
</head>
<body>
    <button id="btn1">Simulated Login logic</button>
    <button id="btn2">Get system information</button>

</body>
</html>

Server side code 1:

(PS: if there are special cases mentioned above, remove the codes of 4.2 and 4.3 in the update mechanism)

1    public class HomeController : Controller
  2     {
  3         private static List<RefreshToken> rTokenList = new List<RefreshToken>();
  4 
  5         public IConfiguration _Configuration { get; }
  6 
  7         public HomeController(IConfiguration Configuration)
  8         {
  9             this._Configuration = Configuration;
 10         }
 11 
 12         /// <summary>
 13         ///Test page
 14         /// </summary>
 15         /// <returns></returns>
 16         public IActionResult Index()
 17         {
 18             return View();
 19         }
 20 
 21         /// <summary>
 22         ///Verify login
 23         /// </summary>
 24         /// <param name="userAccount"></param>
 25         /// <param name="userPwd"></param>
 26         /// <returns></returns>
 27         [HttpPost]
 28         public IActionResult CheckLogin(string userAccount, string userPwd)
 29         {
 30 
 31             if (userAccount == "admin" && userPwd == "123456")
 32             {
 33 
 34                 string AccessTokenKey = _Configuration["AccessTokenKey"];
 35                 string RefreshTokenKey = _Configuration["RefreshTokenKey"];
 36 
 37                 //1. Check the userId in the database first
 38                 string userId = "001";
 39 
 40                 //2. Generate accessToken
 41                 //Expiration time (the following indicates the expiration time of 5 minutes after signature. 20s is set here for demonstration)
 42                 double exp = (DateTime.UtcNow.AddSeconds(20) - new DateTime(1970, 1, 1)).TotalSeconds;
 43                 var payload = new Dictionary<string, object>
 44                     {
 45                          {"userId", userId },
 46                          {"userAccount", userAccount },
 47                          {"exp",exp }
 48                     };
 49                 var accessToken = JWTHelp.JWTJiaM(payload, AccessTokenKey);
 50 
 51                 //3. Generate refreshToken
 52                 //Expiration time (it can not be set, and the following indicates 2-day expiration)
 53                 var expireTime = DateTime.Now.AddDays(2);
 54                 double exp2 = (expireTime - new DateTime(1970, 1, 1)).TotalSeconds;
 55                 var payload2 = new Dictionary<string, object>
 56                     {
 57                          {"userId", userId },
 58                          {"userAccount", userAccount },
 59                          {"exp",exp2 }
 60                     };
 61                 var refreshToken = JWTHelp.JWTJiaM(payload2, RefreshTokenKey);
 62 
 63                 //4. Store the original information of the generated refreshToken in the database / Redis (temporarily stored in a global variable here)
 64                 //First query whether there is one, update if there is one, and add if there is no one
 65                 var RefreshTokenItem = rTokenList.Where(u => u.userId == userId).FirstOrDefault();
 66                 if (RefreshTokenItem == null)
 67                 {
 68                     RefreshToken rItem = new RefreshToken()
 69                     {
 70                         id = Guid.NewGuid().ToString("N"),
 71                         userId = userId,
 72                         expire = expireTime,
 73                         Token = refreshToken
 74                     };
 75                     rTokenList.Add(rItem);
 76 
 77                 }
 78                 else
 79                 {
 80                     RefreshTokenItem.Token = refreshToken;
 81                     RefreshTokenItem.expire = expireTime;   //To match the expiration time generated earlier
 82 
 83                 }
 84                 return Json(new
 85                 {
 86                     status = "ok",
 87                     msg="Login succeeded",
 88                     data = new
 89                     {
 90                         accessToken,
 91                         refreshToken
 92                     }
 93                 });
 94             }
 95             else
 96             {
 97                 return Json(new
 98                 {
 99                     status = "error",
100                     msg = "Login failed",
101                     data = new { }
102                 });
103             }
104 
105 
106         }
107 
108 
109 
110         /// <summary>
111         ///Get system information interface
112         /// </summary>
113         /// <returns></returns>
114         [TypeFilter(typeof(JwtCheck2))]
115         public IActionResult GetMsg()
116         {
117             string msg = "windows10";
118             return Json(new { status = "ok", msg = msg });
119         }
120 
121 
122 
123         /// <summary>
124         ///Update access token (also update refresh token)
125         /// </summary>
126         /// <returns></returns>
127         public IActionResult UpdateAccessToken(string accessToken, string refreshToken)
128         {
129 
130             string AccessTokenKey = _Configuration["AccessTokenKey"];
131             string RefreshTokenKey = _Configuration["RefreshTokenKey"];
132 
133             //1. First verify the physical legitimacy of refreshToken through pure code
134             var result = JWTHelp.JWTJieM(refreshToken, _Configuration["RefreshTokenKey"]);
135             if (result== "expired"|| result == "invalid" || result == "error")
136             {
137                 return Json(new { status = "error", data = "" });
138             }
139 
140             //2. Parse userId and other data from the accessToken (even if the accessToken has expired, it can still be parsed)
141             JwtData myJwtData = JsonConvert.DeserializeObject<JwtData>(this.Base64UrlDecode(accessToken.Split('.')[1]));
142 
143             //3. Take userId, refreshtoken and current time to look up data in refreshtoken table
144             var rTokenItem = rTokenList.Where(u => u.userId == myJwtData.userId && u.Token == refreshToken && u.expire > DateTime.Now).FirstOrDefault();
145             if (rTokenItem==null)
146             {
147                 return Json(new { status = "error", data = "" });
148             }
149 
150             //4. Regenerate the accessToken and refreshtoken and write them into the refreshtoken table
151             //4.1. Generate accessToken
152             //Expiration time (the following indicates the expiration time of 5 minutes after signature. 20s is set here for demonstration)
153             double exp = (DateTime.UtcNow.AddSeconds(20) - new DateTime(1970, 1, 1)).TotalSeconds;
154             var payload = new Dictionary<string, object>
155                     {
156                          {"userId", myJwtData.userId },
157                          {"userAccount", myJwtData.userAccount },
158                          {"exp",exp }
159                     };
160             var MyAccessToken = JWTHelp.JWTJiaM(payload, AccessTokenKey);
161 
162             //4.2. Generate refreshToken
163             //Expiration time (can not be set. The following indicates that it expires 2 days after signing)
164             var expireTime = DateTime.Now.AddDays(2);
165             double exp2 = (expireTime - new DateTime(1970, 1, 1)).TotalSeconds;
166             var payload2 = new Dictionary<string, object>
167                     {
168                          {"userId", myJwtData.userId },
169                          {"userAccount", myJwtData.userAccount },
170                          {"exp",exp2 }
171                     };
172             var MyRefreshToken = JWTHelp.JWTJiaM(payload2, RefreshTokenKey);
173 
174             //4.3 update refreshToken table
175             rTokenItem.Token = MyRefreshToken;
176             rTokenItem.expire = expireTime;
177 
178 
179             //5. Return double Token
180             return Json(new
181             {
182                 status = "ok",
183                 data = new
184                 {
185                     accessToken= MyAccessToken,
186                     refreshToken= MyRefreshToken
187                 }
188             });
189 
190         }
191 
192 
193         /// <summary>
194         ///Base64 decoding
195         /// </summary>
196         /// <param name="base64UrlStr"></param>
197         /// <returns></returns>
198 
199         public string Base64UrlDecode(string base64UrlStr)
200         {
201             base64UrlStr = base64UrlStr.Replace('-', '+').Replace('_', '/');
202             switch (base64UrlStr.Length % 4)
203             {
204                 case 2:
205                     base64UrlStr += "==";
206                     break;
207                 case 3:
208                     base64UrlStr += "=";
209                     break;
210             }
211             var bytes = Convert.FromBase64String(base64UrlStr);
212             return Encoding.UTF8.GetString(bytes);
213         }
214      
215 
216     }
Related interface

Server side code 2:

1  /// <summary>
 2     ///Encryption and decryption of Jwt
 3     ///Note: encryption and encryption use one key
 4     ///Dependent assembly: [JWT]
 5     /// </summary>
 6     public class JWTHelp
 7     {
 8 
 9         /// <summary>
10         ///JWT encryption algorithm
11         /// </summary>
12         ///< param name = "payload" > load part, which stores the information used < / param >
13         ///< param name = "secret" > key < / param >
14         ///< param name = "extraheaders" > stores additional header information. If it is not needed, it can not be transferred to < / param >
15         /// <returns></returns>
16         public static string JWTJiaM(IDictionary<string, object> payload, string secret, IDictionary<string, object> extraHeaders = null)
17         {
18             IJwtAlgorithm algorithm = new HMACSHA256Algorithm();
19             IJsonSerializer serializer = new JsonNetSerializer();
20             IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
21             IJwtEncoder encoder = new JwtEncoder(algorithm, serializer, urlEncoder);
22             var token = encoder.Encode(payload, secret);
23             return token;
24         }
25 
26         /// <summary>
27         ///JWT decryption algorithm
28         /// </summary>
29         ///< param name = "token" > token string to be decrypted < / param >
30         ///< param name = "secret" > key < / param >
31         /// <returns></returns>
32         public static string JWTJieM(string token, string secret)
33         {
34             try
35             {
36                 IJsonSerializer serializer = new JsonNetSerializer();
37                 IDateTimeProvider provider = new UtcDateTimeProvider();
38                 IJwtValidator validator = new JwtValidator(serializer, provider);
39                 IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
40                 IJwtDecoder decoder = new JwtDecoder(serializer, validator, urlEncoder);
41                 
42                 var json = decoder.Decode(token, secret, true);
43                 //If the verification passes, the decrypted string is returned
44                 return json;
45             }
46             catch (TokenExpiredException)
47             {
48                 //Indicates expiration
49                 return "expired";
50             }
51             catch (SignatureVerificationException)
52             {
53                 //Indicates that the verification fails
54                 return "invalid";
55             }
56             catch (Exception)
57             {
58                 return "error";
59             }
60         }
61 
62 
63     }
JWT Help class

Server side code 3:

1  public class RefreshToken
 2     {
 3         //Primary key
 4         public string id { get; set; }
 5         //User number
 6         public string userId { get; set; }
 7         //refreshToken
 8         public string Token { get; set; }
 9         //Expiration time
10         public DateTime expire { get; set; }
11     }
12 }
13 
14    public class JwtData
15     {
16         public DateTime expire { get; set; }  //Represents the expiration time
17 
18         public string userId { get; set; }  
19 
20         public string userAccount { get; set; }
21     }

Entity class

Filter code:

/// <summary>
    ///Bearer authentication, return error in ajax
    ///Verify the legitimacy of the access token
    /// </summary>
    public class JwtCheck2 : ActionFilterAttribute
    {

        private IConfiguration _configuration;
        public JwtCheck2(IConfiguration configuration)
        {
            _configuration = configuration;
        }

        /// <summary>
        ///Execute before action execution
        /// </summary>
        /// <param name="context"></param>
        public override void OnActionExecuting(ActionExecutingContext context)
        {
            //1. Judge whether calibration is required
            var isSkip = context.ActionDescriptor.EndpointMetadata.Any(x => x.GetType() == typeof(SkipAttribute));
            if (isSkip == false)
            {
                //2. Determine what the request is (ajax or non ajax)
                var actionContext = context.HttpContext;
                if (IsAjaxRequest(actionContext.Request))
                {
                    //Indicates ajax
                    var token = context.HttpContext.Request.Headers["Authorization"].ToString();    //ajax request coming
                    string pattern = "^Bearer (.*?)$";
                    if (!Regex.IsMatch(token, pattern))
                    {
                        context.Result = new ContentResult { StatusCode = 401, Content = "token Wrong format!Format is:Bearer {token}" };
                        return;
                    }
                    token = Regex.Match(token, pattern).Groups[1]?.ToString();
                    if (token == "null" || string.IsNullOrEmpty(token))
                    {
                        context.Result = new ContentResult { StatusCode = 401, Content = "token Cannot be empty" };
                        return;
                    }
                    //Verify the correctness of auth
                    var result = JWTHelp.JWTJieM(token, _configuration["AccessTokenKey"]);
                    if (result == "expired")
                    {
                        context.Result = new ContentResult { StatusCode = 401, Content = "expired" };
                        return;
                    }
                    else if (result == "invalid")
                    {
                        context.Result = new ContentResult { StatusCode = 401, Content = "invalid" };
                        return;
                    }
                    else if (result == "error")
                    {
                        context.Result = new ContentResult { StatusCode = 401, Content = "error" };
                        return;
                    }
                    else
                    {
                        //Indicates that the verification has passed, which is used to transfer values to the controller
                        context.RouteData.Values.Add("auth", result);
                    }

                }
                else
                {
                    //Indicates that it is a non ajax request, then auth splicing is passed in the parameter
                    context.Result = new RedirectResult("/Home/NoPerIndex?reason=null");
                    return;
                }
            }

        }


        /// <summary>
        ///Determine whether the request is an ajax request
        /// </summary>
        /// <param name="request"></param>
        /// <returns></returns>
        private bool IsAjaxRequest(HttpRequest request)
        {
            string header = request.Headers["X-Requested-With"];
            return "XMLHttpRequest".Equals(header);
        }
    }

3, Testing

  Set the expiration time of the accessToken to 20s, click login authorization, wait for 20s, and then click the get information button to still get information, seamlessly connect, and update the double tokens.

 

 

Posted by titel on Wed, 01 Sep 2021 11:19:21 -0700