我已经使用快速入门模板设置了 IdentityServer4,以根据 Micorosft Entra ID(使用 FIDO2)对用户进行身份验证。
IdentityServer4 获取令牌并在ExternalController.Callback 方法中开始验证。当它最终执行这一行时:
var principal = handler.ValidateToken(token, validationParameters, out SecurityToken validatedToken);
它因以下异常而失败:
IDX10511:签名验证失败。尝试过的按键: 'Microsoft.IdentityModel.Tokens.X509SecurityKey,KeyId: 'MGLqj10VNLoXaFfpJCBpgB4JaKs',内部 ID: “MGLqj10VNLoXaFfpJCBpgB4JaKs”。 , 密钥 ID: MGLqj10VNLoXaFfpJCBpgB4JaKs Microsoft.IdentityModel.Tokens.RsaSecurityKey,KeyId: 'MGLqj10VNLoXaFfpJCBpgB4JaKs',内部 ID: 'EvI8giarv1jMMohnATIJ9o5MZ_J_rThL2EGO3Upamq4'。 , 密钥 ID: MGLqj10VNLoXaFfpJCBpgB4JaKs'。 钥匙数量 令牌验证参数:“12”。 配置中的按键数量: ‘0’。 匹配的密钥位于“TokenValidationParameters”中。 孩子: “MGLqj10VNLoXaFfpJCBpgB4JaKs”。 捕获的异常:''。令牌: “撤回”。有关详细信息,请参阅https://aka.ms/IDX10511。
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;
}
根据您的代码,您正在生成 Microsoft Graph API 的访问令牌:
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("email");
代币中的aud是
00000003-0000-0000-c000-000000000000
,scp是email openid profile
。
注意:仅必须验证为应用程序意味着或生成的访问令牌。
00000003-0000-0000-c000-000000000000
或 https://graph.microsoft.com
的访问令牌适用于 Microsoft Graph API,不应进行验证。当我通过示例解码令牌时,即使我得到了同样的错误:
如果您想为 Blazor 应用程序或应用程序生成访问令牌,请检查以下内容:
转到您的应用程序 -> 公开 API -> 添加范围
授予您添加的范围的 API 权限:
转到 API 权限 -> 添加权限 -> 我的组织使用的 API -> 搜索您的应用程序 -> 添加权限
现在修改代码并在生成令牌时将
scope 作为 api://ClientID/ScopeName
或
api://ClientID/.default
传递。
options.Scope.Add("api://ClientID/ScopeName");
在验证令牌时,将 audience 作为 ValidAudiences = new[] { _microsoftEntraIdSettings.ClientId }
传递 ClientID 或
api://ClientID
根据您生成的令牌。您可以参考我的