从 Microsoft Entra ID 验证令牌失败?

问题描述 投票:0回答:1

我已经使用快速入门模板设置了 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;
}
c# azure oauth openid microsoft-entra-id
1个回答
0
投票

根据您的代码,您正在生成 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

注意:仅必须验证为应用程序意味着或生成的访问令牌。

  • 带有 aud
    00000003-0000-0000-c000-000000000000
    https://graph.microsoft.com
    的访问令牌适用于 Microsoft Graph API,不应进行验证。
  • Microsoft Graph API 令牌使用不同的签名方式,您无法使用相同的方法来验证 Microsoft Graph API 令牌。
  • 因此,您需要通过在应用程序注册的“公开 API”边栏选项卡中传递为 API 定义的范围来生成令牌。
  • 您必须获得 API 的访问令牌。

当我通过示例解码令牌时,即使我得到了同样的错误

enter image description here

如果您想为 Blazor 应用程序或应用程序生成访问令牌,请检查以下内容:

转到您的应用程序 -> 公开 API -> 添加范围

enter image description here

授予您添加的范围的 API 权限:

转到 API 权限 -> 添加权限 -> 我的组织使用的 API -> 搜索您的应用程序 -> 添加权限

enter image description here

现在修改代码并在生成令牌时将

scope 作为 api://ClientID/ScopeName

api://ClientID/.default
 传递。

options.Scope.Add("api://ClientID/ScopeName");
在验证令牌时,将 

audience 作为 ValidAudiences = new[] { _microsoftEntraIdSettings.ClientId }

 传递 ClientID 或 
api://ClientID
 根据您生成的令牌。

您可以参考我的

SO Thread来验证API的令牌。

© www.soinside.com 2019 - 2024. All rights reserved.