I am using angular and asp.net core I have implemented JWT Token and refresh token with the help of
this artical I have written code to check Jwt is valid in Authguard if jwt is not valid so with refresh token will make call to api and get the new Jwt and refresh token . this scenario is happing only if I am performing any event like refreshing page and navigating to another page. I want to know is there any way that if our jwt token expire without performing any event angular detect it and automatically call to api with refersh token and get the new jwt token
here are codes
authguard
here in authguard isUserAuthenticated() is going to check jwt expired or not if expired it will make api call with refresh token and get the new jwt and refresh token
async canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Promise<boolean> {
if((await this._authService.isUserAuthenticated()) === true){
return true;
}
else{
this._router.navigate([RouteConstant.SIGN_IN]);
return false;
}
}
public async isUserAuthenticated(){
if(this._storageService.getToken() !== null){
if(!this._jwtHelperService.isTokenExpired(this._storageService.getToken()?.toString())){
return true;
}
else{
if(!this._storageService.refreshTokenExists()){
return false;
}
else{
let authTokenClient: AuthTokenClient = {
token: this._storageService.getToken() as string,
refreshToken: this._storageService.getRefreshToken() as string
};
let response: AuthToken = await this._serverService.refreshAuthToken(authTokenClient).toPromise<AuthToken>();
console.log(response);
this.setAuth(response);
return !this._jwtHelperService.isTokenExpired(this._storageService.getToken()?.toString());
}
}
}
else{
return false;
}
}
}
public setAuth(authToken: AuthToken){
this._storageService.saveToken(authToken.token?.toString());
this._storageService.saveRefreshToken(authToken.refreshToken);
}
public async refreshAuthToken(authTokenClient: AuthTokenClient) {
const response = await this._http.post<AuthToken>(environment.apiHost + UrlConstant.ACCOUNT_REFRESH, authTokenClient,{observe: 'response'}).toPromise();
console.log(response);
const newToken = (<any>response).body.token;
console.log(newToken);
const newRefreshToken = (<any>response).body.refreshToken;
console.log(newRefreshToken);
localStorage.setItem("token", newToken);
localStorage.setItem("refreshToken",newRefreshToken);
if (newToken && newRefreshToken == null){
return false
}
else {
return true;
}
}
Startup.cs
services.AddAuthentication(opt =>
{
opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = jwtSettings.ValidIssuer,
ValidAudience = jwtSettings.ValidAudience,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration[ConfigurationKeyConstant.JWT_SECRET_KEY])),
ClockSkew = TimeSpan.Zero
};
});
Controller.cs
[Route("refresh")]
public IActionResult Refresh(AuthTokenBase authTokenBase)
{
if (authTokenBase is null)
{
return BadRequest("Invalid client request");
}
string accessToken = authTokenBase.Token;
string refreshToken = authTokenBase.RefreshToken;
var principal = this._jwtHandler.GetPrincipalFromExpiredToken(accessToken);
var username = principal.Identity.Name;
string newAccessToken = this._jwtHandler.GenerateToken();
string newRefreshToken = this._jwtHandler.GenerateRefreshToken();
return Ok(new AuthToken()
{
Token = newAccessToken,
RefreshToken = newRefreshToken,
IsAuthSuccessful = true
});
}
Under your ConfigureServices at service.AddAuthentication() you can add event for OnAuthenticationFailed.
And there check for exception type if its SecurityTokenExpiredException, then set response header tokenExpired=true.
Later at client side check response header and call refresh token API based on tokenExpired value.
pseudo code:
services.AddAuthentication(opt =>
{
opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = jwtSettings.ValidIssuer,
ValidAudience = jwtSettings.ValidAudience,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration[ConfigurationKeyConstant.JWT_SECRET_KEY])),
ClockSkew = TimeSpan.Zero
};
options.Events = new JwtBearerEvents {
OnAuthenticationFailed = context => {
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
{
context.Response.Headers.Add("IS-TOKEN-EXPIRED", "true");
}
return Task.CompletedTask;
}
};
});
Here in response header it sets IS-TOKEN-EXPIRED as true, if token is expired.
For more detail you can check this Blog article written by me.
You can use angular interceptor.
Intercept every error in http request
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
var modifiedRequest = this.normalizeRequestHeaders(request);
return next.handle(modifiedRequest)
.pipe(
catchError(error => {
if (error instanceof HttpErrorResponse && error.status === 401) {
return this.tryGetRefreshTokenService(request, next, error);
} else {
return this.handleErrorResponse(error);
}
}),
switchMap((event) => {
return this.handleSuccessResponse(event);
})
);
}
Related
I have been trying to get this working now for quite a some time but can't figure out how to do it properly. I am able to implement Rememeber Me with LocalStorage. However I want to implement Remember Me functionality with JWT using cookie where I would be able to set expiration time. I think I have messed up the login logic? Can somebody point out what is wrong here?
I can also add other parts from my application if necessary.
AuthorizeController.cs:
[HttpPost]
public async Task<IActionResult> Login([FromBody] LoginModel login)
{
ApplicationUser user = await this.SignInManager.UserManager.FindByEmailAsync(login.Email);
if (user == null)
{
List<string> errors = new List<string>();
errors.Add("No such user has been found.");
return BadRequest(new LoginResult
{
Successful = false,
Errors = errors,
});
}
bool emailConfirmed = await this.UserManager.IsEmailConfirmedAsync(user);
if (!emailConfirmed)
{
List<string> errors = new List<string>();
errors.Add("Email not confirmed.");
return BadRequest(new LoginResult
{
Successful = false,
Errors = errors,
});
}
Microsoft.AspNetCore.Identity.SignInResult result =
await this.SignInManager.PasswordSignInAsync(login.Email, login.Password, login.RememberMe, false);
if (!result.Succeeded)
{
List<string> errors = new List<string>();
errors.Add("Email and password are invalid.");
return BadRequest(new LoginResult
{
Successful = false,
Errors = errors,
});
}
IList<string> roles = await this.SignInManager.UserManager.GetRolesAsync(user);
List<Claim> claims = new List<Claim>
{
new Claim(ClaimTypes.Name, login.Email)
};
ClaimsIdentity identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
ClaimsPrincipal principal = new ClaimsPrincipal(identity);
AuthenticationProperties props = new AuthenticationProperties
{
IsPersistent = true,
ExpiresUtc = DateTime.UtcNow.AddMonths(1)
};
// to register the cookie to the browser
this.HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal, props).Wait();
foreach (string role in roles)
{
claims.Add(new Claim(ClaimTypes.Role, role));
}
SymmetricSecurityKey key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(this.Configuration["JwtSecurityKey"]));
SigningCredentials creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
DateTime expiry = DateTime.Now.AddDays(Convert.ToInt32(this.Configuration["JwtExpiryInDays"]));
JwtSecurityToken token = new JwtSecurityToken(
this.Configuration["JwtIssuer"],
this.Configuration["JwtAudience"],
claims,
expires: expiry,
signingCredentials: creds
);
return Ok(new LoginResult
{
Successful = true,
Token = new JwtSecurityTokenHandler().WriteToken(token),
});
}
Startup.cs:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = Configuration["JwtIssuer"],
ValidAudience = Configuration["JwtAudience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JwtSecurityKey"]))
};
})
.AddCookie(options =>
{
options.Cookie.Name = "MySpecialCookie";
options.LoginPath = "/login";
//options.LogoutPath = "/Home/Index";
//options.AccessDeniedPath = "AccessDenied";
options.ExpireTimeSpan = TimeSpan.FromDays(30);
options.SlidingExpiration = true; // the cookie would be re-issued on any request half way through the ExpireTimeSpan
//options.Cookie.Expiration = TimeSpan.FromDays(5);
options.EventsType = typeof(CookieAuthEvent);
});
services.AddScoped<CookieAuthEvent>();
services.AddAuthorization(config =>
{
config.AddPolicy(Policies.IsAdmin, Policies.IsAdminPolicy());
config.AddPolicy(Policies.IsUser, Policies.IsUserPolicy());
});
services.ConfigureApplicationCookie(options =>
{
options.Cookie.HttpOnly = true;
options.Events.OnRedirectToLogin = context =>
{
context.Response.StatusCode = 401;
return Task.CompletedTask;
};
});
On Client side I am currently using AuthorizeApi with LocalStorage. This is working but I want to move this to Cookie.
AuthorizeApi.cs:
public async Task<LoginResult> Login(LoginModel loginModel)
{
//var stringContent = new StringContent(JsonSerializer.Serialize(LoginModel), Encoding.UTF8, "application/json");
HttpResponseMessage responseMessage = await this.HttpClient.PostAsJsonAsync("Authorize/Login", loginModel);
LoginResult result = await responseMessage.Content.ReadFromJsonAsync<LoginResult>();
if (result.Successful)
{
if (loginModel.RememberMe)
{
await this.LocalStorage.SetItemAsync("MySpecialToken", result.Token);
}
((ApiAuthenticationStateProvider)this.AuthenticationStateProvider).MarkUserAsAuthenticated(result.Token);
this.HttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", result.Token);
return result;
}
return result;
}
ApiAuthenticationStateProvider.cs:
public void MarkUserAsAuthenticated(string token)
{
ClaimsPrincipal authenticatedUser = new ClaimsPrincipal(new ClaimsIdentity(ParseClaimsFromJwt(token), "jwt"));
Task<AuthenticationState> authState = Task.FromResult(new AuthenticationState(authenticatedUser));
NotifyAuthenticationStateChanged(authState);
}
On an older project I created custom authentication that used cookies to store the session identifier.
The process was simple. Start by adding a service to house the tokens for the current user session, that will probably be sent to your API, or alternatively update your injected HttpClient right after authentication or on your MainLayout once you've retrieved the value from the cookie:
if (!httpClient.DefaultRequestHeaders.Contains("SessionID"))
httpClient.DefaultRequestHeaders.Add("SessionID", await JSRuntime.InvokeAsync<string>("MyJs.Cookies.Get", "SessionID"));
Then on your MainLayout check if the cookie has a value before you navigate to authentication. To do this, I used the following JavaScript:
window.MyJs = {
Cookies: {
Set: function (name, value, date) {
var d = new Date(date);
var expires = "expires=" + d.toUTCString();
document.cookie = name + "=" + value + ";" + expires + ";path=/";
},
Get: function (name) {
name = name + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
},
Remove: function (name) {
RadixTrie.Cookies.Set(name, "", "01 Jan 1970 00:00:00 UTC");
}
}
}
Do the check in MainLayout like this:
protected override async Task OnAfterRenderAsync(bool firstRender) {
if (firstRender)
{
...
var sessionID = await JSRuntime.InvokeAsync<string>("MyJs.Cookies.Get", "SessionID");
if (string.IsNullOrWhiteSpace(sessionID))
NavigationManager.NavigateTo("Auth/Login", true);
...
}
}
Once you've authenticated, you can just set the cookie:
internal async Task Login() {
...
await JSRuntime.InvokeVoidAsync("MyJs.Cookies.Set", "SessionID", loginResponse.Token, loginResponse.Expires);
//{loginResponse.Token:string} {loginResponse.Expires:datetime}
...
}
EDIT:
I wasn't satisfied with this answer and took some time to consider alternatives. I don't have a fully coded solution, but a better way of implementing a token for authentication, and in your case to keep a session live, would be using Set-Cookie header in your responses from your API.
I suggest creating middleware to handle the reading and resetting of the token.
But let's start with the login. Once a user authenticated, you can update the response in your endpoint like:
[HttpPost]
public async Task<IActionResult> Login([FromBody] LoginModel login)
{
...
Response.Headers.Add("Set-Cookie", $"SessionID={Guid.NewGuid()}; Expires={DateTime.Now.AddMonths(1).ToString("dd MMM yyyy hh:mm:ss") + " UTC"}; HttpOnly"); //Valid for 1 month, HttpOnly
...
return Ok();
}
It would be a good idea to make the token and cookie string generation reusable at this point. Consider encryption too.
Thereafter, add middleware to your API and on each request read the cookie to get the token:
public async Task Invoke(HttpContext context)
{
...
context.Request.Cookies.TryGetValue("SessionID", out string sessionID);
...
await _next(context);
...
//Reset the token after each request for improved security
context.Response.Headers.Add("Set-Cookie", $"SessionID={Guid.NewGuid()}; Expires={DateTime.Now.AddMonths(1).ToString("dd MMM yyyy hh:mm:ss") + " UTC"}; HttpOnly"); //Valid for 1 month, HttpOnly
}
EDIT 2:
If a cookie is reset and there are asynchronous work being done using the API, some requests might fail if a request is made just after the cookie change before being updated. This is a very small window and should be low in chance. I will take some time in the future to test this theory and update this answer.
In an ASP.NET Core 5 project with RestAPI, I'm using JWT Bearer with tokens and refresh-tokens. I configured the startup like this:
var jwtSecretKey = Configuration.GetValue<string>("Jwt:Key");
var key = Encoding.UTF8.GetBytes(jwtSecretKey);
var tokenValidationParameters = new TokenValidationParameters
{
SaveSigninToken = true,
ValidateActor = true,
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = Configuration["Jwt:Issuer"],
ValidAudience = Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(key),
// set clockskew to zero so tokens expire exactly at token expiration time (instead of 5 minutes later)
ClockSkew = TimeSpan.Zero
};
services.AddAuthentication(options =>
{
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = false;
options.SaveToken = true;
options.TokenValidationParameters = tokenValidationParameters;
});
For generating the Tokens I used this code (Something to notice is I used UTC dates):
string validIssuer = _configuration["Jwt:Issuer"];
string validAudience = _configuration["Jwt:Audience"];
var jwtSecretKey = _configuration.GetValue<string>("Jwt:Key");
var issuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSecretKey));
var accessTimeout = _configuration.GetValue<int>("Jwt:MinutosExpiracionTokenAcceso");
DateTime fechaHoy = DateTime.UtcNow;
DateTime fechaExpiracionToken = DateTime.UtcNow.AddMinutes(accessTimeout);
var userRoles = await _userManager.GetRolesAsync(user);
var claims = new List<Claim>
{
new Claim(ClaimTypes.Sid, user.Id),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) // (JWT ID) Unique Identifier for JWT, can be used to prevent the JWT from being replayed. Is best suited for refresh tokens
};
foreach (var userRole in userRoles)
{
claims.Add(new Claim(ClaimTypes.Role, userRole));
}
var token = new JwtSecurityToken(
validIssuer,
validAudience,
claims,
fechaHoy,
fechaExpiracionToken,
new SigningCredentials(issuerSigningKey, SecurityAlgorithms.HmacSha256)
);
var refreshTimeout = _configuration.GetValue<int>("Jwt:MinutosExpiracionTokenAutorizacion");
var refreshToken = new RefreshToken()
{
JwtId = token.Id,
IsUsed = false,
UserId = user.Id,
AddedDate = DateTime.UtcNow,
ExpiryDate = DateTime.UtcNow.AddMinutes(refreshTimeout),
IsRevoked = false,
Token = RandomString(25) + Guid.NewGuid()
};
await _unitOfWork.GetRepository<RefreshToken>().AddAsync(refreshToken);
await _unitOfWork.SaveChangesAsync();
var tokensDto = new TokensResponseDto
{
Token = new JwtSecurityTokenHandler().WriteToken(token),
RefreshToken = refreshToken.Token
};
return tokensDto;
I'm facing issues validating the token (_tokenValidationParameters are the same as used in startup):
var jwtTokenHandler = new JwtSecurityTokenHandler();
ClaimsPrincipal principal = null;
SecurityToken validatedToken = null;
principal = jwtTokenHandler.ValidateToken(tokenRequest.Token, _tokenValidationParameters, out validatedToken);
It's throwing the mentioned exception with no specific dates, no matter whether I use localtime such as DateTime.Now instead of UTC.
I tried by adding LifetimeValidator in TokenValidationParameters in startup like this:
LifetimeValidator = (DateTime? notBefore,
DateTime? expires,
SecurityToken securityToken,
TokenValidationParameters validationParameters
) =>
{
if (expires != null)
{
if (DateTime.UtcNow < expires.Value.ToUniversalTime())
{
return true; // Still valid
}
}
return false; // Expired
}
However, ValidateToken fails with this exception:
IDX10230: Lifetime validation failed. Delegate returned false, securitytoken: 'System.IdentityModel.Tokens.Jwt.JwtSecurityToken'.
Thanks for any help
I had a similar problem. My server (generating token) time and my client (consuming token) time were different. Synchronized and worked perfectly.
In my case when I deployed the application on one of the Linux server, I had a similar "IDX10222" error, The reason behind this error is that the server current time is running behind the token creation time, so server is unable to validate the token.
For Example - Token Creation Time - 10:00:00 on dd:MM:yyyy UTC,
Linux Server Time - 08:00:00 on dd:MM:yyyy UTC
Then we get this error - Lifetime validation failed. The token is not yet valid.
Please check your machine datetime and fix if it is not in sync with the current time.
You Try:
/*
ClaimsPrincipal principal = null;
principal = jwtTokenHandler.ValidateToken(tokenRequest.Token, _tokenValidationParameters, out validatedToken);
*/
validatedToken = jwtTokenHandler.ReadJwtToken(token);
/*Your own validation logic*/
Basically, I can log in just fine in my Client and access all pages. If I log out, I can't access anything on my client, it always redirects to my login page. Directly accessing the api endpoint is another matter. Say I call mylocalapp.com/api/users, directly from the browser url bar, I can still get all data. So I added [Authorize]. Unexpectedly, I get a 401 if I do that, DESPITE being logged in. So I checked HttpContext.User and simply User.Identity and they're empty, despite successful authentication. ClaimsIdentity is also empty. Can you identify anything that I might have done wrong? According to what I found on Google, this IS how it should be done. Thanks for your help.
Login method:
Takes user data, checks if ok, creates a JWT token and gets browser cookies (if null - session expired) then appends the token to the browser. And then logs the user in via Sign In Manager. If ok, returns ok. Context var is just for debugging purposes.
[AllowAnonymous]
[HttpPost]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
async public Task<IActionResult> LoginPost(AuthUser authUser)
{
var user = _context.Users.FirstOrDefault(user => user.Email == authUser.Email);
if (authUser is null) return BadRequest(AppResources.NullUser);
if (user is null) return BadRequest(AppResources.UserBadCredentials);
else
{
var isPassword = _userManager.CheckPasswordAsync(user, authUser.Password);
if (!isPassword.Result) return BadRequest(AppResources.UserBadCredentials);
var token = _jwtHandlerAuth.Authentication(authUser);
if (token == null) return BadRequest(AppResources.UserAuthenticationImpossible);
string cookieValue = Request.Cookies["jwt"];
var returnUser = new ReturnUser
{
Email = user.Email,
Name = $"{user.FirstName} {user.LastName}",
UserName = user.UserName
};
if (cookieValue != token)
{
Response.Cookies.Append("jwt", token, new CookieOptions
{
HttpOnly = true,
IsEssential = true,
SameSite = SameSiteMode.None,
Secure = true
});
}
var signInResult = await _signInManager.PasswordSignInAsync(authUser.Email, authUser.Password, false, false);
if (!signInResult.Succeeded) return BadRequest(AppResources.UserAuthenticationImpossible);
var context = _signInManager.Context;
return Ok(returnUser);
}
}
My Authentication method (creates the token):
public string Authentication(AuthUser authUser)
{
var tokenHandler = new JwtSecurityTokenHandler();
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[]
{
new Claim("UserName", authUser.Email),
new Claim("Email", authUser.Email)
}),
Expires = DateTime.UtcNow.AddHours(2),
SigningCredentials = new SigningCredentials(
new SymmetricSecurityKey(Encoding.UTF32.GetBytes(_privateKey)),
SecurityAlgorithms.HmacSha256Signature)
};
var claimsPrincipal = new ClaimsPrincipal(tokenDescriptor.Subject);
var token = tokenHandler.CreateToken(tokenDescriptor);
tokenHandler.WriteToken(token);
return tokenHandler.WriteToken(token);
}
And my Startup class services and middleware:
var builder = services.AddIdentityCore<User>();
var identityBuilder = new IdentityBuilder(builder.UserType, builder.Services);
identityBuilder.AddRoles<IdentityRole>();
identityBuilder.AddEntityFrameworkStores<IdentityContext>();
identityBuilder.AddSignInManager<SignInManager<User>>();
identityBuilder.AddDefaultTokenProviders();
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/build";
});
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
x.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddCookie("Identity.Application").AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF32.GetBytes(Configuration.GetSection("Jwt:PrivateKey").Value)),
ValidateIssuer = false,
ValidateAudience = false
};
});
services.AddSingleton<IJwtHandlerAuth>(new JwtHandlerAuth(Configuration.GetSection("Jwt:PrivateKey").Value));
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
I am generating a valid JWT in my API, and returning it with an expiry embedded. See code and example below:
public static string GenerateToken(string securityKey,
string claimName, string issuer, RedisManagerPool redisClient)
{
var claims = new[]
{
new Claim(ClaimTypes.Name,
claimName)
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(securityKey));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var expiry = DateTime.Now.AddMinutes(UtilityCommand.Cache.GetCacheValue<int>(Functions.ParameterPath
+ Functions.Integration
+ Functions.JWT
+ "/expiry_minutes", redisClient));
var token = new JwtSecurityToken(
issuer: issuer,
audience: issuer,
claims: claims,
expires: expiry,
signingCredentials: creds);
return new JwtSecurityTokenHandler().WriteToken(token);
}
Example:
{
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "scpi",
"exp": 1598960076,
"iss": "https://lcsapi",
"aud": "https://lcsapi"
}
But no matter how long I use the token for, it never expires? Where am I going wrong here? Even if I set the expiry to something like 1 minute. Here is my auth check:
public APIGatewayCustomAuthorizerResponse GetAuthentication(APIGatewayCustomAuthorizerRequest authorizerRequest, ILambdaContext context)
{
var tokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = Issuer,
ValidateAudience = true,
ValidateLifetime = UtilityCommand.Cache.GetCacheValue<bool>(ParameterPath + Integration + JWT + "/jwtexpires", _redisClient), // testing
ValidAudience = Issuer,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(SecurityKey)),
ClockSkew = TimeSpan.FromMinutes(5), // Required to account for potential drift times between systems.
ValidateIssuerSigningKey = true
};
SecurityToken validatedToken;
JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
bool authorized = false;
if (!string.IsNullOrWhiteSpace(authorizerRequest.AuthorizationToken))
{
try
{
var jwt = authorizerRequest.AuthorizationToken.Replace("Bearer ", string.Empty);
var user = handler.ValidateToken(jwt, tokenValidationParameters, out validatedToken);
var claim = user.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Name);
if (claim != null)
authorized = claim.Value == ClaimName; // Ensure that the claim value matches the assertion
}
catch (Exception ex)
{
context.Logger.LogLine($"Error occurred validating token: {ex.Message}");
}
}
else
{
context.Logger.LogLine($"Error occurred validating token: No token provided.");
}
return GenerateAuthorizerResponse(authorized, authorizerRequest, context);
}
Glad you've resolved it. Worth making sure you are checking expiry against DateTime.UtcNow, since the expiry claim is a UTC value.
Answer, I needed to add the following to my TokenValidationParameters.
LifetimeValidator = LifetimeValidator,
Which accepts a delegate function that checks the expiry (I didn't realise this wasn't handled automatically). It's just a bool return on whether the expiry is passed:
private bool LifetimeValidator(DateTime? notBefore, DateTime? expires, SecurityToken token, TokenValidationParameters #params)
{
if (expires != null)
{
return expires > DateTime.Now;
}
return false;
}
This is my jwt setup in Startup.cs
services.AddIdentity<User, Role>()
.AddUserManager<CustomUserManager>()
.AddEntityFrameworkStores<ManagementStudioDbContext>();
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = "bearer";
x.DefaultChallengeScheme = "bearer";
})
.AddJwtBearer("bearer",x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
//x.TokenValidationParameters = tokenValidationParameters;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Environment.GetEnvironmentVariable(MSCASGlobals.MS_SecretKey))),
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidIssuer = "SDD",
ValidAudience = "SDD",
ClockSkew = TimeSpan.Zero,
};
x.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
if(context.Exception.GetType() == typeof(SecurityTokenExpiredException))
{
context.Response.Headers.Add("Token-Expired", "true");
}
return Task.CompletedTask;
}
};
});
When I try to run my Authentication API, I get this error:
Unable to resolve service for type
'Microfoft.IdentityModel.Tokens.TokenValidationparameters' while
attempting to active 'Appone.Connect.Api.Token'
This is the code to create the token:
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration[AppOne.ClassLibrary.Globals.MSCASGlobals.MS_SecretKey]));
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = DateTime.UtcNow.AddMinutes(2),
SigningCredentials = credentials,
IssuedAt = DateTime.Now,
Issuer = "SDD",
Audience = "SDD"
};
var token = tokenHandler.CreateToken(tokenDescriptor);
var refreshToken = tokens.GenerateRefreshToken();
Connect.Api is this controller class. It is already injected in my Startup.cs
public class Token : Controller
{
public Token()
{
}
public string GenerateRefreshToken()
{
var random = new byte[64];
var rng = RandomNumberGenerator.Create();
rng.GetBytes(random);
return Convert.ToBase64String(random).Replace("/", "_").Replace("+", "_");
}
public string RefreshToken(string Token, string RefreshToken, string SecretKey)
{
var validatedToken = GetPrincipalFromToken(Token, SecretKey);
if (validatedToken == null)
{
return null;
}
return "Hello";
}
public ClaimsPrincipal GetPrincipalFromToken(string Token, string SecretKey)
{
var tokenHandler = new JwtSecurityTokenHandler();
var tokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false, //you might want to validate the audience and issuer depending on your use case
ValidateIssuer = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(SecretKey)),
ValidateLifetime = false //here we are saying that we don't care about the token's expiration date
};
try
{
var principal = tokenHandler.ValidateToken(Token, tokenValidationParameters, out var validatedToken);
if (!ValidateSecurityAlgorithm(validatedToken))
{
return null;
};
return principal;
}
catch(Exception e)
{
return null;
}
}
private bool ValidateSecurityAlgorithm(SecurityToken SecurityToken)
{
var res = (SecurityToken is JwtSecurityToken jwtSecurityToken) && jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256,StringComparison.InvariantCultureIgnoreCase);
return res;
}
}