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.