I've been tasked to validate a JWT token that's been encoded using the PS256 algorithm and for the last two days I've been having trouble with it. I lack knowledge on this subject and I've been chipping away slowly at the problem trying different solutions.
// Encoded
eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImtpZDEyMzQifQ.eyJpc3MiOiJmb28uYmFyLnRlc3Rpc3N1ZXIiLCJleHAiOjE1NTEyMDEwNjgsImF0X2hhc2giOiJqaFl3c1pyTnZ0dFNYQnR6QVMtWlNnIn0.yJePyxdJWyydG4HM97oQag6ulGKa5Afw-LHYYEXz7lVy8v0IJD0mSO9WtowlWJIeD2Vvthuj71XUfHsgz0LD9rK0VBucJbd_OiIXpbwPUqBcdj82DNLFXDJfCJnUC-Rv8QP7OUVBvLjvBQ6WYMrx1Qnq8xP6qeL_ohKwRmo6EDhZRkYBz9gFhfha1ZlKcnyR73nXdShwy7OmmyiRvVWPBf_GgSsfz8FNNoKySW1MA4tRa7cl3zPlyCnWyLaZ3kcQsmTqarHG--YXSDF5ozZ_Sx6TkunCxrOYzOFNcPyeIWqI84cemM6TgMBw9jhzMCk7Y4Fzxe5KEYJH4GlGA4s4zg
// Header
{
"alg": "PS256",
"typ": "JWT",
"kid": "kid1234"
}
// Payload
{
"iss": "foo.bar.testissuer",
"exp": 1551201068,
"at_hash": "jhYwsZrNvttSXBtzAS-ZSg"
}
I have a working implementation for RS256 encoded JWT which is using the JWTSecurityTokenHandler provided in Microsoft.IdentityModel.Tokens and System.IdentityModel.Tokens.Jwt. For the RS256 implementation I have a IssuerSigningKeyResolver that is making custom checks for the kid and supplying the public key
var tokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = issuer,
ValidateLifetime = true,
RequireSignedTokens = true,
RequireExpirationTime = true,
ValidateAudience = false,
ValidateIssuer = true,
IssuerSigningKeyResolver = (string token, SecurityToken securityToken, string kid, TokenValidationParameters validationParameters) =>
{
// Custom kid checks
var rsa = RSA.Create();
rsa.ImportParameters(new RSAParameters
{
Exponent = Base64UrlEncoder.DecodeBytes(matchingKid.E),
Modulus = Base64UrlEncoder.DecodeBytes(matchingKid.N),
});
latestSecurityKeys.Add(matchingKid.Kid, new RsaSecurityKey(rsa));
var securityKeys = new SecurityKey[1]
{
new RsaSecurityKey(rsa)
};
return securityKeys;
}
};
var tokenHandler = new JwtSecurityTokenHandler();
try
{
var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out SecurityToken validatedToken);
return true;
}
catch (SecurityTokenException ex)
{
// Do something with ex
return false;
}
This implementation is not working for PS256 encoded JWT. I debugged the JwtSecurityTokenHandler inside System.IdentityModel.Tokens.Jwt, but it seems that even though PS256 is in the supported algorithms list the verification fails.
I must state again that my knowledge on this subject is limited. From what I understand RSA256 and PS256 are in the same family of algorithms? Would I be better off to just create a custom validation of the PS256 JWT using another library like jose-jwt?
After raising the issue with Microsoft it seems that right now such validation is not supported by Microsoft.IdentityModel.Tokens and System.IdentityModel.Tokens.Jwt . Details can be found here - https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/1117
In the end I validated my token using jose-jwt and some custom checks.
private bool IsValid(string token, string issuer, string configId)
{
var jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
var jwtSecurityToken = jwtSecurityTokenHandler.ReadToken(token) as JwtSecurityToken;
// Extract the kid from token header
var kidHeader = jwtSecurityToken.Header.Where(k => k.Key.ToLower() == "kid")?.FirstOrDefault();
if (kidHeader?.Value == null) ThrowInvalidOperation($"Failed to find matching kid for Issuer: {issuer.ToLower() }");
var kid = kidHeader?.Value as string;
// Extract the expiration time from token payload
var expirationTime = jwtSecurityToken.Payload?.Exp;
if (expirationTime == null) ThrowInvalidOperation($"Failed to find matching expiration time for Issuer: {issuer.ToLower() }");
// Decode to verify signature
var verifiedToken = JWT.Decode(token, GetPublicKey(kid, issuer, providerId));
if (verifiedToken != null)
{
var json = JsonConvert.DeserializeObject<dynamic>(verifiedToken);
return IsValidIssuer(json, issuer) && IsValidExpirationTime(json, expirationTime);
}
else
{
return false;
}
void ThrowInvalidOperation(string msg) => throw new InvalidOperationException(msg);
}
private bool IsValidIssuer(dynamic json, string issuer)
{
if (json != null && issuer != null)
{
if (json["iss"] == issuer)
{
return true;
}
else
{
return false;
}
}
return false;
}
private bool IsValidExpirationTime(dynamic json, int? expTime)
{
if (json != null && expTime != null)
{
if (json["exp"] == expTime)
{
return true;
}
else
{
return false;
}
}
return false;
}
private RSA GetPublicKey(string kid, string validIssuer, string configId)
{
var openIdConfig = openIdConfigurationProvider.GetOpenIdConfiguration(configId);
var matchingKid = openIdConfig?.JsonWebKeySet?.Keys?.FirstOrDefault(x => x.Kid == kid);
if (matchingKid == null)
{
throw new InvalidOperationException($"kid is null");
}
var rsa = RSA.Create();
rsa.ImportParameters(new RSAParameters
{
Exponent = Base64UrlEncoder.DecodeBytes(matchingKid.E),
Modulus = Base64UrlEncoder.DecodeBytes(matchingKid.N),
});
return rsa;
}
Related
I am trying to write unit tests for authentication logic implemented using Azure AD client credentials flow using MOQ.
The first test case is to check that if the "Audience" is valid. I am trying to mock claim to set up the "aud" or "appId" claims using ClaimTypes but not able to find anything like ClaimTypes.Aud
var identity = new ClaimsIdentity(new Claim[] {
new Claim(ClaimTypes.Name, "Sahil")
});
var mockPrincipal = new Mock<ClaimsPrincipal>(identity);
mockPrincipal.Setup(x => x.Identity).Returns(identity);
mockPrincipal.Setup(x => x.IsInRole(It.IsAny<string>())).Returns(true);
How can I set up the "aud" and "appId" claims in C#
OR
Just setup mockPrincipal so that when it tries to check if "aud" is valid it returs false.
I am trying to write unit tests for the below code.
public void Authenticate(JwtBearerOptions options)
{
_configuration.Bind("AzureAD", options);
options.TokenValidationParameters.ValidateAudience = true;
options.TokenValidationParameters.ValidateIssuerSigningKey = true;
options.TokenValidationParameters.ValidateIssuer = true;
options.Events ??= new JwtBearerEvents();
var existingHandlers = options.Events.OnTokenValidated;
options.Events.OnTokenValidated = async context =>
{
string appId = GetAppIdFromToken(context);
bool isAllowed = await CheckAppIdIsAllowedAsync(context, appId);
if (isAllowed)
{
_logger.LogInformation($"[{nameof(Authenticate)}] AppId in allow list");
}
else
{
_logger.LogError($"[{nameof(Authenticate)}] AppId {appId} not in allowed list");
}
await Task.CompletedTask.ConfigureAwait(false);
};
options.Events.OnTokenValidated += existingHandlers;
}
private string GetAppIdFromToken(TokenValidatedContext context)
{
string appId = context.Principal.Claims.FirstOrDefault(x => x.Type == "appid" || x.Type == "azp")?.Value;
return appId;
}
private async Task<bool> CheckAppIdIsAllowedAsync(TokenValidatedContext context, string appId)
{
IEnumerable<string> AllowedApps = _configuration.GetSection("AllowedAppPrincipals").Get<string[]>();
var FoundAppId = AllowedApps.FirstOrDefault(a => a == appId);
if (FoundAppId == null)
{
context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
context.Response.ContentType = "application/json";
const string message = "{\"error\" : \"Unacceptable app principal\"}";
byte[] arr = Encoding.ASCII.GetBytes(message);
await context.Response.BodyWriter.WriteAsync(arr);
context.Fail(message);
return false;
}
return true;
}
How to mock aud and appId claims with Moq?
I tried to reproduce how to get audience invalid in my environment.
It usually happens when the issuer endpoint is different or scope is not given correctly or different scope than what is intended is mentioned.
https://login.microsoftonline.com/xx/oauth2/v2.0/token
Here i gave scope for api other than microsoft graph.
But next tep i am calling graph endpoint and so i am getting invalid audience error .
https://graph.microsoft.com/v1.0/users/xxx
To check for mocking test , you can consider below point as direct claims for "aud" audience in c# couldn't be obtained AFAIK.
Aud claim is Application ID URI or GUID Identifies the intended
audience of the token. In v2.0 tokens, audience must be client ID
of the API whereas in v1.0 tokens, it can be the client ID or the
resource URI used in the request.
One way to validate it is to check following way with issuer/audience
You can give custom values according to the authorization endpoint
Code:
string AUDIENCE = "<GUID of your Audience according to the app>";
string TENANT = "<GUID of your Tenant>";
private static async Task<SecurityToken> validateJwtTokenAsync(string token)
{
// URL based on your AAD-TenantId
var stsDiscoveryEndpoint = String.Format(CultureInfo.InvariantCulture, "https://login.microsoftonline.com/{0}/.well-known/openid-configuration", TENANT);
//To Get tenant information
var configManager = new ConfigurationManager<OpenIdConnectConfiguration>(stsDiscoveryEndpoint)
// Get Config from AAD:
var config = await configManager.GetConfigurationAsync();
// Validate token:
var tokenHandler = new JwtSecurityTokenHandler();
var validationParameters = new TokenValidationParameters
{
ValidAudience = AUDIENCE,
ValidIssuer = config.Issuer,
IssuerSigningTokens = config.SigningTokens,
CertificateValidator = X509CertificateValidator.ChainTrust,
};
var validatedToken = (SecurityToken)new JwtSecurityToken();
tokenHandler.ValidateToken(token, validationParameters, out validatedToken);
return validatedToken;
}
Or
var TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = key,
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = false,
....
ValidIssuer = configuration["JwtAuthentication:Issuer"],
ValidAudience = configuration["JwtAuthentication:Audience"]
};
So from validation parameters, you can get if that we that audience was valid or not, it majorly occurs when issuer is different from what we expected or when scopes are not correct.
You can try get those validate as claims to check for mocking
Snippent below taken from TokenValidationParameters.AudienceValidator, System.IdentityModel.Tokens C# (CSharp) Code Examples - HotExamples
public virtual ClaimsPrincipal ValidateToken(string securityToken, TokenValidationParameters validationParameters, out SecurityToken
validatedToken)
{
if (string.IsNullOrWhiteSpace(securityToken))
{
throw new ArgumentNullException("securityToken");
}
if (validationParameters == null)
{
throw new ArgumentNullException("validationParameters");
}
if (validationParameters.ValidateAudience)
{
if (validationParameters.AudienceValidator != null)
{
if
(!validationParameters.AudienceValidator(jwt.Audiences, jwt,
validationParameters))
{
throw new SecurityTokenInvalidAudienceException(string.Format(CultureInfo.InvariantCulture,
ErrorMessages.IDX10231, jwt.ToString()));
}
}
else
{
this.ValidateAudience(jwt.Audiences, jwt,
validationParameters);
}
}
ClaimsIdentity identity = this.CreateClaimsIdentity(jwt, issuer,
validationParameters);
if (validationParameters.SaveSigninToken)
{
identity.BootstrapContext = new
BootstrapContext(securityToken);
}
validatedToken = jwt;
return new ClaimsPrincipal(identity);
}
Also check Reference :c# - How to mock ConfigurationManager.AppSettings with moq - Stack Overflow
I am create a JWT access token and refresh token on login of valid user, access token is short lived and refresh token is with expiration time of 7 days, When I am trying to generate new access token after expiry using refresh token it is working fine and response with new access token and refresh token but after long time such as after 3 or 4 hours when I am trying it is not working. I am also comment in Refresh token method code where I am getting error.
Please see my code:
Controller:
public IActionResult RefreshToken([FromBody] RefreshTokenRequest request)
{
try
{
if (string.IsNullOrWhiteSpace(request.RefreshToken))
{
return Unauthorized();
}
var jwtResult = _jwtAuthManager.Refresh(request.RefreshToken, request.AccessToken, DateTime.Now);
var userName = jwtResult.RefreshToken.UserName;
var role = _userService.GetUserRole(userName);
var claims = new[]
{
new Claim(ClaimTypes.Role, role)
};
_logger.LogInformation($"User [{userName}] has refreshed JWT Token");
if (jwtResult == null)
{
return BadRequest();
}
return Ok(new
{
UserName = userName,
Role= role,
AccessToken = jwtResult.AccessToken,
RefreshToken = jwtResult.RefreshToken.TokenString,
Status = "Success",
Message = "New access token generated successfully"
});
}
catch (SecurityTokenException e)
{
return Unauthorized(e.Message); // return 401 so that the client side can redirect the user to login page
}
}
Generate token method:
public JwtAuthResult GenerateTokens(string username, Claim[] claims, DateTime now)
{
var shouldAddAudienceClaim = string.IsNullOrWhiteSpace(claims?.FirstOrDefault(x => x.Type == JwtRegisteredClaimNames.Aud)?.Value);
var jwtToken = new JwtSecurityToken(
_jwtTokenConfig.Issuer,
shouldAddAudienceClaim ? _jwtTokenConfig.Audience : string.Empty,
claims,
expires: now.AddMinutes(_jwtTokenConfig.AccessTokenExpiration),
signingCredentials: new SigningCredentials(new SymmetricSecurityKey(_secret), SecurityAlgorithms.HmacSha256Signature));
var accessToken = new JwtSecurityTokenHandler().WriteToken(jwtToken);
var refreshToken = new RefreshToken
{
UserName = username,
TokenString = GenerateRefreshTokenString(),
ExpireAt = now.AddMinutes(_jwtTokenConfig.RefreshTokenExpiration),
};
_usersRefreshTokens.AddOrUpdate(refreshToken.TokenString, refreshToken, (s, t) => refreshToken);
return new JwtAuthResult
{
AccessToken = accessToken,
RefreshToken = refreshToken
};
}
Refresh Token Method:
public JwtAuthResult Refresh(string refreshToken, string accessToken, DateTime now)
{
var (principal, jwtToken) = DecodeJwtToken(accessToken);
if (jwtToken == null || !jwtToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256Signature))
{
throw new SecurityTokenException("Invalid token");
}
var userName = principal.Identity?.Name;
if (!_usersRefreshTokens.TryGetValue(refreshToken, out var existingRefreshToken))
{
throw new SecurityTokenException("Invalid token not found");
}
var result = existingRefreshToken;
if (existingRefreshToken.UserName != userName || existingRefreshToken.ExpireAt <= now) //After 3 or 4 hours I am getting error in this condition.
{
throw new SecurityTokenException("Invalid UserName or refresh token expired");
}
return GenerateTokens(userName, principal.Claims.ToArray(), now); // need to recover the original claims
}
Claim Principal Method:
public (ClaimsPrincipal, JwtSecurityToken) DecodeJwtToken(string token)
{
if (string.IsNullOrWhiteSpace(token))
{
throw new SecurityTokenException("Invalid token");
}
var principal = new JwtSecurityTokenHandler()
.ValidateToken(token,
new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = _jwtTokenConfig.Issuer,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(_secret),
ValidAudience = _jwtTokenConfig.Audience,
ValidateAudience = true,
ValidateLifetime = false,
ClockSkew = TimeSpan.FromMinutes(1)
},
out var validatedToken);
return (principal, validatedToken as JwtSecurityToken);
}
I have the code below:
var tokenDescriptor = new SecurityTokenDescriptor()
{
NotBefore = DateTime.UtcNow,
Expires = DateTime.UtcNow.AddSeconds(client.AccessTokenLifetime),
Issuer = issuer,
Audience = (await _apiResourcesStore.FindByScopesNameAsync(tokenRequest.Scope.Split(" ").ToList())).Select(p => p.Name).ToArray(),
}
and I want add multiple audiences, but Audience type is string. Is there any way to cheat the token descriptor? Or maybe there is an alternative for token descriptor?
I did it and now it's working:
var tokenDescriptor = new SecurityTokenDescriptor()
{
NotBefore = DateTime.UtcNow,
Expires = DateTime.UtcNow.AddSeconds(client.AccessTokenLifetime),
Issuer = issuer,
Claims = new Dictionary<string, object>
{
{ JwtRegisteredClaimNames.Aud, (await _apiResourcesStore.FindByScopesNameAsync(tokenRequest.Scope.Split(" ").ToList())).Select(p => p.Name).ToArray() }
}
};
I just added new Claim.
I am calling Async method in a non-async method in the below way in WebApi authentication handler. And it hangs when it executes the inner external Async method. I need to set the claims before the subsequent code executes. So I am setting the return value to the Thread.CurrentPrincipal. Please advise.
I have tried the below and none of them worked.
Task.Run(() => Thread.CurrentPrincipal = this.ValidateTokenAsync(accessToken).GetAwaiter().GetResult()); - this works but the subsequent code execution does not wait on this and so the claims are not utilized there.
Thread.CurrentPrincipal = this.ValidateTokenAsync(accessToken).Result;
Task<ClaimsPrincipal> claimsPrincipalTask = this.ValidateTokenAsync(accessToken);
Task.WaitAll(claimsPrincipalTask);
Thread.CurrentPrincipal = Thread.CurrentPrincipal.GetAwaiter().GetResult();
Thread.CurrentPrincipal = this.ValidateTokenAsync(accessToken).GetAwaiter().GetResult();
private async Task<ClaimsPrincipal> ValidateTokenAsync(string accessToken)
{
LoggingUtilities.Logger.TraceInformation("Validating JWT.");
ClaimsPrincipal principal = ClaimsPrincipal.Current;
if (principal == null || principal.Identity == null || !principal.Identity.IsAuthenticated)
{
principal = await JwtValidator.ValidateTokenAsync(accessToken).ConfigureAwait(false);
}
return principal;
}
The JwtValidator.ValidateTokenAsync method:
public static async Task<ClaimsPrincipal> ValidateTokenAsync(string accessToken)
{
string authority = string.Format(CultureInfo.InvariantCulture, aadInstance, tenant);
string issuer = null;
string stsDiscoveryEndpoint = string.Format(CultureInfo.InvariantCulture, "{0}/.well-known/openid-configuration", authority);
List<SecurityToken> signingTokens = null;
try
{
// The issuer and signingTokens are cached for 24 hours. They are updated if any of the conditions in the if condition is true.
if (DateTime.UtcNow.Subtract(stsMetadataRetrievalTime).TotalHours > 24
|| string.IsNullOrEmpty(globalIssuer)
|| globalSigningTokens == null)
{
// Get tenant information that's used to validate incoming jwt tokens
ConfigurationManager<OpenIdConnectConfiguration> configManager = new ConfigurationManager<OpenIdConnectConfiguration>(stsDiscoveryEndpoint);
OpenIdConnectConfiguration config = await configManager.GetConfigurationAsync().ConfigureAwait(false);
globalIssuer = config.Issuer;
globalSigningTokens = config.SigningTokens.ToList();
stsMetadataRetrievalTime = DateTime.UtcNow;
}
issuer = globalIssuer;
signingTokens = globalSigningTokens;
}
catch (Exception)
{
LoggingUtilities.Logger.TraceWarning("Failed to get signing tokens.");
throw;
}
JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();
TokenValidationParameters validationParameters = new TokenValidationParameters
{
ValidAudience = audience,
ValidIssuer = issuer,
IssuerSigningTokens = signingTokens,
CertificateValidator = X509CertificateValidator.None
};
ClaimsPrincipal principal;
try
{
// Validate token.
SecurityToken validatedToken;
principal = tokenHandler.ValidateToken(
accessToken,
validationParameters,
out validatedToken);
}
catch (SecurityTokenValidationException)
{
LoggingUtilities.Logger.TraceWarning("Failed to validate the JWT.");
throw;
}
catch (Exception)
{
LoggingUtilities.Logger.TraceWarning("Failed to validate the JWT.");
throw;
}
return principal;
}
I used to have a reference to Microsoft.IdentityModel.Tokens.JWT and everything was working fine.
I updated to use the new System.IdentityModel.Tokens.Jwt but nothing seems to work now. It cannot find the ValidateToken method of the JwtSecurityTokenHandler and the TokenValidationParameters have no AllowedAudience, SigningToken or ValidateExpiration properties.
What am I missing here? Can anyone provide with a working sample of a JWT validation with this?
My "old" code :
private static void ValidateJwt(string jwt)
{
var handler = new JWTSecurityTokenHandler();
var validationParameters = new Microsoft.IdentityModel.Tokens.JWT.TokenValidationParameters()
{
AllowedAudience = "https://my-rp.com",
//SigningToken = new BinarySecretSecurityToken(Convert.FromBase64String(myBase64Key)),
SigningToken = new X509SecurityToken(
X509
.LocalMachine
.My
.Thumbprint
.Find("UYTUYTVV99999999999YTYYTYTY88888888", false)
.First()),
ValidIssuer = "https://my-issuer.com/trust/issuer",
ValidateExpiration = true
};
try
{
var principal = handler.ValidateToken(jwt, validationParameters);
}
catch (Exception e)
{
Console.WriteLine("{0}\n {1}", e.Message, e.StackTrace);
}
Console.WriteLine();
}
After a lot of research and tests, I finally found that some properties names for TokenValidationParameters had changed and JwtSecurityTokenHandler.ValidateToken() method signature too.
So here's the modified working version of the above code.
private static void ValidateJwt(string jwt)
{
var handler = new JwtSecurityTokenHandler();
var validationParameters = new TokenValidationParameters()
{
ValidAudience = "https://my-rp.com",
IssuerSigningTokens = new List<X509SecurityToken>() { new X509SecurityToken(
X509
.LocalMachine
.My
.Thumbprint
.Find("UYTUYTVV99999999999YTYYTYTY88888888", false)
.First()) },
ValidIssuer = "https://my-issuer.com/trust/issuer",
CertificateValidator = X509CertificateValidator.None,
RequireExpirationTime = true
};
try
{
SecurityToken validatedToken;
var principal = handler.ValidateToken(jwt, validationParameters, out validatedToken);
}
catch (Exception e)
{
Console.WriteLine("{0}\n {1}", e.Message, e.StackTrace);
}
Console.WriteLine();
}
And for the reference, the JwtSecurityTokenHandler lives in the System.IdentityModel.Tokens namespace. Don't forget to add the package "JSON Web Token Handler For the Microsoft .Net Framework 4.5" (version 4.0.0 at the time I write theses lines).
Hope it can save a few hours of search for some of you guys!