How to properly consume OpenID Connect jwks_uri metadata in C#?
This is what I ended up going with:
//Model the JSON Web Key Set
public class JsonWebKeySet
{
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore, NullValueHandling = NullValueHandling.Ignore, PropertyName = "keys", Required = Required.Default)]
public JsonWebKey[] Keys { get; set; }
}
//Model the JSON Web Key object
public class JsonWebKey
{
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore, NullValueHandling = NullValueHandling.Ignore, PropertyName = "kty", Required = Required.Default)]
public string Kty { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore, NullValueHandling = NullValueHandling.Ignore, PropertyName = "use", Required = Required.Default)]
public string Use { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore, NullValueHandling = NullValueHandling.Ignore, PropertyName = "kid", Required = Required.Default)]
public string Kid { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore, NullValueHandling = NullValueHandling.Ignore, PropertyName = "x5t", Required = Required.Default)]
public string X5T { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore, NullValueHandling = NullValueHandling.Ignore, PropertyName = "e", Required = Required.Default)]
public string E { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore, NullValueHandling = NullValueHandling.Ignore, PropertyName = "n", Required = Required.Default)]
public string N { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore, NullValueHandling = NullValueHandling.Ignore, PropertyName = "x5c", Required = Required.Default)]
public string[] X5C { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore, NullValueHandling = NullValueHandling.Ignore, PropertyName = "alg", Required = Required.Default)]
public string Alg { get; set; }
}
I first make a request to the jwks_uri
endpoint which is provided in the OpenID Connect discovery document. The request will populate the above objects accordingly. I then pass the JsonWebKeySet
object to a method that creates a ClaimsPrincipal
string idToken = "<the id_token that was returned from the Token endpoint>";
List<SecurityKey> keys = this.GetSecurityKeys(jsonWebKeySet);
var parameters = new TokenValidationParameters
{
ValidateAudience = true,
ValidAudience = tokenValidationParams.Audience,
ValidateIssuer = true,
ValidIssuer = tokenValidationParams.Issuer,
ValidateIssuerSigningKey = true,
IssuerSigningKeys = keys,
NameClaimType = NameClaimType,
RoleClaimType = RoleClaimType
};
var handler = new JwtSecurityTokenHandler();
handler.InboundClaimTypeMap.Clear();
SecurityToken jwt;
ClaimsPrincipal claimsPrincipal = handler.ValidateToken(idToken, parameters, out jwt);
// validate nonce
var nonceClaim = claimsPrincipal.FindFirst("nonce")?.Value ?? string.Empty;
if (!string.Equals(nonceClaim, "<add nonce value here>", StringComparison.Ordinal))
{
throw new AuthException("An error occurred during the authentication process - invalid nonce parameter");
}
return claimsPrincipal;
The GetSecurityKeys
method is implemented like so
private List<SecurityKey> GetSecurityKeys(JsonWebKeySet jsonWebKeySet)
{
var keys = new List<SecurityKey>();
foreach (var key in jsonWebKeySet.Keys)
{
if (key.Kty == OpenIdConnectConstants.Rsa)
{
if (key.X5C != null && key.X5C.Length > 0)
{
string certificateString = key.X5C[0];
var certificate = new X509Certificate2(Convert.FromBase64String(certificateString));
var x509SecurityKey = new X509SecurityKey(certificate)
{
KeyId = key.Kid
};
keys.Add(x509SecurityKey);
}
else if (!string.IsNullOrWhiteSpace(key.E) && !string.IsNullOrWhiteSpace(key.N))
{
byte[] exponent = Base64UrlUtility.Decode(key.E);
byte[] modulus = Base64UrlUtility.Decode(key.N);
var rsaParameters = new RSAParameters
{
Exponent = exponent,
Modulus = modulus
};
var rsaSecurityKey = new RsaSecurityKey(rsaParameters)
{
KeyId = key.Kid
};
keys.Add(rsaSecurityKey);
}
else
{
throw new PlatformAuthException("JWK data is missing in token validation");
}
}
else
{
throw new NotImplementedException("Only RSA key type is implemented for token validation");
}
}
return keys;
}
A RSA public key will always contains at least members kty
(with value RSA
), n
and e
(AQAB
i.e. 65537 public exponent for almost all keys).
Other members are optional and used to provide information about the key. In general, you will find the following recommended members:
- its ID (
kid
), - how to use it (signature or encryption)
- what algorithm they are designed for (
RS256
in your examples).
When the key comes from a X.509 certificate, you will often find x5t
or x5t#256
(sha1 and sha256 certificate thumbprints respectively).
Some systems are not able to use JWK directly and PKCS#1 keys are provided (x5c
member).
You can use either the (n
,e
) couple or the x5c
member (if provided). It depends on the capabilities of the library/third party application you use.