跳到主要内容
ecdsa.com
.NET

Microsoft.IdentityModel (JsonWebTokenHandler)

Microsoft.IdentityModel 的 JsonWebTokenHandler,ASP.NET Core 身份验证和 Microsoft Entra ID SDK 底层的 JWT 验证器:默认时钟偏差容差为五分钟,要求 exp,并接受用 P-384 密钥签名的 ES256 令牌。
许可证
MIT
检查的版本
8.23.0 ·
源代码
github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet
文档
learn.microsoft.com/dotnet/api/microsoft.identitymodel.jsonwebtokens.jsonwebtokenhandler

兼容性概览

JOSE 与 JWT

JWS 算法
HS256HS384HS512RS256RS384RS512PS256PS384PS512ES256ES384ES512

Microsoft.IdentityModel.JsonWebTokens 通过 JsonWebTokenHandler 和 TokenValidationParameters 对象创建并验证 JWT;ASP.NET Core 中的 AddJwtBearer 让请求都经由它处理,因此它的默认设置就是大多数 .NET API 的默认设置。8.23.0 对 HS、RS、PS 和 ES 系列的签名与验证均成功;该包中没有 EdDSA 的安全密钥类型。

ValidAlgorithms 默认为 null,由密钥决定:用 EC 密钥验证 RS256 令牌和用该 EC 密钥的 PEM 文本伪造的 HS256 令牌时,都报错 IDX10517: Signature validation failed.;除非把 RequireSignedTokens 设为 false,否则未签名的令牌会报错 IDX10504。缺口在于曲线绑定:用 P-384 密钥也能签出 ES256 令牌,同一个处理程序还会让它验证通过,而其他库——jose、PyJWT、golang-jwt——都会拒绝。应像示例那样设置 ValidAlgorithms,并使用长度正确的密钥。

有效期规则与大多数库正好相反:exp 是必需的(IDX10225),而 TokenValidationParameters.DefaultClockSkew 为五分钟,因此令牌过期后最多五分钟内仍然可用。对短期访问令牌来说,这个容差相当宽松;应显式设置 ClockSkew。即使没有设置 Expires,CreateToken 也会添加 exp。

ES256 签名是 JWS 要求的 64 字节原始 r‖s,与 .NET 自身 ECDsa 的默认格式一致;DER 签名会报出与伪造签名相同的 IDX10517。除非在调试时启用 IdentityModelEventSource.ShowPII,否则错误信息会把令牌内容隐藏为 [PII of type ... is hidden]——可以改用 JWT 调试器解码令牌,并用 JWKS 检测检查提供商的密钥。

事实及其依据的检查

以下内容均非抄自文档。检查脚本 content/libraries/verify/microsoft-identitymodel/run.sh 于2026年10月20日在容器镜像 mcr.microsoft.com/dotnet/sdk:10.0.402-noble 中运行了 Microsoft.IdentityModel (JsonWebTokenHandler) 8.23.0;本页内容必须与它的输出一致,每条事实都列出了对应的输出行。

  • JWS 算法

    HS256HS384HS512RS256RS384RS512PS256PS384PS512ES256ES384ES512

    256/384/512 的 HS、RS、PS 和 ES 签名与验证均成功;该包中没有 EdDSA 密钥类型。

    检查记录
    fact jws-algorithms = HS256, HS384, HS512, RS256, RS384, RS512, PS256, PS384, PS512, ES256, ES384, ES512
    evidence jws-algorithms: EdDSA signing with an ECDsaSecurityKey or other built-in key: no EdDSA security key type
  • ES256 签名编码

    raw

    ES256 签名是 64 字节的原始 r‖s。

    检查记录
    fact jws-ecdsa-encoding = raw
  • JWS 中的 DER 签名

    rejected

    DER 编码的 ES256 签名会报错 IDX10517: Signature validation failed.

    检查记录
    fact jws-der-signature = rejected
    evidence jws-der-signature: SecurityTokenSignatureKeyNotFoundException: IDX10517: Signature validation failed. The token's kid is missing. Keys tried:
  • 算法允许列表

    from-key

    ValidAlgorithms 为 null 时由密钥决定:用 EC 密钥验证 RS256 令牌,报错 IDX10517。

    检查记录
    fact jwt-algorithm-allowlist = from-key
    evidence jwt-algorithm-allowlist: ValidAlgorithms null, RS256 token against the EC key: SecurityTokenSignatureKeyNotFoundException: IDX10517: Signature validation failed. The token's kid is missing. Keys tried:
  • 未签名的令牌(alg none)

    opt-in

    默认情况下,未签名的令牌会报错 IDX10504: Unable to validate signature, token does not have a signature;设置 RequireSignedTokens = false 后则验证通过。

    检查记录
    fact jwt-alg-none = opt-in
    evidence jwt-alg-none: default parameters: SecurityTokenInvalidSignatureException: IDX10504: Unable to validate signature, token does not have a signature:
    evidence jwt-alg-none: RequireSignedTokens = false: valid
  • 把公钥用作 HMAC 密钥

    rejected

    以 PEM 文本为密钥的 HS256 令牌,用 EC 密钥验证时报错 IDX10517。

    检查记录
    fact jwt-hmac-public-key = rejected
    evidence jwt-hmac-public-key: HS256 token keyed with the PEM text, EC key, ValidAlgorithms null: SecurityTokenSignatureKeyNotFoundException: IDX10517: Signature validation failed. The token's kid is missing. Keys tried:
  • 按 alg 检查曲线

    not-enforced

    CreateToken 用 P-384 密钥签出了 ES256 令牌;用该密钥验证、且把 ValidAlgorithms 设为 ES256 时,验证成功。

    检查记录
    fact jwt-ecdsa-curve-check = not-enforced
    evidence jwt-ecdsa-curve-check: ES256 with a P-384 key: signed; validation: valid
  • 默认时钟偏差

    300s

    TokenValidationParameters.DefaultClockSkew 为 5 分钟:过期不超过约 300 秒的令牌仍能验证通过。

    检查记录
    fact jwt-clock-skew = 300s
    evidence jwt-clock-skew: TokenValidationParameters.DefaultClockSkew = 00:05:00
  • 是否要求过期时间声明

    yes

    不含 exp 的令牌会报错 SecurityTokenNoExpirationException: IDX10225。

    检查记录
    fact jwt-exp-required = yes
    evidence jwt-exp-required: CreateToken without Expires adds exp anyway: True
    evidence jwt-exp-required: hand-built HS256 token without exp: SecurityTokenNoExpirationException: IDX10225: Lifetime validation failed. The token is missing an Expiration Time. Tokentype:
  • 默认隐藏错误详情

    yes

    验证错误会把令牌内容替换为 [PII of type ... is hidden];设置 IdentityModelEventSource.ShowPII = true 后,这个占位符消失了。

    检查记录
    fact error-detail-hidden-by-default = yes
    evidence error-detail-hidden-by-default: PII placeholder in the default message: True; after IdentityModelEventSource.ShowPII = true: False

用法

创建 ES256 令牌并验证:锁定算法、颁发者和受众,并把时钟偏差容差降到 30 秒。
using System.Security.Cryptography;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;

var key = new ECDsaSecurityKey(ECDsa.Create(ECCurve.NamedCurves.nistP256)) { KeyId = "2026-10" };
var handler = new JsonWebTokenHandler();
var token = handler.CreateToken(new SecurityTokenDescriptor
{
    Issuer = "https://auth.example.com",
    Audience = "api",
    Claims = new Dictionary<string, object> { ["sub"] = "user-42" },
    Expires = DateTime.UtcNow.AddMinutes(10),
    SigningCredentials = new SigningCredentials(key, SecurityAlgorithms.EcdsaSha256),
});

// Pin the algorithm (ES256 is not tied to P-256 here) and shrink the 5-minute default skew.
var result = await handler.ValidateTokenAsync(token, new TokenValidationParameters
{
    IssuerSigningKey = key,
    ValidIssuer = "https://auth.example.com",
    ValidAudience = "api",
    ValidAlgorithms = new[] { SecurityAlgorithms.EcdsaSha256 },
    ClockSkew = TimeSpan.FromSeconds(30),
});
Console.WriteLine($"valid: {result.IsValid}, sub: {result.Claims["sub"]}");

代码片段测试工具会在容器镜像 mcr.microsoft.com/dotnet/sdk:10.0.402-noble 中原样运行每个示例,只要输出不匹配,运行就会失败。

环境准备
dotnet new console --force -o . >/dev/null && dotnet add package Microsoft.IdentityModel.JsonWebTokens --version 8.23.0 >/dev/null
命令
rm -f Program.cs && DOTNET_NOLOGO=1 dotnet run 2>/dev/null
输出必须匹配(正则表达式)
valid: True, sub: user-42

库