In Part-1 .Net Core Authentication Using JWT(JSON Web Token), we have discussed step by step implementation about generating authentication token using JWT(JSON Web Token). Now we will discuss the generation of refresh token and using refresh token we will fetch authentication token again on its expiration. This article will be the continuation of Part - 1.
RandomNumberGenerator Instance:
System.Security.Cryptography.RandomNumberGenerator will be used to generate a random number which will be used as a refresh token.
Note: It is not a mandatory approach to use 'System.Security.Cryptography.RandomNumberGenerator'. You can use your own some secured technique to generate a unique token string or you can use GUID.
Generate Refresh Token:
Let's add a private method that returns a random unique key that we can use as a refresh token.
Logic/AccountLogic.cs:
private string GetRefreshToken() { var key = new Byte[32]; using (var refreshTokenGenerator = RandomNumberGenerator.Create()) { refreshTokenGenerator.GetBytes(key); return Convert.ToBase64String(key); } }
- #L4 at this line System.Security.Cryptography.RandomNumberGenerator.Create() is a static method it will create an instance of the default implementation of a cryptographic random number generator that can be used to generate random data.
- #L6 at this line generated random number value copied to 'key' variable as an array of byte data.
- #L7 at this line an array of byte data converted to base64 string format and it will be used as our unique refresh token.
Fetch Refresh Token:
From login endpoint in Part - 1 we returned only Access Token, now we need to push refresh token as well.
As the first step let's update our user table with a new column 'RefreshToken' as below.
The reason behind to store refresh token is to validate it.
Now let's add a new model class that returns an access token and refresh token as below
Models/TokenModel.cs:
namespace JwtApiSample.Models { public class TokenModel { public string Token { get; set; } public string RefreshToken { get; set; } } }
Now let's update the User.cs file with column 'RefreshToken' that added to the User table in the previous step.
Data/Entities/User.cs:
namespace JwtApiSample.Data.Entities { public class User { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string Email { get; set; } public string Password { get; set; } public string PhoneNumber { get; set; } public string RefreshToken { get; set; } } }Now let's update the 'GetAuthenticationToken' method in AccountLogic.cs file that will returns both access token and refresh token as below.
Logic/IAccountLogic.cs:
public interface IAccountLogic { TokenModel GetAuthenticationToken(LoginModel loginModel); }Logic/AccountLogic.cs:
public TokenModel GetAuthenticationToken(LoginModel loginModel) { User currentUser = _myWorldDbContext.User.Where(_ => _.Email.ToLower() == loginModel.Email.ToLower() && _.Password == loginModel.Password).FirstOrDefault(); if (currentUser != null) { var symmetricSecurityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_tokenSettings.Key)); var credentials = new SigningCredentials(symmetricSecurityKey, SecurityAlgorithms.HmacSha256); var userCliams = new Claim[]{ new Claim("email", currentUser.Email), new Claim("phone", currentUser.PhoneNumber), }; var jwtToken = new JwtSecurityToken( issuer: _tokenSettings.Issuer, audience: _tokenSettings.Audience, expires: DateTime.Now.AddMinutes(20), signingCredentials: credentials, claims: userCliams ); string token = new JwtSecurityTokenHandler().WriteToken(jwtToken); string refreshToken = GetRefreshToken(); currentUser.RefreshToken = refreshToken; _myWorldDbContext.SaveChanges(); return new TokenModel { Token = token, RefreshToken = refreshToken }; } return null; }
- #L25 at this line fetching a private method that returns refresh token.
- #L27-28 at these lines saving refresh token to the user table.
- #L31-35 at these lines outputs the TokenModel which holds access token and refresh token.
Controllers/AccountController.cs:
[HttpPost] [Route("login-token")] public IActionResult GetLoginToken(LoginModel model) { var tokenModel = _accountLogic.GetAuthenticationToken(model); if (tokenModel == null) { return NotFound(); } return Ok(tokenModel); }Now test endpoint output as below.
Implement Logic To Generate JWT Token Using RefreshToken:
On the expiration of the JWT token of a user instead of asking the user to enter his credentials for login, we can use refresh token which will regenerate JWT token.
Let's write the logic of refresh token to generate JWT token as follows.
Logic/AccountLogic.cs:
public TokenModel ActivateTokenUsingRefreshToke(TokenModel tokenModel) { var tokenHandler = new JwtSecurityTokenHandler(); var claimsPrincipal = tokenHandler.ValidateToken(tokenModel.Token, new TokenValidationParameters { ValidateIssuer = true, ValidIssuer = _tokenSettings.Issuer, ValidateAudience = true, ValidAudience = _tokenSettings.Audience, ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_tokenSettings.Key)), ValidateLifetime = true }, out SecurityToken validatedToken); var jwtToken = validatedToken as JwtSecurityToken; if (jwtToken == null || !jwtToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256)) { return null; } var email = claimsPrincipal.Claims.Where(_ => _.Type == ClaimTypes.Email).Select(_ => _.Value).FirstOrDefault(); if (string.IsNullOrEmpty(email)) { return null; } var currentUser = _myWorldDbContext.User.Where(_ => _.Email == email && _.RefreshToken == tokenModel.RefreshToken).FirstOrDefault(); if (currentUser == null) { return null; } var symmetricSecurityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_tokenSettings.Key)); var credentials = new SigningCredentials(symmetricSecurityKey, SecurityAlgorithms.HmacSha256); var newJwtToken = new JwtSecurityToken( issuer: _tokenSettings.Issuer, audience: _tokenSettings.Audience, expires: DateTime.Now.AddMinutes(20), signingCredentials: credentials, claims: jwtToken.Claims ); string token = new JwtSecurityTokenHandler().WriteToken(newJwtToken); string refreshToken = GetRefreshToken(); currentUser.RefreshToken = refreshToken; _myWorldDbContext.SaveChanges(); return new TokenModel { Token = token, RefreshToken = refreshToken }; }
- #L1 at this line we declared a new method 'ActivateTokenUsingRefreshToken', it takes input model 'TokenModel' where we will post our refresh token and expired or old Jwt token. This method returns also a TokenModel where it will contain newly generated Jwt Access token and refresh token.
- #L3 at this line we instantiating an object of 'System.IdentityModel.Tokens.Jwt.JwtSecurityHandler'.
- #L5-L14 at these lines we have used JwtSecuityHandler.ValidateToken(string token, TokenValidationParameters, out SecurityToken validatedToken) method and it return type is 'ClaimsPrincipal'. For this method, we pass our expired token as the first input parameter. As a second parameter, we are passing token validation parameters which are exactly similar to our rules used in the token generation endpoint. Finally, on successful validation of our expired token, we can get the parsed token data as 'validatedToken' out variable. It returns user claims data from the token as well.
- #L17 at this line 'Microsoft.IdentityModel.Tokens.SecurityToken' is typecast to 'System.IdentityModel.Tokens.Jwt.JwtSecurityToken' which gives more options on our old token information.
- #L19-21 at these lines checking for token and token algorithm matches as expected or not.
- #L24-28 at these lines fetching email claim, because using this email we going to check user valid or not and existed in our database or not.
- #L30-34 at these lines using our refresh token and email from the claims fetching user from the database. if not found null result will be returned to the endpoint.
- #L36-45 at these lines we can see token generation code similar to what we did in Part - 1 for Jwt token. The only difference here instead of manually adding claims from the database we used claims from old Jwt token. But if you feel like the claims will be updated very frequently then here also add claims from the database manually like in Part - 1.
- #L47 at this line fetching new Jwt token
- #L48 at this line fetching new refresh token
- #L50-51 at these lines we updating the database with a newly generated refresh.
- #L54-58 at these lines we returning our new Jwt token and refresh token as output.
public interface IAccountLogic { // code hidden for display purpose TokenModel ActivateTokenUsingRefreshToke(TokenModel tokenModel); }
RefreshToken EndPoint:
Let's add a new action method endpoint for the refresh token as below.
Controllers/AccountController.cs:
[HttpPost] [Route("activate-token-by-refreshtoken")] public IActionResult ActivateAccessTokenByRefresh(TokenModel refreshToken) { var resultTokenModel = _accountLogic.ActivateTokenUsingRefreshToke(refreshToken); if (refreshToken == null) { return NotFound(); } return Ok(resultTokenModel); }Let's test the endpoint and result shows as below
Refactor AccountLogic Code:
We have some common code in login endpoint and refresh token endpoint where we can remove the redundant code.
Logic/AccountLogic.cs:
using System; using System.Collections.Generic; using System.IdentityModel.Tokens.Jwt; using System.Linq; using System.Security.Claims; using System.Security.Cryptography; using System.Text; using JwtApiSample.Data.Context; using JwtApiSample.Data.Entities; using JwtApiSample.Models; using JwtApiSample.Shared; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; namespace JwtApiSample.Logic { public class AccountLogic : IAccountLogic { private readonly TokenSettings _tokenSettings; private readonly MyWorldDbContext _myWorldDbContext; public AccountLogic( IOptions<TokenSettings> tokenSettings, MyWorldDbContext myWorldDbContext ) { _tokenSettings = tokenSettings.Value; _myWorldDbContext = myWorldDbContext; } public TokenModel GetAuthenticationToken(LoginModel loginModel) { User currentUser = _myWorldDbContext.User.Where(_ => _.Email.ToLower() == loginModel.Email.ToLower() && _.Password == loginModel.Password).FirstOrDefault(); if (currentUser != null) { var symmetricSecurityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_tokenSettings.Key)); var credentials = new SigningCredentials(symmetricSecurityKey, SecurityAlgorithms.HmacSha256); var userCliams = new List<Claim>{ new Claim("email", currentUser.Email), new Claim("phone", currentUser.PhoneNumber), }; return GetTokens(currentUser, userCliams); } return null; } private string GetRefreshToken() { var key = new Byte[32]; using (var refreshTokenGenerator = RandomNumberGenerator.Create()) { refreshTokenGenerator.GetBytes(key); return Convert.ToBase64String(key); } } private TokenModel GetTokens( User currentUser, List<Claim> userClaims ) { var symmetricSecurityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_tokenSettings.Key)); var credentials = new SigningCredentials(symmetricSecurityKey, SecurityAlgorithms.HmacSha256); var newJwtToken = new JwtSecurityToken( issuer: _tokenSettings.Issuer, audience: _tokenSettings.Audience, expires: DateTime.Now.AddMinutes(20), signingCredentials: credentials, claims: userClaims ); string token = new JwtSecurityTokenHandler().WriteToken(newJwtToken); string refreshToken = GetRefreshToken(); currentUser.RefreshToken = refreshToken; _myWorldDbContext.SaveChanges(); return new TokenModel { Token = token, RefreshToken = refreshToken }; } public TokenModel ActivateTokenUsingRefreshToke(TokenModel tokenModel) { var tokenHandler = new JwtSecurityTokenHandler(); var claimsPrincipal = tokenHandler.ValidateToken(tokenModel.Token, new TokenValidationParameters { ValidateIssuer = true, ValidIssuer = _tokenSettings.Issuer, ValidateAudience = true, ValidAudience = _tokenSettings.Audience, ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_tokenSettings.Key)), ValidateLifetime = true }, out SecurityToken validatedToken); var jwtToken = validatedToken as JwtSecurityToken; if (jwtToken == null || !jwtToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256)) { return null; } var email = claimsPrincipal.Claims.Where(_ => _.Type == ClaimTypes.Email).Select(_ => _.Value).FirstOrDefault(); if (string.IsNullOrEmpty(email)) { return null; } var currentUser = _myWorldDbContext.User.Where(_ => _.Email == email && _.RefreshToken == tokenModel.RefreshToken).FirstOrDefault(); if (currentUser == null) { return null; } return GetTokens(currentUser, jwtToken.Claims.ToList()); } } }Successfully we have implemented Jwt token endpoint and refresh token endpoint.
Support Me!
Buy Me A Coffee
PayPal Me
Wrapping Up:
Hopefully, I think this article delivered some useful information about refresh token to regenerate JWT access token in .Net Core application. I love to have your feedback, suggestions, and better techniques in the comment section below.
ActivateTokenUsingRefreshToke line 95 throws Exception:
ReplyDeleteIDX10223: Lifetime validation failed. The token is expired. ValidTo: 'System.DateTime', Current time: 'System.DateTime'.
Hi Denis
DeleteOnce try to override the 'LifetimeValidator' delegate in TokenValidationParameters object
like:
private bool CustomLifetimeValidator(DateTime? notBefore, DateTime? expires, SecurityToken tokenToValidate, TokenValidationParameters @param)
{
if (expires != null)
{
return expires > DateTime.UtcNow;
}
return false;
}
use this custom method in 'TokenValidationParameters.LifetimeValidator'
like:
var claimsPrincipal = tokenHandler.ValidateToken(tokenModel.Token,
new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = _tokenSettings.Issuer,
ValidateAudience = true,
ValidAudience = _tokenSettings.Audience,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_tokenSettings.Key)),
ValidateLifetime = true,
LifetimeValidator = CustomLifetimeValidator
}, out SecurityToken validatedToken);
I have the same issue as Dennis but when I use your example I get the same issue. If I change return expires > DateTime.UtcNow; to return expires < DateTime.UtcNow; It works. Is that correct?
ReplyDeleteHi Naveen, I know this article is a few years old, but I stumbled upon it recently and have been studying your code, which is very interesting to me as I'm just learning about refresh tokens. I know from my research that refresh tokens are intended to live longer than access tokens. But in your implementation, there doesn't seem to be any concept of expiration time with respect to the refresh tokens. Does that mean the user is meant to be able to access the system forever without re-authenticating? In what scenario would a user be forced to re-authenticate, other than through the deletion or modification of the refresh token on the server? Also, one other question -- how is this intended to work if there are multiple client devices per user? It seems the server would need to issue/maintain one refresh token per device? Or am I missing something there? Thanks again for this excellent work.
ReplyDelete