17

OpenID Connect discovery documents typically include a jwks_uri property. The data returned from the jwks_uri seems to take on at least two different forms. One form contains fields called x5c and x5t. An example of this looks like:

{
    "keys": [
        {
            "kty": "RSA",
            "use": "sig",
            "kid": "C61F8F2524D080D0DB0A508747A94C2161DEDAC8",
            "x5t": "xh-PJSTQgNDbClCHR6lMIWHe2sg", <------ HERE
            "e": "AQAB",
            "n": "lueb...",
            "x5c": [
                "MIIC/..." <------ HERE
            ],
            "alg": "RS256"
        }
    ]
}

The other version that I see omits the x5c and x5t properties but contains e and n. An example of this is:

{
    "keys": [
        {
            "kty": "RSA",
            "alg": "RS256",
            "use": "sig",
            "kid": "cb11e2f233aee0329a5344570349cddb6b8ff252",
            "n": "sJ46h...", <------ HERE
            "e": "AQAB"      <------ HERE
        }
    ]
}

I am using C#'s Microsoft.IdentityModel.Tokens.TokenValidationParameters and I am trying to figure out how to supply the property IssuerSigningKey. A sample usage of this class is

new TokenValidationParameters
{
    ValidateAudience = true,
    ValidateIssuer = true,
    ...,
    IssuerSigningKey = new X509SecurityKey(???) or new JsonWebKey(???) //How to create this based on x5c/x5t and also how to create this based on e and n ?
}

Given these two different JWK formats, how do I use them to provide the IssuerSigningKey to the TokenValidationParameter so I can validate access tokens?

Rob L
  • 3,073
  • 6
  • 31
  • 61
  • 3
    Very minor feedback/correction: "Every OpenID Connect provider publishes a discovery document..." is not quiet true. 'Discovery' is an optional part of the spec and some providers might not implement it. *cough* Apple *cough*: https://bitbucket.org/openid/connect/src/default/How-Sign-in-with-Apple-differs-from-OpenID-Connect.md – Greg Pendlebury Oct 21 '19 at 02:59
  • @GregPendlebury You are absolutely correct. I will update this. Thanks for pointing that out. – Rob L Oct 22 '19 at 21:27

3 Answers3

17

Here is what I ended up going with initially. Note that these types are now available in the Microsoft.IdentityModel.Tokens NuGet package, but I'll show them here for clarity:

//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)
          {
              throw new NotImplementedException("Only RSA key type is implemented for token validation");
          }

          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);
                 continue;
           }
           
           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);
                  continue;
           }
           
           // Throw Exception if need be
           // throw new ConfigurationException("Missing or incomplete JWK data");
      }

      return keys;
  }
Rob L
  • 3,073
  • 6
  • 31
  • 61
  • Where are OpenIdConnectConstants and Base64Url defined? Thanks – owade Sep 27 '18 at 09:00
  • 1
    @owade The OIDC constants are in a file that contains constants. The Rsa property is simply the value "RSA". The Base64Url is just a static function that looks like the 3rd answer seen here https://stackoverflow.com/questions/1228701/code-for-decoding-encoding-a-modified-base64-url – Rob L Sep 27 '18 at 09:07
  • 4
    I came from the end of the Internet to reach this answer... It's funny how there is so little info on how to understand `IssuerSigningKeys` and token validation. I'm so happy I found your work, Rob. – Maurice Klimek Apr 02 '20 at 13:55
  • Thanks for the solution. I'm pretty new to OAuth. Is this validation enough when for example API receives the token from Azure AD? Do we need more controls? – CageE Jul 05 '20 at 18:52
  • 1
    `JsonWebKeySet`, `JsonWebKey`and `GetSigningKeys` are now implemented in [Microsoft.IdentityModel.Tokens](https://learn.microsoft.com/en-us/dotnet/api/microsoft.identitymodel.tokens). – Dude Pascalou Feb 24 '23 at 16:55
10

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.

Spomky-Labs
  • 15,473
  • 5
  • 40
  • 64
  • 2
    To expand on Florent's comment a bit: {{x5c}} (the certificate chain), if decoded (for example, try running {{openssl x509 -in certificate.pem -text -noout}}, shows a modulus and exponent. These are the same as {{n}} and {{e}} values in the JWK spec (encoded differently; the cert output shows in hex while the JWK params show in base64url-encoding. It's a different way of presenting the same information. Ultimately what matters when verifying using keys is the modulus and exponent. – Mark Mar 21 '19 at 22:27
6

A bit of an update - the Microsoft.IdentityModel.Tokens nuget includes the JsonWebKey with a constructor that takes jwk JSON string.

// JSON class
public class OpenIdConnectKeyCollection
{
    [JsonProperty("keys")]
    public ICollection<JToken> JsonWebKeys { get; set; }
}  
  
// map the keys using the JSON ctor
var jsonKeys = keysResp.JsonWebKeys;
var jwk = jsonKeys
    .Select(k => new JsonWebKey(k.ToString()))
    .ToList();
JDH
  • 308
  • 4
  • 8