如何在C#中正确使用OpenID Connect jwks_uri元数据?

17

OpenID Connect发现文档通常包含一个jwks_uri属性。从jwks_uri返回的数据似乎至少采取两种不同形式。其中一种形式包含称为x5cx5t的字段。其中一个示例如下:

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

我看到的另一个版本省略了x5c和x5t属性,但包含en。这样的一个例子是:

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

我正在使用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 ?
}

考虑到这两种不同的JWK格式,我该如何使用它们来提供IssuerSigningKeyTokenValidationParameter,以验证访问令牌?


3
非常细微的反馈/更正:「Every OpenID Connect provider publishes a discovery document...」并不完全正确。 「Discovery」是规范中的可选部分,有些提供程序可能没有实现它。 咳嗽声苹果���司咳嗽声: https://bitbucket.org/openid/connect/src/default/How-Sign-in-with-Apple-differs-from-OpenID-Connect.md - Greg Pendlebury
@GregPendlebury 您说得完全正确。我会进行更新。感谢您指出这一点。 - Rob L
3个回答

17
这是我最初选择的内容。请注意,这些类型现在可以在Microsoft.IdentityModel.Tokens NuGet包中使用,但为了清晰起见,我将在这里展示它们:
//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; }
}

我首先向OpenID Connect发现文档中提供的`jwks_uri`端点发送请求。该请求将相应地填充上述对象。然后,我将`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)
          {
              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;
  }

OpenIdConnectConstants和Base64Url定义在哪里?谢谢。 - owade
1
@owade OIDC常量存储在一个包含常量的文件中。Rsa属性只是值为“RSA”的简单属性。Base64Url只是一个静态函数,类似于此处第三个答案 https://dev59.com/13M_5IYBdhLWcg3wymU0 - Rob L
4
我从互联网的尽头来到这里找答案......有趣的是,关于如何理解IssuerSigningKeys和令牌验证的信息非常少。我很高兴找到了你的作品,Rob。 - Maurice Klimek
谢谢您提供的解决方案。我对OAuth还不是很熟悉。当API从Azure AD接收到令牌时,这种验证是否足够?我们需要更多的控制吗? - CageE
1
JsonWebKeySetJsonWebKeyGetSigningKeys现在已经在Microsoft.IdentityModel.Tokens中实现。 - Dude Pascalou

11

RSA公钥至少包含以下成员kty(值为RSA)、ne(对于几乎所有密钥,AQAB即65537是公共指数)。

其他成员是可选的,用于提供有关密钥的信息。通常,您会发现以下推荐的成员:

  • 其ID(kid),
  • 如何使用它(签名或加密)
  • 它们专为什么算法设计(例如,在您的示例中RS256)。

当密钥来自X.509证书时,您经常会发现x5tx5t#256(分别为sha1和sha256证书指纹)。一些系统无法直接使用JWK,因此提供了PKCS#1密钥(x5c成员)。

您可以使用(n, e)组或x5c成员(如果提供)。这取决于您使用的库/第三方应用程序的功能。


2
稍微扩展一下Florent的评论:{{x5c}}(证书链),如果解码(例如,尝试运行{{openssl x509 -in certificate.pem -text -noout}}),会显示一个模数和指数。这些与JWK规范中的{{n}}和{{e}}值相同(以不同的方式编码;证书输出以十六进制显示,而JWK参数以base64url编码显示)。这是呈现相同信息的不同方式。最终,在使用密钥进行验证时,重要的是模数和指数。 - Mark

7

稍作更新 - Microsoft.IdentityModel.Tokens nuget 包含具有构造函数的 JsonWebKey,该构造函数采用 jwk JSON 字符串作为参数。

// 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();

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接