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
相关产品推荐
相关产品推荐

