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

.NetCore 3.1验证Azure AD生成的V1 JWT令牌签名始终返回false求排查

Azure AD V1 JWT验证代码问题排查与修正

存在的核心错误点

  • 冗余的签名校验逻辑:前半部分已经初始化了TokenValidationParameters和OpenID配置管理器,但是自定义的SignatureValidator直接返回JWT对象,完全跳过了官方类库内置的签名校验逻辑;后半部分又重复请求公钥接口手动校验,两套逻辑完全割裂,且前面的配置没有实际生效。
  • 异步方法错误使用:configManager.GetConfigurationAsync调用了.Result(),在ASP.NET Core上下文中容易引发死锁,同时你已经通过配置管理器拿到了SigningKeys,再单独请求公钥接口属于重复操作,完全多余。
  • Base64编码处理错误:JWT使用的是Base64Url编码,不是标准Base64,直接调用Convert.FromBase64String解码header会因-/_字符、缺少padding等问题抛出异常,被你吞掉后直接返回false。
  • 签名处理完全错误:你直接对signature字符串做UTF8.GetBytes,正确操作应该是先将Base64Url格式的signature字符串解码为字节数组,再用于校验。
  • 哈希校验逻辑错误:rsa.VerifyHash接收的第一个参数是原始内容的哈希值,你直接传入header.payload的原始字节,没有提前计算SHA256哈希,校验必然失败。
  • 变量与异常处理问题:isValid未初始化就返回,且catch块吞掉了所有异常,运行中的报错完全无法感知,多数情况下返回的都是默认值false。
  • 公钥端点不匹配:如果你的应用是单租户应用,使用通用端点拉取公钥可能和令牌签发的租户公钥不匹配,导致kid校验失败。

修正后的代码

优先推荐直接用官方JwtSecurityTokenHandler完成校验,不需要手动实现签名逻辑,稳定性和安全性更高:

public async Task<bool> ValidateAsync(string token)
{
    try
    {
        ConfigurationManager<OpenIdConnectConfiguration> configManager = new ConfigurationManager<OpenIdConnectConfiguration>(
            this.tokenSettings.StsDiscoveryEndpoint, 
            new OpenIdConnectConfigurationRetriever());
        // 改用await调用异步方法,避免死锁
        OpenIdConnectConfiguration config = await configManager.GetConfigurationAsync(CancellationToken.None);
        
        TokenValidationParameters validationParameters = new TokenValidationParameters
        {
            ValidIssuer = this.tokenSettings.IssuerValidator,
            ValidAudience = this.tokenSettings.Audience,
            IssuerSigningKeys = config.SigningKeys,
            ValidateAudience = true,
            ValidateIssuer = true,
            ValidateLifetime = true,
            // 去掉错误的SignatureValidator自定义逻辑,使用默认签名校验
        };

        JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();
        tokenHandler.ValidateToken(token, validationParameters, out SecurityToken validatedToken);
        // 校验通过返回true
        return validatedToken != null;
    }
    catch (Exception ex)
    {
        // 可在此处打印ex.Message排查具体校验失败原因,比如颁发者不匹配、受众错误、令牌过期等
        return false;
    }
}

如果确实需要手动校验签名,需要补充Base64Url转码、哈希计算的逻辑,同时不要遗漏颁发者、受众、有效期的校验:

// 补充Base64Url解码方法
private byte[] Base64UrlDecode(string input)
{
    input = input.Replace('-', '+').Replace('_', '/');
    switch (input.Length % 4)
    {
        case 2: input += "=="; break;
        case 3: input += "="; break;
    }
    return Convert.FromBase64String(input);
}

public async Task<bool> ValidateAsync(string token)
{
    bool isValid = false;
    try
    {
        var allParts = token.Split(".");
        if (allParts.Length != 3) return false;
        
        string header = allParts[0];
        string payload = allParts[1];
        string signature = allParts[2];
        
        // Base64Url解码header获取kid
        var headerJson = Encoding.UTF8.GetString(Base64UrlDecode(header));
        var kid = JObject.Parse(headerJson)["kid"].ToString();
        
        // 复用配置管理器拿到的SigningKeys,无需重复请求接口
        ConfigurationManager<OpenIdConnectConfiguration> configManager = new ConfigurationManager<OpenIdConnectConfiguration>(
            this.tokenSettings.StsDiscoveryEndpoint, 
            new OpenIdConnectConfigurationRetriever());
        OpenIdConnectConfiguration config = await configManager.GetConfigurationAsync(CancellationToken.None);
        var signingKey = config.SigningKeys.FirstOrDefault(k => k.KeyId == kid) as X509SecurityKey;
        if (signingKey == null) return false;

        byte[] bytesToSign = Encoding.UTF8.GetBytes(string.Join(".", header, payload));
        // 先计算待校验内容的SHA256哈希
        byte[] hashBytes = SHA256.Create().ComputeHash(bytesToSign);
        // Base64Url解码签名内容
        byte[] signatureToCheck = Base64UrlDecode(signature);
        
        using (RSA rsa = signingKey.Certificate.GetRSAPublicKey())
        {
            isValid = rsa.VerifyHash(hashBytes, signatureToCheck, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
        }
        
        // 签名校验通过后,补充校验颁发者、受众、过期时间等核心参数
        if (isValid)
        {
            var payloadJson = Encoding.UTF8.GetString(Base64UrlDecode(payload));
            var payloadObj = JObject.Parse(payloadJson);
            if (payloadObj["iss"].ToString() != this.tokenSettings.IssuerValidator
                || payloadObj["aud"].ToString() != this.tokenSettings.Audience
                || payloadObj["exp"].ToObject<long>() < DateTimeOffset.UtcNow.ToUnixTimeSeconds())
            {
                isValid = false;
            }
        }
    }
    catch (Exception ex)
    {
        // 可在此处打印异常日志排查问题
        isValid = false;
    }
    return isValid;
}

内容的提问来源于stack exchange,提问作者Scorpy47

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.24 14:24:04