Hot Chocolate GraphQL Custom Authentication Series Using Pure Code First Technique - Part4 - Refresh Token
Part3 we had enabled the JWT token validation and discussed different authorization techniques as well. In this article, we will understand Refresh Token creation and its usage.
When To Use Refresh Token:
A refresh token is a unique random encrypted string. On the expiration of the JWT auth access token, instead of showing a login page to the user, we can make the user authenticated immediately using the refresh token. By using refresh token we can fetch new user access tokens from the server without any user credentials.
Generate Refresh Token:
In 'AuthLogic.cs' file add a new private method like 'GenerateRefreshToken()'.
Logics/AuthLogics.cs:
private string GenerateRefreshToken() { var randomNumber = new byte[32]; using (var generator = RandomNumberGenerator.Create()) { generator.GetBytes(randomNumber); return Convert.ToBase64String(randomNumber); } }
- Here using 'System.Security.Cryptography.RandomNumberGenerator' generated refresh token of length 32bytes.
Models/TokenResponseModel.cs:
namespace GraphQL.PureCodeFirst.Auth.Models { public class TokenResponseModel { // Message property helps to serve validation errors to client public string Message{get;set;} public string AccessToken { get; set; } public string RefreshToken { get; set; } } }Let's update our 'Login()' method to return 'TokenResponseModel' as output.
Logics/AuthLogic.cs:
public TokenResponseModel Login(LoginInputType loginInput) { var result = new TokenResponseModel{Message = "Success"}; if (string.IsNullOrEmpty(loginInput.Email) || string.IsNullOrEmpty(loginInput.Password)) { result.Message = "Invalid Credentials"; return result; } var user = _authContext.User.Where(_ => _.EmailAddress == loginInput.Email).FirstOrDefault(); if (user == null) { result.Message = "Invalid Credentials"; return result; } if (!ValidatePasswordHash(loginInput.Password, user.Password)) { result.Message = "Invalid Credentials"; return result; } var roles = _authContext.UserRoles.Where(_ => _.UserId == user.UserId).ToList(); result.AccessToken = GetJWTAuthKey(user, roles); result.RefreshToken = GenerateRefreshToken(); user.RefreshToken = result.RefreshToken; user.RefershTokenExpiration = DateTime.Now.AddDays(7); _authContext.SaveChanges(); return result; }
- (Line: 3) Initialized 'TokenResponseModel' object.
- (Line: 28) Generating the Refresh Token.
- (Line: 30-32) Saving refresh token and its expiration to 'User' table.
Logics/IAuthLogic.cs:
TokenResponseModel Login(LoginInputType loginInput);Also, update our 'Login' resolver in the 'MutationResolver.cs' file.
Resolvers/MutationResolver.cs:
public TokenResponseModel Login([Service] IAuthLogic authLogic,LoginInputType loginInput) { return authLogic.Login(loginInput); }Let's frame mutation for login.
Mutation:
mutation($MyLogin:LoginInputTypeInput){ login(loginInput:$MyLogin){ message, accessToken, refreshToken } }Variable:
{ "MyLogin":{ "email":"test@test.com", "password":"Test@1234" } }
Refresh Token Working Flow:
Let's understand the refresh token working flow:
- On the expiration of our JWT access token, the client app should renew the access token by using the refresh token.
- So the client app will send the refresh token and expired access token to the server.
- The server application decodes our expired access token and fetches the claims inside of it.
- Now using email claim and refresh token we should query the 'User' table.
- If a user record founds then we will create a new access token and a new refresh token as well.
- The best practice is to use a refresh token only once, always try to create a new refresh token along with a JWT access token.
Implement Access Token Renewal Logic Using Refresh Token:
Let's create a private method like 'GetClaimsFromExpiredToken()'. This method contains logic to fetch claims from the expired token.
Logics/AuthLogic.cs:
private ClaimsPrincipal GetClaimsFromExpiredToken(string accessToken) { var tokenValidationParameter = new TokenValidationParameters { ValidIssuer = _tokenSettings.Issuer, ValidateIssuer = true, ValidAudience = _tokenSettings.Audience, ValidateAudience = true, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_tokenSettings.Key)), ValidateLifetime = false // ignore expiration }; var jwtHandler = new JwtSecurityTokenHandler(); var principal = jwtHandler.ValidateToken(accessToken, tokenValidationParameter, out SecurityToken securityToken); var jwtScurityToken = securityToken as JwtSecurityToken; if (jwtScurityToken == null) { return null; } return principal; }
- (Line: 3-11) Initialize 'TokenValidationParameters' object. The 'ValidateLifetime' must set to false because here our target to decrypt the expired token.
- (Line: 13-14) Using the 'JwtSecurityTokenHandler' instance we will validate our expired access token and finally generates claims.
InputTypes/RenewTokenInputType.cs:
namespace GraphQL.PureCodeFirst.InputTypes { public class RenewTokenInputType { public string AccessToken { get; set; } public string RefreshToken { get; set; } } }Now let's create a public method like 'RenewAccessToken()' that contains all core logic to generate a new jwt access token and refresh token.
Logics/AuthLogic.cs:
public TokenResponseModel RenewAccessToken(RenewTokenInputType renewToken) { var result = new TokenResponseModel { Message = "Success" }; ClaimsPrincipal principal = GetClaimsFromExpiredToken(renewToken.AccessToken); if (principal == null) { result.Message = "Invalid Token"; return result; } string email = principal.Claims.Where(_ => _.Type == "Email").Select(_ => _.Value).FirstOrDefault(); if (string.IsNullOrEmpty(email)) { result.Message = "Invalid Token"; return result; } var user = _authContext.User .Where(_ => _.EmailAddress == email && _.RefreshToken == renewToken.RefreshToken && _.RefershTokenExpiration > DateTime.Now).FirstOrDefault(); if (user == null) { result.Message = "Invalid Token"; return result; } var userRoles = _authContext.UserRoles.Where(_ => _.UserId == user.UserId).ToList(); result.AccessToken = GetJWTAuthKey(user, userRoles); result.RefreshToken = GenerateRefreshToken(); user.RefreshToken = result.RefreshToken; user.RefershTokenExpiration = DateTime.Now.AddDays(7); _authContext.SaveChanges(); return result; }
- (Line: 5) Trying to fetch the claims from the expired jwt access token.
- (Line: 12) Trying to fetch user email from the user claims.
- (Line: 19-25) Fetching a user record from the database by filtering with email, refresh token, and refresh token expiration values.
- (Line: 27) Fetching roles from the 'UserRoles' table.
- (Line: 29) Fetching new jwt access token.
- (Line: 31) Generating the new refresh token.
- (Line: 33-34) Saving refresh token and refresh token expiration new values to the database.
Logics/IAuthLogic.cs:
TokenResponseModel RenewAccessToken(RenewTokenInputType renewToken);Let's create a resolver method for the renewal of tokens in the 'MutationResolver.cs' file.
Resolvers/MutationResolver.cs:
public TokenResponseModel RenewAccessToken([Service] IAuthLogic authLogic,RenewTokenInputType renewToken) { return authLogic.RenewAccessToken(renewToken); }Let's frame the mutation.
Mutation:
mutation($RenewToken:RenewTokenInputTypeInput){ renewAccessToken(renewToken:$RenewToken){ message, accessToken, refreshToken } }Variable:
{ "RenewToken":{ "accessToken":"your expired access token value", "refreshToken":"refresh token value" } }That's all about the usage of the Refresh Token.
Video Session:
Support Me!
Buy Me A Coffee
PayPal Me
Wrapping Up:
Hopefully, I think this article delivered some useful information on Refresh Token to renew the expired JWT Access Token in the Pure Code First technique in Hot Chocolate GraphQL. I love to have your feedback, suggestions, and better techniques in the comment section below.
Comments
Post a Comment