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

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看起来匹配正常,但验证还是失败了,求解决建议?

Program.cs 认证配置代码
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;
        }
    };
});
ExternalController 相关代码
[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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 03:27:33