Handle Refresh Token Using ASP.NET Core 2.0 And JSON Web Token

Posted Frank_520

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Handle Refresh Token Using ASP.NET Core 2.0 And JSON Web Token相关的知识,希望对你有一定的参考价值。

来源:
   https://www.c-sharpcorner.com/article/handle-refresh-token-using-asp-net-core-2-0-and-json-web-token/

 

In this article , you will learn how to deal with the refresh token when you use jwt (JSON Web Token) as your access_token.

Backgroud

Many people choose jwt as their access_token when the client sends a request to the Resource Server.

 

However, before the client sends a request to the Resource Server, the client needs to get the access_token from the Authorization Server. After receiving and storing the access_token, the client uses access_token to send a request to the Resource Server.

 

But as all we know, the expired time for a jwt is too short. And we do not require the users to pass their name and password once more! At this time, the refresh_token provides a vary convenient way that we can use to exchange a new access_token.

 

The normal way may be as per the following.

技术图片

I will use ASP.NET Core 2.0 to show how to do this work.

 

Requirement first

 

You need to install the SDK of .NET Core 2.0 preview and the VS 2017 preview.

 

Now, let‘s begin!

 

First of all, building a Resource Server

 

Creating an ASP.NET Core Web API project.

 

Edit the Program class to specify the url when we visit the API.

 

 1 public class Program  
 2 
 3 {  
 4 
 5 public static void Main(string[] args)  
 6 
 7     {  
 8 
 9         BuildWebHost(args).Run();  
10 
11     }  
12 
13 public static IWebHost BuildWebHost(string[] args) =>  
14 
15         WebHost.CreateDefaultBuilder(args)  
16 
17             .UseStartup<Startup>()  
18 
19             .UseUrls("http://localhost:5002")  
20 
21             .Build();  
22 
23 }  

 

Add a private method in Startup class which configures the jwt authorization. There are some differences when we use the lower version of .NET Core SDK.

 

 1 public void ConfigureJwtAuthService(IServiceCollection services)  
 2 
 3 {  
 4 
 5 var audienceConfig = Configuration.GetSection("Audience");  
 6 
 7 var symmetricKeyAsBase64 = audienceConfig["Secret"];  
 8 
 9 var keyByteArray = Encoding.ASCII.GetBytes(symmetricKeyAsBase64);  
10 
11 var signingKey = new SymmetricSecurityKey(keyByteArray);  
12 
13 var tokenValidationParameters = new TokenValidationParameters  
14 
15     {  
16 
17 // The signing key must match!  
18 
19         ValidateIssuerSigningKey = true,  
20 
21         IssuerSigningKey = signingKey,  
22 
23 // Validate the JWT Issuer (iss) claim  
24 
25         ValidateIssuer = true,  
26 
27         ValidIssuer = audienceConfig["Iss"],  
28 
29 // Validate the JWT Audience (aud) claim  
30 
31         ValidateAudience = true,  
32 
33         ValidAudience = audienceConfig["Aud"],  
34 
35 // Validate the token expiry  
36 
37         ValidateLifetime = true,  
38 
39         ClockSkew = TimeSpan.Zero  
40 
41     };  
42 
43     services.AddAuthentication(options =>  
44 
45     {  
46 
47         options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;  
48 
49         options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;  
50 
51     })  
52 
53     .AddJwtBearerAuthentication(o =>  
54 
55     {  
56 
57         o.TokenValidationParameters = tokenValidationParameters;  
58 
59     });  
60 
61 }  

 

And, we need to use this method in the ConfigureServices method.

 

 1 public void ConfigureServices(IServiceCollection services)  
 2 
 3 {  
 4 
 5 //configure the jwt   
 6 
 7     ConfigureJwtAuthService(services);  
 8 
 9     services.AddMvc();  
10 }  

 

Do not forget touse the authentication in the Configure method.

 

 

 

 

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)  

{  

    loggerFactory.AddConsole(Configuration.GetSection("Logging"));  

    loggerFactory.AddDebug();  

//use the authentication  

    app.UseAuthentication();  

    app.UseMvc();  

}  

 

 

 

 

 

The last step of our Resource Server is to edit the ValueController so that we can use the authentication when we visit this API.

 

 

 
[Route("api/[controller]")]  

public class ValuesController : Controller  

{  

// GET api/values/5  

    [HttpGet("{id}")]  

    [Authorize]  

public string Get(int id)  

    {  

return "visit by jwt auth";  

    }          

}  
 

  

Turn to the Authentication Server

 

How to design the authentication?

 

Here is my point of view,

 

When the client uses the parameters to get an access_token , the client needs to pass the parameters in the querystring are as follow:

 

Parameter Value
grant_type the value must be password
client_id the client_id is assigned by manager
client_secret the client_secret is assigned by manager
username the name of the user
password the password of the user


 When the client use the parameters to refresh a expired access_token , the client need to pass the parameters in the querystring are as follow,

Parameter Value
grant_type the value must be refresh_token
client_id the client_id is assigned by manager
client_secret the client_secret is assigned by manager
refresh_token after authentication the server will return a refresh_token

 

Here is the implementation!

 

Create a new ASP.NET Core project and a new controller named TokenController.

 

  1 [Route("api/token")]  
  2 
  3 public class TokenController : Controller  
  4 
  5 {  
  6 
  7 //some config in the appsettings.json  
  8 
  9 private IOptions<Audience> _settings;  
 10 
 11 //repository to handler the sqlite database  
 12 
 13 private IRTokenRepository _repo;  
 14 
 15 public TokenController(IOptions<Audience> settings, IRTokenRepository repo)  
 16 
 17     {  
 18 
 19 this._settings = settings;  
 20 
 21 this._repo = repo;  
 22 
 23     }  
 24 
 25     [HttpGet("auth")]  
 26 
 27 public IActionResult Auth([FromQuery]Parameters parameters)  
 28 
 29     {  
 30 
 31 if (parameters == null)  
 32 
 33         {  
 34 
 35 return Json(new ResponseData  
 36 
 37             {  
 38 
 39                 Code = "901",  
 40 
 41                 Message = "null of parameters",  
 42 
 43                 Data = null  
 44 
 45             });  
 46 
 47         }  
 48 
 49 if (parameters.grant_type == "password")  
 50 
 51         {  
 52 
 53 return Json(DoPassword(parameters));  
 54 
 55         }  
 56 
 57 else if (parameters.grant_type == "refresh_token")  
 58 
 59         {  
 60 
 61 return Json(DoRefreshToken(parameters));  
 62 
 63         }  
 64 
 65 else  
 66 
 67         {  
 68 
 69 return Json(new ResponseData  
 70 
 71             {  
 72 
 73                 Code = "904",  
 74 
 75                 Message = "bad request",  
 76 
 77                 Data = null  
 78 
 79             });  
 80 
 81         }  
 82 
 83     }  
 84 
 85 //scenario 1 : get the access-token by username and password  
 86 
 87 private ResponseData DoPassword(Parameters parameters)  
 88 
 89     {  
 90 
 91 //validate the client_id/client_secret/username/passwo  
 92 
 93 var isValidated = UserInfo.GetAllUsers().Any(x => x.ClientId == parameters.client_id  
 94 
 95                                 && x.ClientSecret == parameters.client_secret  
 96 
 97                                 && x.UserName == parameters.username  
 98 
 99                                 && x.Password == parameters.password);  
100 
101 if (!isValidated)  
102 
103         {  
104 
105 return new ResponseData  
106 
107             {  
108 
109                 Code = "902",  
110 
111                 Message = "invalid user infomation",  
112 
113                 Data = null  
114 
115             };  
116 
117         }  
118 
119 var refresh_token = Guid.NewGuid().ToString().Replace("-", "");  
120 
121 var rToken = new RToken  
122 
123         {  
124 
125             ClientId = parameters.client_id,  
126 
127             RefreshToken = refresh_token,  
128 
129             Id = Guid.NewGuid().ToString(),  
130 
131             IsStop = 0  
132 
133         };  
134 
135 //store the refresh_token   
136 
137 if (_repo.AddToken(rToken))  
138 
139         {  
140 
141 return new ResponseData  
142 
143             {  
144 
145                 Code = "999",  
146 
147                 Message = "OK",  
148 
149                 Data = GetJwt(parameters.client_id, refresh_token)  
150 
151             };  
152 
153         }  
154 
155 else  
156 
157         {  
158 
159 return new ResponseData  
160 
161             {  
162 
163                 Code = "909",  
164 
165                 Message = "can not add token to database",  
166 
167                 Data = null  
168 
169             };  
170 
171         }  
172 
173     }  
174 
175 //scenario 2 : get the access_token by refresh_token  
176 
177 private ResponseData DoRefreshToken(Parameters parameters)  
178 
179     {  
180 
181 var token = _repo.GetToken(parameters.refresh_token, parameters.client_id);  
182 
183 if (token == null)  
184 
185         {  
186 
187 return new ResponseData  
188 
189             {  
190 
191                 Code = "905",  
192 
193                 Message = "can not refresh token",  
194 
195                 Data = null  
196 
197             };  
198 
199         }  
200 
201 if (token.IsStop == 1)  
202 
203         {  
204 
205 return new ResponseData  
206 
207             {  
208 
209                 Code = "906",  
210 
211                 Message = "refresh token has expired",  
212 
213                 Data = null  
214 
215             };  
216 
217         }  
218 
219 var refresh_token = Guid.NewGuid().ToString().Replace("-", "");  
220 
221         token.IsStop = 1;  
222 
223 //expire the old refresh_token and add a new refresh_token  
224 
225 var updateFlag = _repo.ExpireToken(token);  
226 
227 var addFlag = _repo.AddToken(new RToken  
228 
229         {  
230 
231             ClientId = parameters.client_id,  
232 
233             RefreshToken = refresh_token,  
234 
235             Id = Guid.NewGuid().ToString(),  
236 
237             IsStop = 0  
238 
239         });  
240 
241 if (updateFlag && addFlag)  
242 
243         {  
244 
245 return new ResponseData  
246 
247             {  
248 
249                 Code = "999",  
250 
251                 Message = "OK",  
252 
253                 Data = GetJwt(parameters.client_id, refresh_token)  
254 
255             };  
256 
257         }  
258 
259 else  
260 
261         {  
262 
263 return new ResponseData  
264 
265             {  
266 
267                 Code = "910",  
268 
269                 Message = "can not expire token or a new token",  
270 
271                 Data = null  
272 
273             };  
274 
275         }  
276 
277     }  
278 
279 //get the jwt token   
280 
281 private string GetJwt(string client_id, string refresh_token)  
282 
283     {  
284 
285 var now = DateTime.UtcNow;  
286 
287 var claims = new Claim[]  
288 
289         {  
290 
291 new Claim(JwtRegisteredClaimNames.Sub, client_id),  
292 
293 new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),  
294 
295 new Claim(JwtRegisteredClaimNames.Iat, now.ToUniversalTime().ToString(), ClaimValueTypes.Integer64)  
296 
297         };  
298 
299 var symmetricKeyAsBase64 = _settings.Value.Secret;  
300 
301 var keyByteArray = Encoding.ASCII.GetBytes(symmetricKeyAsBase64);  
302 
303 var signingKey = new SymmetricSecurityKey(keyByteArray);  
304 
305 var jwt = new JwtSecurityToken(  
306 
307             issuer: _settings.Value.Iss,  
308 
309             audience: _settings.Value.Aud,  
310 
311             claims: claims,  
312 
313             notBefore: now,  
314 
315             expires: now.Add(TimeSpan.FromMinutes(2)),  
316 
317             signingCredentials: new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256));  
318 
319 var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);  
320 
321 var response = new  
322 
323         {  
324 
325             access_token = encodedJwt,  
326 
327             expires_in = (int)TimeSpan.FromMinutes(2).TotalSeconds,  
328 
329             refresh_token = refresh_token,  
330 
331         };  
332 
333 return JsonConvert.SerializeObject(response, new JsonSerializerSettings { Formatting = Formatting.Indented });  
334 
335     }  
336 
337 }  

 

 

 

 

Both above two scenarios only use one action , because the parameters are similar.

 

When the grant_type is password ,we will create a refresh_token and store this refresh_token to the sqlite database. And return the jwt toekn to the client.

 

When the grant_type is refresh_token ,we will expire or delete the old refresh_token which belongs to this client_id and store a new refresh_toekn to the sqlite database. And return the new jwt toekn to the client.

 

Note

I use a GUID as my refresh_token , because GUID is more easier to generate and manager , you can use a more complex value as the refresh token.

 

At last , Create a console app to test the refresh token.

 

 

 
class Program  

{  

static void Main(string[] args)  

    {  

        HttpClient _client = new HttpClient();  

        _client.DefaultRequestHeaders.Clear();  

        Refresh(_client);  

        Console.Read();  

    }  
  
private static void Refresh(HttpClient _client)  

    {  

var client_id = "100";  

var client_secret = "888";  

var username = "Member";  

var password = "123";  

var asUrl = $"http://localhost:5001/api/token/auth?grant_type=password&client_id={client_id}&client_secret={client_secret}&username={username}&password={password}";  

        Console.WriteLine("begin authorizing:");  

        HttpResponseMessage asMsg = _client.GetAsync(asUrl).Result;  

        string result = asMsg.Content.ReadAsStringAsync().Result;  

var responseData = JsonConvert.DeserializeObject<ResponseData>(result);  

if (responseData.Code != "999")  

        {  

            Console.WriteLine("authorizing fail");  

return;  

        }  

var token = JsonConvert.DeserializeObject<Token>(responseData.Data);  

        Console.WriteLine("authorizing successfully");              

        Console.WriteLine($"the response of authorizing {result}");              

        Console.WriteLine("sleep 2min to make the token expire!!!");  

        System.Threading.Thread.Sleep(TimeSpan.FromMinutes(2));  

        Console.WriteLine("begin to request the resouce server");  

var rsUrl = "http://localhost:5002/api/values/1";  

        _client.DefaultRequestHeaders.Add("Authorization", "Bearer " + token.access_token);  

        HttpResponseMessage rsMsg = _client.GetAsync(rsUrl).Result;  

        Console.WriteLine("result of requesting the resouce server");  

        Console.WriteLine(rsMsg.StatusCode);  

        Console.WriteLine(rsMsg.Content.ReadAsStringAsync().Result);  

//refresh the token  

if (rsMsg.StatusCode == HttpStatusCode.Unauthorized)  

        {  

            Console.WriteLine("begin to refresh token");  

var refresh_token = token.refresh_token;  

            asUrl = $"http://localhost:5001/api/token/auth?grant_type=refresh_token&client_id={client_id}&client_secret={client_secret}&refresh_token={refresh_token}";  

            HttpResponseMessage asMsgNew = _client.GetAsync(asUrl).Result;  

            string resultNew = asMsgNew.Content.ReadAsStringAsync().Result;  

var responseDataNew = JsonConvert.DeserializeObject<ResponseData>(resultNew);  

if (responseDataNew.Code != "999")  

            {  

                Console.WriteLine("refresh token fail");  

return;  

            }  

            Token tokenNew = JsonConvert.DeserializeObject<Token>(responseDataNew.Data);  

            Console.WriteLine("refresh token successful");  

            Console.WriteLine(asMsg.StatusCode);  

            Console.WriteLine($"the response of refresh token {resultNew}");  

            Console.WriteLine("requset resource server again");  

            _client.DefaultRequestHeaders.Clear();  

            _client.DefaultRequestHeaders.Add("Authorization", "Bearer " + tokenNew.access_token);  

            HttpResponseMessage rsMsgNew = _client.GetAsync("http://localhost:5002/api/values/1").Result;  

            Console.WriteLine("the response of resource server");  

            Console.WriteLine(rsMsgNew.StatusCode);  

            Console.WriteLine(rsMsgNew.Content.ReadAsStringAsync().Result);  

        }  

    }  

}  

 

 

We should pay attention to the request of the Resource Server!

 

We must add a HTTP header when we send a HTTP request : `Authorization:Bearer token`

 

Now , using the dotnet CLI command to run our three projects.

 

Here is the screenshot of the runninng result.

技术图片

Note

 

  • In the console app, I do not store the access_token and the refresh_token, I just used them once . You should store them in your project ,such as the web app, you can store them in localstorage.
  • When the access_token is expired , the client should remove the expired access_toekn and because the short time will cause the token expired , we do not need to worry about the leakage of the token !

Summary

 

This article introduced an easy way to handle the refresh_token when you use jwt. Hope this will help you to understand how to deal with the tokens.

以上是关于Handle Refresh Token Using ASP.NET Core 2.0 And JSON Web Token的主要内容,如果未能解决你的问题,请参考以下文章

为啥我们不应该使用 access_token 作为 refresh_token

refresh token axios

既生瑜何生亮 access_token VS refresh_token

通过 Keycloak 中的 refresh_token 刷新 access_token

Spring Cloud微服务安全实战_5-5_refresh token失效处理

JWT refresh_token 包含啥?