Skip to content
ecdsa.com
.NET

Microsoft.IdentityModel (JsonWebTokenHandler)

Microsoft.IdentityModel's JsonWebTokenHandler, the JWT validator under ASP.NET Core authentication and Microsoft Entra ID SDKs: five minutes of default clock skew, exp required, and ES256 tokens accepted from P-384 keys.
License
MIT
Checked version
8.23.0 ·
Source
github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet
Documentation
learn.microsoft.com/dotnet/api/microsoft.identitymodel.jsonwebtokens.jsonwebtokenhandler

Compatibility at a glance

JOSE and JWT

JWS algorithms
HS256HS384HS512RS256RS384RS512PS256PS384PS512ES256ES384ES512

Microsoft.IdentityModel.JsonWebTokens creates and validates JWTs with JsonWebTokenHandler and a TokenValidationParameters object; AddJwtBearer in ASP.NET Core feeds requests through it, which makes its defaults the defaults of most .NET APIs. 8.23.0 signed and validated the HS, RS, PS and ES families; there is no EdDSA security key type in the package.

ValidAlgorithms is null by default, and the key decides: an RS256 token and an HS256 token forged with the EC key's PEM text both failed against an EC key with IDX10517: Signature validation failed. Unsigned tokens fail with IDX10504 unless RequireSignedTokens is set to false. The gap is curve binding: ES256 signed with a P-384 key and the same handler validated the resulting token, which other libraries — jose, PyJWT, golang-jwt — refuse. Set ValidAlgorithms and use keys of the right size, as the sample does.

Lifetime rules are the opposite of most libraries: exp is required (IDX10225) and TokenValidationParameters.DefaultClockSkew is five minutes, so a token keeps working for up to five minutes after it expires. That is generous for short-lived access tokens; set ClockSkew explicitly. CreateToken adds exp even when Expires is not set.

ES256 signatures are the 64-byte raw r‖s that JWS requires, matching .NET's own ECDsa default; a DER signature fails with the same IDX10517 as a forgery. Error messages hide token contents as [PII of type ... is hidden] unless IdentityModelEventSource.ShowPII is enabled during debugging — decode the token in the JWT debugger instead, and check a provider's keys with the JWKS checker.

Facts and the checks behind them

Nothing below is copied from documentation. The check content/libraries/verify/microsoft-identitymodel/run.sh ran Microsoft.IdentityModel (JsonWebTokenHandler) 8.23.0 in the container image mcr.microsoft.com/dotnet/sdk:10.0.402-noble on October 20, 2026; the page must match what it printed, and each fact lists those lines.

  • JWS algorithms

    HS256HS384HS512RS256RS384RS512PS256PS384PS512ES256ES384ES512

    HS, RS, PS and ES at 256/384/512 signed and validated; no EdDSA key type exists in the package.

    Recorded by the check
    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 signature encoding

    raw

    ES256 signatures are 64 bytes of raw r‖s.

    Recorded by the check
    fact jws-ecdsa-encoding = raw
  • DER signature inside a JWS

    rejected

    A DER-encoded ES256 signature fails with IDX10517: Signature validation failed.

    Recorded by the check
    fact jws-der-signature = rejected
    evidence jws-der-signature: SecurityTokenSignatureKeyNotFoundException: IDX10517: Signature validation failed. The token's kid is missing. Keys tried:
  • Algorithm allow-list

    from-key

    With ValidAlgorithms null, the key decides: an RS256 token against an EC key failed with IDX10517.

    Recorded by the check
    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:
  • Unsigned tokens (alg none)

    opt-in

    Unsigned tokens fail with IDX10504: Unable to validate signature, token does not have a signature by default and validate with RequireSignedTokens = false.

    Recorded by the check
    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
  • Public key used as an HMAC secret

    rejected

    An HS256 token keyed with the PEM text failed against the EC key with IDX10517.

    Recorded by the check
    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:
  • Curve checked against alg

    not-enforced

    CreateToken signed ES256 with a P-384 key, and validation with that key and ValidAlgorithms set to ES256 succeeded.

    Recorded by the check
    fact jwt-ecdsa-curve-check = not-enforced
    evidence jwt-ecdsa-curve-check: ES256 with a P-384 key: signed; validation: valid
  • Default clock skew

    300s

    TokenValidationParameters.DefaultClockSkew is 5 minutes: tokens expired up to about 300 seconds earlier still validated.

    Recorded by the check
    fact jwt-clock-skew = 300s
    evidence jwt-clock-skew: TokenValidationParameters.DefaultClockSkew = 00:05:00
  • Expiry claim required

    yes

    A token without exp fails with SecurityTokenNoExpirationException: IDX10225.

    Recorded by the check
    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:
  • Error details hidden by default

    yes

    Validation errors replace token contents with [PII of type ... is hidden]; setting IdentityModelEventSource.ShowPII = true removed the placeholder.

    Recorded by the check
    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

Usage

Create an ES256 token and validate it with the algorithm, issuer and audience pinned and the clock skew reduced to 30 seconds.
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"]}");

The snippet harness runs every sample verbatim in the container image mcr.microsoft.com/dotnet/sdk:10.0.402-noble, and the run fails unless the output matches.

Setup
dotnet new console --force -o . >/dev/null && dotnet add package Microsoft.IdentityModel.JsonWebTokens --version 8.23.0 >/dev/null
Command
rm -f Program.cs && DOTNET_NOLOGO=1 dotnet run 2>/dev/null
Output must match (regular expression)
valid: True, sub: user-42

Libraries