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
what does your jwt look like? JWT is consist of three parts:header,playload and signature, and they are splited by"." in your jwt string.Your error was caused by the wrong structure of jwt,I suppose. and you could see the test result as below:
Updated
The error message of the second request(I deleted a"." in the jwt):