使用EdDSA签名在ASP.NET Core中验证OpenID Connect令牌

本文详细介绍了如何在ASP.NET Core中使用EdDSA算法验证OpenID Connect令牌,包括密钥加载方法和改进的配置检索器实现,解决默认设置无法验证EdDSA签名的问题。

使用EdDSA签名在ASP.NET Core中使用OpenID Connect验证令牌

2025年8月6日 · by damienbod · in .NET Core, ASP.NET Core, OAuth2, OpenId, Security, Uncategorized · 1条评论

某些身份提供商使用EdDSA / ED25519算法来签名和颁发令牌。本文展示了如何使用ScottBrady的Nuget包和ASP.NET Core来验证这些令牌。使用默认的OpenID Connect设置时,密钥不会被读取,令牌也无法验证。

错误消息可能返回如下内容:

1
IDX10511: Signature validation failed. Keys tried: 'Microsoft.IdentityModel.Tokens.JsonWebKey

使用ScottBrady.IdentityModel Nuget包来实现此需求,感谢Scott创建了这个包。

密钥将按照标准中规定的web格式在OpenID Connect服务器上发布。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{
  "keys": [
    {
      "alg": "EdDSA",
      "crv": "Ed25519",
      "kid": ".....",
      "kty": "OKP",
      "x": "....."
    }
  ]
}

OpenID Connect服务器提供了一个JWK端点,用于发布公钥。这用于验证所颁发令牌的签名。可以使用JsonWebKeySet类型读取密钥列表,并可转换为EdDsaSecurityKey密钥。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
using Microsoft.IdentityModel.Tokens;
using Org.BouncyCastle.Tls;
using ScottBrady.IdentityModel.Crypto;
using ScottBrady.IdentityModel.Tokens;
using System.Security.Cryptography.X509Certificates;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace RazorPageOidcClient;

public class LoadPublicSigningKeys
{
    public static async Task<IEnumerable<EdDsaSecurityKey>> LoadEdDsaKeysAsync(string jwkUrl)
    {
        using var httpClient = new HttpClient();
        var jwkJson = await httpClient.GetStringAsync(jwkUrl);
        
        var jwkSet = JsonSerializer.Deserialize<JsonWebKeySet>(jwkJson);
        if(jwkSet == null)
        {
            throw new ArgumentNullException("Jwk endpoint not working or not found");
        }
        
        var keys = new List<EdDsaSecurityKey>();
        
        foreach (var key in jwkSet.Keys.Where(k => k.Alg == "EdDSA" && k.Crv == "Ed25519"))
        {
            // 解码公钥
            byte[] publicKeyBytes = Base64UrlEncoder.DecodeBytes(key.X);
            
            // 仅使用公钥创建EdDSA参数
            var parameters = new EdDsaParameters(ExtendedSecurityAlgorithms.Curves.Ed25519)
            {
                X = publicKeyBytes
            };
            
            // 创建EdDSA公钥
            var edDsa = EdDsa.Create(parameters);
            
            keys.Add(new EdDsaSecurityKey(edDsa));
        }
        
        return keys;
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
var keys = LoadPublicSigningKeys
   .LoadEdDsaKeysAsync("https://{authority}/.well-known/jwks.json")
   .GetAwaiter().GetResult();

services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
}).AddCookie().AddOpenIdConnect( options =>
{
    ...
    

});

像这样读取密钥并不是最优的。最好直接挂钩到OpenID Connect已知端点的默认读取中。

改进的解决方案

感谢Frank Quednau

创建OIDC检索器:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
internal class OkpEnrichedRetriever : IConfigurationRetriever<OpenIdConnectConfiguration>
{
    private readonly IConfigurationRetriever<OpenIdConnectConfiguration> baseImplementation 
            = new OpenIdConnectConfigurationRetriever();
    
    public async Task<OpenIdConnectConfiguration> GetConfigurationAsync(string address, 
        IDocumentRetriever retriever, CancellationToken cancel)
    {
        var config = await baseImplementation.GetConfigurationAsync(address, retriever, cancel);
        
        foreach (var jsonWebKey in config.JsonWebKeySet.Keys)
        {
            if (ExtendedJsonWebKeyConverter.TryConvertToEdDsaSecurityKey(jsonWebKey, out var key))
            {
                config.SigningKeys.Add(key);
            }
        }
        return config;
    }
}

这可以按如下方式使用:

1
2
3
4
5
6
7
8
9
services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
}).AddCookie().AddOpenIdConnect( options =>
{
    options.ConfigurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(
        $"{authority}/.well-known/openid-configuration",
        new OkpEnrichedRetriever());

现在你有了一个更清晰的解决方案。

链接

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计