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

Posted

技术标签:

【中文标题】如何在 C# 中正确使用 OpenID Connect jwks_uri 元数据?【英文标题】:How to properly consume OpenID Connect jwks_uri metadata in C#? 【发布时间】:2018-04-17 17:14:01 【问题描述】:

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 格式,我如何使用它们将 IssuerSigningKey 提供给 TokenValidationParameter,以便我可以验证访问令牌?

【问题讨论】:

非常小的反馈/更正:“每个 OpenID Connect 提供者都发布了一个发现文档……”并不是真的。 “发现”是规范的可选部分,一些提供者可能不会实现它。 咳嗽苹果咳嗽:bitbucket.org/openid/connect/src/default/… @GregPendlebury 你是绝对正确的。我会更新这个。感谢您指出这一点。 【参考方案1】:

这就是我最终的结果:

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

【讨论】:

OpenIdConnectConstants 和 Base64Url 定义在哪里?谢谢 @owade OIDC 常量位于包含常量的文件中。 Rsa 属性就是值“RSA”。 Base64Url 只是一个静态函数,看起来像这里看到的第三个答案***.com/questions/1228701/… 我从互联网的尽头来到这个答案......有趣的是,关于如何理解IssuerSigningKeys 和令牌验证的信息如此之少。我很高兴找到你的作品,Rob。 感谢您的解决方案。我对 OAuth 很陌生。例如,当 API 从 Azure AD 接收令牌时,此验证是否足够?我们需要更多控制吗?【参考方案2】:

RSA 公钥将始终至少包含成员 kty(值为 RSA)、neAQAB,即几乎所有密钥的 65537 公钥)。

其他成员是可选的,用于提供有关密钥的信息。 一般来说,你会发现以下推荐的成员:

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

当密钥来自 X.509 证书时,您通常会找到 x5tx5t#256(分别为 sha1 和 sha256 证书指纹)。 某些系统无法直接使用 JWK,并且提供了 PKCS#1 密钥(x5c 成员)。

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

【讨论】:

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

一点更新 - Microsoft.IdentityModel.Tokens nuget 包括采用 jwk JSON 字符串的 JsonWebKey with a constructor。

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

【讨论】:

以上是关于如何在 C# 中正确使用 OpenID Connect jwks_uri 元数据?的主要内容,如果未能解决你的问题,请参考以下文章

请问使用C#怎么得到微信公众号获得关注者openid

使用 C# 以编程方式向 MVC 控制器验证 Azure Ad/OpenId 并获取重定向 uri

SSIS 包未启动 - 待执行

如何使用 C# 在 OSX 中正确运行 chmod?

如何正确使用 SendInput() 在 C# 中模拟鼠标输入

使用 openid connect 时如何确保客户端服务的 GDPR/ToS 合规性?