Reputation: 350
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?
Upvotes: 0
Views: 2859
Reputation: 350
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;
}
Upvotes: 1