IdentityServer4验证Microsoft Entra ID令牌失败求助
我用IdentityServer4快速启动模板搭建系统,对接支持FIDO2的Microsoft Entra ID做用户认证。IdentityServer4获取令牌后,在ExternalController.Callback方法执行以下代码时:
var principal = handler.ValidateToken(token, validationParameters, out SecurityToken validatedToken);
抛出异常:
IDX10511: Signature validation failed. Keys tried:
'Microsoft.IdentityModel.Tokens.X509SecurityKey, KeyId:
'MGLqj10VNLoXaFfpJCBpgB4JaKs', InternalId:
'MGLqj10VNLoXaFfpJCBpgB4JaKs'. , KeyId: MGLqj10VNLoXaFfpJCBpgB4JaKs
Microsoft.IdentityModel.Tokens.RsaSecurityKey, KeyId:
'MGLqj10VNLoXaFfpJCBpgB4JaKs', InternalId:
'EvI8giarv1jMMohnATIJ9o5MZ_J_rThL2EGO3Upamq4'. , KeyId:
'MGLqj10VNLoXaFfpJCBpgB4JaKs '. Number of keys in
TokenValidationParameters: '12'. Number of keys in Configuration:
'0'. Matched key was in 'TokenValidationParameters'. kid:
'MGLqj10VNLoXaFfpJCBpgB4JaKs'. Exceptions caught: ''. token:
'RETRACTED'. See https://aka.ms/IDX10511 for details.
KID看起来匹配正常,但验证还是失败了,求解决建议?
builder.Services.AddAuthentication(options => { options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; }) .AddCookie() .AddOpenIdConnect("Microsoft Entra ID", options => { var microsoftEntraIdSettings = builder.Configuration.GetSection("MicrosoftEntraID").Get<MicrosoftEntraIDSettings>(); options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme; options.Authority = $"https://login.microsoftonline.com/{microsoftEntraIdSettings.TenantId}/v2.0"; options.ClientId = microsoftEntraIdSettings.ClientId; options.ClientSecret = microsoftEntraIdSettings.ClientSecret; options.ResponseType = "code"; options.SaveTokens = true; options.Scope.Add("openid"); options.Scope.Add("profile"); options.Scope.Add("email"); options.CallbackPath = "/signin-oidc"; options.Events = new OpenIdConnectEvents { OnRemoteFailure = context => { // Log detailed error information var error = context.Failure; context.Response.Redirect("/Home/Error?message=" + error?.Message); context.HandleResponse(); return Task.CompletedTask; }, OnTokenValidated = context => { return Task.CompletedTask; }, OnAuthenticationFailed = context => { return Task.CompletedTask; } }; });
[HttpGet] public async Task<IActionResult> Callback() { // read external identity from the temporary cookie var result = await HttpContext.AuthenticateAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme); if (result?.Succeeded != true) { throw new Exception("External authentication error"); } var token = result.Properties.GetTokenValue("access_token"); var validatedToken = await ValidateTokenAsync(token); if (validatedToken == null) throw new Exception("Token validation failed"); if (_logger.IsEnabled(LogLevel.Debug)) { var externalClaims = result.Principal.Claims.Select(c => $"{c.Type}: {c.Value}"); _logger.LogDebug("External claims: {@claims}", externalClaims); } // lookup our user and external provider info var (user, provider, providerUserId, claims) = FindUserFromExternalProvider(result); if (user == null) { // this might be where you might initiate a custom workflow for user registration // in this sample we don't show how that would be done, as our sample implementation // simply auto-provisions new external user user = AutoProvisionUser(provider, providerUserId, claims); } // this allows us to collect any additional claims or properties // for the specific protocols used and store them in the local auth cookie. // this is typically used to store data needed for signout from those protocols. var additionalLocalClaims = new List<Claim>(); var localSignInProps = new AuthenticationProperties(); ProcessLoginCallback(result, additionalLocalClaims, localSignInProps); // issue authentication cookie for user var isuser = new IdentityServerUser(user.SubjectId) { DisplayName = user.Username, IdentityProvider = provider, AdditionalClaims = additionalLocalClaims }; await HttpContext.SignInAsync(isuser, localSignInProps); // delete temporary cookie used during external authentication await HttpContext.SignOutAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme); // retrieve return URL var returnUrl = result.Properties.Items["returnUrl"] ?? "~/"; // check if external login is in the context of an OIDC request var context = await _interaction.GetAuthorizationContextAsync(returnUrl); await _events.RaiseAsync(new UserLoginSuccessEvent(provider, providerUserId, user.SubjectId, user.Username, true, context?.Client.ClientId)); // The client is native, so this change in how to // return the response is for better UX for the end user. if (context != null && context.IsNativeClient()) return this.LoadingPage("Redirect", returnUrl); return Redirect(returnUrl); } private async Task<ClaimsPrincipal> ValidateTokenAsync(string token) { var handler = new JwtSecurityTokenHandler(); IdentityModelEventSource.ShowPII = true; IdentityModelEventSource.LogCompleteSecurityArtifact = true; // Fetch the OpenID Connect configuration document var config = await GetOpenIdConnectConfigurationAsync(); if (config == null || !config.SigningKeys.Any()) { throw new Exception("No signing keys found in configuration"); } // Extract the Key ID (kid) from the token header var tokenKid = GetKidFromToken(token); _logger.LogInformation("Token kid: {TokenKid}", tokenKid); _logger.LogInformation("Config Signing Keys: {@Keys}", config.SigningKeys.Select(k => k.KeyId)); // Set up token validation parameters var validationParameters = new TokenValidationParameters { ValidIssuer = $"https://login.microsoftonline.com/{_microsoftEntraIdSettings.TenantId}/v2.0", ValidAudiences = new[] { _microsoftEntraIdSettings.ClientId }, IssuerSigningKeys = config.SigningKeys, //config.SigningKeys, // Use all keys from the configuration ValidateIssuerSigningKey = true, ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = false, ValidateTokenReplay = true, ClockSkew = new TimeSpan(2, 0, 0), // Additional logging for key retrieval IssuerSigningKeyResolver = (token, securityToken, kid, validationParameters) => { var keys = config.SigningKeys.Where(k => k.KeyId == kid).ToList(); _logger.LogInformation("Resolved keys for kid {Kid}: {@Keys}", kid, keys); return keys; } }; try { // Validate the token using the matched public key var principal = handler.ValidateToken(token, validationParameters, out SecurityToken validatedToken); _logger.LogInformation("Token validated successfully"); return principal; } catch (Exception ex) { _logger.LogError(ex, "Token validation failed"); throw; } } private async Task<OpenIdConnectConfiguration> GetOpenIdConnectConfigurationAsync() { try { // Fetch the OpenID Connect configuration document from Microsoft Entra ID var config = await _configurationManager.GetConfigurationAsync(CancellationToken.None); _logger.LogInformation("Fetched OpenID Connect configuration: {@Config}", config); return config; } catch (Exception ex) { _logger.LogError(ex, "Failed to fetch OpenID Connect configuration"); throw; } } private string GetKidFromToken(string token) { var handler = new JwtSecurityTokenHandler(); var jwtToken = handler.ReadJwtToken(token); return jwtToken.Header.Kid; }
检查令牌类型:你当前验证的是
access_token,但Microsoft Entra ID v2.0端点返回的access_token可能是不透明令牌(非JWT格式),尤其是当目标资源为Microsoft Graph等微软服务时。这类令牌无法通过JwtSecurityTokenHandler验证,建议改为验证id_token,将代码中result.Properties.GetTokenValue("access_token")替换为result.Properties.GetTokenValue("id_token")。修正Issuer与Audience配置:
- 确认
ValidIssuer与令牌的iss声明完全匹配,Entra ID v2.0的issuer格式为https://login.microsoftonline.com/{租户ID}/v2.0,注意租户ID需为纯ID值而非域名,且末尾的v2.0不可省略。 - 若验证的是access_token,
ValidAudiences应设置为目标资源的ID(如https://graph.microsoft.com),而非你的客户端ID;只有id_token的aud才是客户端ID,这是access_token验证失败的常见原因。
- 确认
移除自定义密钥解析器:你已通过
IssuerSigningKeys = config.SigningKeys配置了所有签名密钥,自定义的IssuerSigningKeyResolver可能与默认逻辑冲突,导致密钥匹配异常。删除该解析器,依赖默认的密钥匹配流程即可。开启令牌有效期验证:当前
ValidateLifetime = false关闭了有效期验证,建议将其设为true,并保留合理的ClockSkew(如默认的5分钟),避免因令牌过期或服务器时间不同步引发的隐性问题。刷新OpenID配置缓存:若
_configurationManager缓存了Entra ID的配置文档,可能存在密钥过期的情况。可以强制刷新配置,或调整缓存策略缩短过期时间,确保获取最新的签名密钥。核对密钥与令牌算法一致性:用工具解码令牌,查看头部的
alg(签名算法)和kid,对比Entra ID配置文档中的密钥,确认对应密钥的算法与令牌签名算法匹配(如令牌用RS256则密钥需为RSA类型)。
内容的提问来源于stack exchange,提问作者Banshee

