I'm trying to implement token refresh feature in angular 12 and .net core 5.
this is my JWT service registration:
startup.cs:
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.SaveToken = true;
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ClockSkew = TimeSpan.Zero,
ValidAudience = _conf["JWT:ValidAudience"],
ValidIssuer = _conf["JWT:ValidIssuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_conf["JWT:Secret"]))
};
});
And this is my appsettings.json:
"JWT": {
"ValidAudience": "http://localhost:4200",
"ValidIssuer": "http://localhost:4200",
"Secret": "JWTRefreshTokenHIGHsecuredPasswordVVVp1OH7Xzyr",
"TokenValidityInMinutes": 1,
"RefreshTokenValidityInDays": 7
}
after calling login controller a simple access token and a refresh token is sent back to client:
LoginController:
//login codes omitted for simplicity
foreach (var role in userRoles)
{
authClaims.Add(new Claim(ClaimTypes.Role, role));
}
var accessToken = CreateToken(authClaims);
var refreshToken = GenerateRefreshToken();
_ = int.TryParse(_configuration["JWT:RefreshTokenValidityInDays"], out int refreshTokenValidityInDays);
user.RefreshToken = refreshToken;
user.RefreshTokenExpiryTime = DateTime.Now.AddDays(refreshTokenValidityInDays);
CreateToken Method :
private JwtSecurityToken CreateToken(List<Claim> authClaims)
{
var authSecurityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JWT:Secret"]));
_ = int.TryParse(_configuration["JWT:TokenValidityInMinutes"], out int TokenValidityInMinutes);
var expires = DateTime.Now.ToLocalTime().AddMinutes(TokenValidityInMinutes);
var token = new JwtSecurityToken(
issuer: _configuration["JWT:ValidIssuer"],
audience: _configuration["JWT:ValidAudience"],
claims: authClaims,
expires: expires,
signingCredentials: new SigningCredentials(authSecurityKey, SecurityAlgorithms.HmacSha256)
) ;
return token;
}
Now after logging in i have access token and refresh token saved in browser SessionStorage.
Here on client side I've implemented a HTTP INTERCEPTOR to handle 401 unauthorized error. I want to refresh my JWT token when my backend says that token is expired.
auth.interceptor.ts:
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
private isRefreshing = false;
private refreshTokenSubject: BehaviorSubject<any> = new BehaviorSubject<any>(null);
constructor(private tokenService: tokenStorageService, private authService: AuthenticationService) { }
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<Object>> {
debugger;
let authReq = req;
const token = this.tokenService.getToken();
if (token != null) {
authReq = this.addTokenHeader(req, token);
}
return next.handle(authReq).pipe(catchError(error => {
if (error instanceof HttpErrorResponse && !authReq.url.includes('auth/login') && error.status === 401) {
debugger;
return this.handle401Error(authReq, next);
}
return throwError(error);
}));
}
private handle401Error(request: HttpRequest<any>, next: HttpHandler) {
if (!this.isRefreshing) {
this.isRefreshing = true;
this.refreshTokenSubject.next(null);
const token = this.tokenService.getRefreshToken();
const refreshToken=this.tokenService.getRefreshToken();
if (token)
return this.authService.refreshToken(token,refreshToken).pipe(
switchMap((token: any) => {
this.isRefreshing = false;
this.tokenService.saveToken(token.accessToken);
this.refreshTokenSubject.next(token.accessToken);
return next.handle(this.addTokenHeader(request, token.accessToken));
}),
catchError((err) => {
debugger;
this.isRefreshing = false;
//this.tokenService.signOut();
return throwError(err);
})
);
}
return this.refreshTokenSubject.pipe(
filter(token => token !== null),
take(1) switchMap((token) => next.handle(this.addTokenHeader(request, token)))
);
}
private addTokenHeader(request: HttpRequest<any>, token: string) {
return request.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
});
}
}
one minute after login my token gets expired and interceptor calls my refresh-token api.
RefreshToken Controller:
public async Task<object> RefreshToken(tokenModel tokenModel)
{
if (tokenModel == null)
throw new ServiceException("Invalid Token Model");
string accessToken = tokenModel.accessToken;
string refreshToken = tokenModel.refreshToken;
var principal = GetPrincipalsFromExpiredToken(accessToken);
if(principal==null)
{
throw new ServiceException("Invalid access token or refresh token");
}
string username = principal.Identity.Name;
var user = await _userManager.FindByNameAsync(username);
if(user==null || user.RefreshToken!=refreshToken || user.RefreshTokenExpiryTime<=DateTime.Now)
{
throw new ServiceException("Invalid access token or refresh token");
}
var newAccessToken = CreateToken(principal.Claims.ToList());
var newRefreshToken = GenerateRefreshToken();
user.RefreshToken = newRefreshToken;
await _userManager.UpdateAsync(user);
return new
{
accessToken=new JwtSecurityTokenHandler().WriteToken(newAccessToken),
refreshToken=newRefreshToken
};
}
and this is GetPrincipalsFromExpiredToken method:
private ClaimsPrincipal GetPrincipalsFromExpiredToken(string token)
{
var tokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false,
ValidateIssuer = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JWT:Secret"])),
ValidateLifetime = false
};
var tokenHandler = new JwtSecurityTokenHandler();
SecurityToken securityToken = null;
var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out securityToken);
if (securityToken is not JwtSecurityToken jwtSecurityToken ||
!jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256,
StringComparison.InvariantCultureIgnoreCase))
throw new SecurityTokenException("Invalid token");
return principal;
}
on tokenHandler.ValidateToken I get this error saying:
IDX12741: JWT: '[PII is hidden. For more details, see https://aka.ms/IdentityModel/PII.]' must have three segments (JWS) or five segments (JWE).
what wrong am I doing? is there a simpler way of implementing jwt refresh token?
this is the generated token:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoidGVzdEBrYXNiaW0uaXIiLCJqdGkiOiJjM2I2ZTQyZi00ZGI1LTQzMDMtYjY4Mi02YWU5Yzg3ZjI1ZTUiLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3JvbGUiOiJVc2VyIiwiZXhwIjoxNjUyMzc0ODI4LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjQyMDAiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjQyMDAifQ.8rzkMkVENPAyWV2DpPhUUAzza0cXY_HiUpWq2u_Sqqs