You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

JwtBearer中间件使用ES256返回401签名密钥未找到错误

ES256算法JwtBearer认证返回401 "The signature key was not found" 问题排查

问题现象

使用ES256签名算法的JwtBearer中间件执行认证时始终返回401状态码,响应错误信息为:

Bearer error="invalid_token", error_description="The signature key was not found"

切换回已调试通过的RSA签名方案时认证流程可正常运行;移除接口的[Authorize]特性后,手动调用自定义AppTokenHandler的Validate方法可成功验证令牌有效性,该方法使用的TokenValidationParameters与JwtBearer中间件配置完全一致,但中间件始终无法通过认证。已尝试直接传入完整ECDsa实例、注册带/不带kid的ECDsaSecurityKey单例、使用JsonWebKey单例、直接注册ECDsa实例等方案,问题均复现。

相关实现代码

令牌创建实现

令牌生成逻辑代码如下:

public class AppTokenHandler : TokenValidator, IAppTokenHandler
{
    private readonly JwtSecurityTokenHandler _handler = new JwtSecurityTokenHandler();
    private readonly AppTokenConfiguration _appTokenConfiguration;
    private readonly RsaSecurityKey _publicKey;
    private readonly ECDsa _key;

    public AppTokenHandler(IOptions<AppTokenConfiguration> appTokenConfiguration, RsaSecurityKey publicKey, ECDsa key)
    {
        _appTokenConfiguration = appTokenConfiguration.Value;
        _publicKey = publicKey;
        _key = key;
    }

    public string Create(Dictionary<string, object> claims)
    {
        var name = claims["name"].ToString();

        ////create token security key used to sign token from app's rsa private key
        //using var rsa = RSA.Create();
        //var rsaKey = _appTokenConfiguration.RsaKey;
        //rsa.ImportRSAPrivateKey(Convert.FromBase64String(rsaKey), out _);
        //RsaSecurityKey rsaSecurityKey = new(rsa);

        ////create signing credentials, specifying not to cache signature provider
        //SigningCredentials signingCredentials = new(rsaSecurityKey, SecurityAlgorithms.RsaSha256)
        //{
        //    CryptoProviderFactory = new CryptoProviderFactory { CacheSignatureProviders = false }
        //};

        SigningCredentials signingCredentials = new(new ECDsaSecurityKey(_key), SecurityAlgorithms.EcdsaSha256);

        // create token
        var tokenDescriptor = new SecurityTokenDescriptor
        {
            Audience = _appTokenConfiguration.Audience,
            Claims = claims,
            Expires = DateTime.UtcNow.AddDays(2),
            IssuedAt = DateTime.UtcNow,
            Issuer = _appTokenConfiguration.Issuer,
            SigningCredentials = signingCredentials,
            Subject = new ClaimsIdentity(new Claim[]
            {
                new Claim(ClaimTypes.NameIdentifier, name),
            })
        };

        var encodedJwt = _handler.CreateEncodedJwt(tokenDescriptor);

        return encodedJwt;
    }

    public override bool Validate(string tokenString, out JwtSecurityToken token, out SecurityTokenValidationException validationException)
    {
        validationException = null;
        token = null;

        var publicKey = ECDsa.Create(_key.ExportParameters(false));

        var validationParameters = new TokenValidationParameters
        {
            // validate lifetime
            RequireExpirationTime = true,
            ValidateLifetime = true,
            // validate audience
            RequireAudience = true,
            ValidateAudience = true,
            ValidAudience = _appTokenConfiguration.Audience,
            // validate issuer
            ValidateIssuer = true,
            ValidIssuer = _appTokenConfiguration.Issuer,
            // set source of name
            NameClaimType = "name",
            // validate signing key
            RequireSignedTokens = true,
            ValidateIssuerSigningKey = true,
            //IssuerSigningKey = _publicKey
            IssuerSigningKey = new ECDsaSecurityKey(publicKey)
        };
        
        try
        {
            var validate = _handler.ValidateToken(tokenString, validationParameters, out var validatedSecurityToken);
            token = _handler.ReadJwtToken(tokenString);
        }
        catch (SecurityTokenValidationException ex)
        {
            validationException = ex;
            return false;
        }
        catch
        {
            throw;
        }

        return true;
    }

    public Dictionary<string, object> MapClaims(JwtSecurityToken accessToken, JwtSecurityToken idToken)
    {
        List<string> claimKeys = new()
        {
            "name",
            "preferred_username",
            "oid",
            "tid",
            "azp",
            "family_name",
            "given_name",
            "email"
        };

        var claims = accessToken?
            .Claims
            .Where(x=>claimKeys.Contains(x.Type))
            .ToDictionary(x => x.Type, x => x.Value as object)
            ??
            new Dictionary<string, object>();

        var idTokenClaims = idToken
            .Claims
            .Where(x => claimKeys.Contains(x.Type))
            .ToDictionary(x => x.Type, x => x.Value as object);

        foreach (var claim in idTokenClaims.Where(x => !claims.ContainsKey(x.Key)))
            claims.Add(claim.Key, claim.Value);

        claims.Add("scp", "app_authorized_user");

        return claims;
    }
}

JwtBearer中间件配置

JWT Bearer认证参数配置代码如下:

public class AppTokenOptions
{
    public static Action<JwtBearerOptions> ConfigureToken(IServiceCollection services)
    {
        return options =>
        {
            var serviceProvider = services.BuildServiceProvider();
            var authConfig = serviceProvider.GetRequiredService<IOptions<AppTokenConfiguration>>();
            var publicKey = serviceProvider.GetRequiredService<RsaSecurityKey>();
            var privkey = serviceProvider.GetRequiredService<ECDsa>();
            //var key = ECDsa.Create(privkey.ExportParameters(false));
            options.IncludeErrorDetails = true;

            options.TokenValidationParameters = new TokenValidationParameters
            {
                // validate lifetime
                RequireExpirationTime = true,
                ValidateLifetime = true,
                // validate audience
                RequireAudience = true,
                ValidateAudience = true,
                ValidAudience = authConfig.Value.Audience,
                // validate issuer
                ValidateIssuer = true,
                ValidIssuer = authConfig.Value.Issuer,
                // set source of name
                NameClaimType = "name",
                // validate signing key
                RequireSignedTokens = true,
                ValidateIssuerSigningKey = true,
                //IssuerSigningKey = publicKey
                IssuerSigningKey = new ECDsaSecurityKey(ECDsa.Create(privkey.ExportParameters(false)))
            };

            options.Events = new JwtBearerEvents();

            options.Events.OnTokenValidated = async context =>
            {
                (context.Principal?.Identity as ClaimsIdentity)?.AddClaim(new Claim("cpcb", "test"));
            };
        };
    }
}

认证服务注册逻辑

Startup.cs的ConfigureServices方法中认证相关服务注册代码如下:

public static class AuthServicesRegistration
{
    public static IServiceCollection ConfigureApplicationAuthServices(this IServiceCollection services, IConfiguration Configuration)
    {
        // get relevant config sections
        IConfiguration appAuth= Configuration.GetSection("Auth:app");
        IConfiguration aadIdTokenAuth = Configuration.GetSection("Auth:AADIdToken");

        // create keys for app token
        using (RSA rsa = RSA.Create(3072))
        {
            string rsaKey = Convert.ToBase64String(rsa.ExportRSAPrivateKey());
            string rsaPublicKey = Convert.ToBase64String(rsa.ExportRSAPublicKey());

            appAuth["RsaKey"] = rsaKey;
            appAuth["RsaPublicKey"] = rsaPublicKey;
        }

        // bind auth configs
        services.Configure<AppTokenConfiguration>(appAuth);
        services.Configure<AzureAdIdTokenConfiguration>(aadIdTokenAuth);

        // add public key instance as singleton
        // so it can be used in .net's token validation middleware
        // otherwise if just declared when defined token validation parameters
        // the RSA instance will be prematurely disposed and you will get misleading 401s
        services.AddSingleton(provider => {
            RSA rsa = RSA.Create();
            rsa.ImportRSAPublicKey(Convert.FromBase64String(appAuth["RsaPublicKey"]), out _);
            return new RsaSecurityKey(rsa);
        });

        services.AddSingleton(provider =>
        {
            return ECDsa.Create(ECCurve.NamedCurves.nistP256);
        });

        // add provider for microsoft openidconnect config
        services.AddSingleton<IOpenIdConnectConfigurationProvider>(provider =>
        {
            var stsDiscoveryEndpoint = "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration";
            var configProvider = new OpenIdConnectConfigurationProvider(stsDiscoveryEndpoint, new OpenIdConnectConfigurationRetriever());
            configProvider.AutomaticRefreshInterval = TimeSpan.FromHours(1);
            return configProvider;
        });

        // add authentication schemes
        // default is app token
        services.AddAuthentication("app")
            .AddJwtBearer("app", AppTokenOptions.ConfigureToken(services))
            .AddMicrosoftIdentityWebApi(Configuration, "Auth:AzureAd", "aad");
        
        // configure aad token options
        services.Configure("aad", AzureAdTokenOptions.ConfigureAadToken());

        // add authorization
        services.AddAuthorization();

        // add auth related services
        services.AddScoped<IAppTokenHandler, AppTokenHandler>();
        services.AddScoped<ITokenValidator, MicrosoftIdTokenValidator>();

        return services;
    }
}

测试接口实现

令牌生成、令牌验证测试接口代码如下:

[ApiController]
[Route("api/auth")]
public class AuthController : ControllerBase
{
    private readonly IAppTokenHandler _appTokenHandler;
    private readonly ITokenValidator _idTokenValidator;

    public AuthController(IAppTokenHandler tokenHandler, ITokenValidator idTokenValidator) : base()
    {
        _appTokenHandler = tokenHandler;
        _idTokenValidator = idTokenValidator;
    }

    /// <summary>
    /// Returns token for requested resource verifying using msal accesstoken
    /// </summary>
    /// <returns></returns>
    [Authorize(AuthenticationSchemes = "aad")]
    [RequiredScope(AcceptedScope = new[] { "app_login" })]
    [Route("token")]
    [HttpGet]
    public async Task<IActionResult> token()
    {
        if (!Request.Headers.TryGetValue("identity", out var idTokenString)) return Unauthorized("No identity present to verify");

        var aadToken = await HttpContext.GetTokenAsync("aad", "access_token");
        
        JwtSecurityToken accessToken = null;
        if (aadToken != null)
        {
            accessToken = new JwtSecurityToken(aadToken);
        }
        
        // verify idToken
        if (!_idTokenValidator.Validate(idTokenString, out var idToken, out var validationException))
        {
            return Unauthorized($"invalid id token: {validationException.Message}");
        }
        
        // get / verify user
        // get claims from tokens
        var claims = _appTokenHandler.MapClaims(accessToken, idToken);

        // generate token
        var encodedJwt = _appTokenHandler.Create(claims);

        return Ok(encodedJwt);
    }

    /// <summary>
    /// Test authorize endpoint
    /// </summary>
    /// <returns></returns>
    [Authorize]
    [RequiredScope(AcceptedScope = new[] { "app_authorized_user" })]
    [Route("validate/{token}")]
    [HttpGet]
    public async Task<IActionResult> validate(string token)
    {
        var authorization = Request.Headers.Authorization.ToString().Substring("Bearer ".Length).Trim();
        try
        { 
            // both validate calls are successful when authorize attribute is commented out
            if (_appTokenHandler.Validate(token, out _, out var ex))
            {
                Debug.WriteLine("valid token");
            }
            else
            {
                Debug.WriteLine("invalid token", ex.Message);
            }

            if (_appTokenHandler.Validate(authorization, out _, out var ex2))
            {
                Debug.WriteLine("valid auth header");
            }
            else
            {
                Debug.WriteLine("invalid auth header", ex2.Message);
            }
        }
        catch (Exception exc)
        {
            Debug.WriteLine(exc.Message);
        }

        return Ok(token);
    }
}

待排查疑问

  • 签名密钥的创建方式是否存在错误?
  • 向JwtBearer中间件传入公钥/私钥的方式是否不正确?
  • 令牌创建流程是否遗漏了必要配置?
  • 如何挂钩JwtBearer中间件的事件获取更详细的错误排查信息?已知OnTokenValidated事件因认证失败不会触发,是否存在其他可用于定位问题的事件钩子?

内容的提问来源于stack exchange,提问作者pbordeaux

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 21:33:25