web-dev-qa-db-ja.com

C#でOpenID Connect jwks_uriメタデータを適切に使用する方法

OpenID Connect仕様に従って、プロバイダーはjwks_uriプロパティを含むディスカバリドキュメントを公開できます。 jwks_uriから返されるデータは、2つの異なる形式をとるようです。 1つのフォームには、x5cおよびx5tというフィールドが含まれています。この例は次のようになります。

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

私が目にする他のバージョンでは、x5cおよびx5tプロパティが省略されていますが、eおよびnが含まれています。この例は次のとおりです。

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

違いが何であるか、なぜx5c/x5tの組み合わせが使用されるのか、また単にe/nである理由がよくわかりません。私はC#のMicrosoft.IdentityModel.Tokens.TokenValidationParametersを使用しており、プロパティIssuerSigningKeyを指定する方法を理解しようとしています。このクラスの使用例は次のとおりです。

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 ?
}

2つの異なるJWKメタデータ形式を考慮して、それらを使用してIssuerSigningKeyTokenValidationParameterに提供し、アクセストークンを検証できるようにするにはどうすればよいですか?

9
Rob L

これは私が最終的に行ったことです:

//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; }
}

最初にjwks_uriエンドポイントは、OpenID Connectディスカバリドキュメントで提供されます。リクエストにより、上記のオブジェクトが適宜入力されます。次に、JsonWebKeySetオブジェクトを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;

GetSecurityKeysメソッドは次のように実装されています

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;
  }
7
Rob L

RSA公開鍵には、常に少なくともメンバーkty(値RSA)、nおよびeAQAB、つまり65537パブリックが含まれます。ほとんどすべてのキーの指数)。

他のメンバーはオプションであり、キーに関する情報を提供するために使用されます。一般的に、次の推奨メンバーが見つかります。

  • そのID(kid)、
  • 使用方法(署名または暗号化)
  • それらがどのアルゴリズムで設計されているか(例ではRS256)。

キーがX.509証明書からのものである場合、x5tまたはx5t#256(それぞれsha1およびsha256証明書のサムプリント)が見つかることがよくあります。一部のシステムはJWKを直接使用できず、PKCS#1キーが提供されます(x5cメンバー)。

ne)カップルまたはx5cメンバー(提供されている場合)を使用できます。これは、使用するライブラリ/サードパーティアプリケーションの機能によって異なります。

6