#:package Microsoft.IdentityModel.JsonWebTokens@8.23.0 using System.Security.Cryptography;using Microsoft.IdentityModel.JsonWebTokens;using Microsoft.IdentityModel.Tokens; var publicKey = ECDsa.Create();publicKey.ImportFromPem(File.ReadAllText("pub.pem")); var parameters = new TokenValidationParameters{ IssuerSigningKey = new ECDsaSecurityKey(publicKey), ValidAlgorithms = [SecurityAlgorithms.EcdsaSha256], // "ES256" and nothing else ValidIssuer = "https://ecdsa.com", ValidateAudience = false, // this token has no aud; set ValidAudience when yours do ClockSkew = TimeSpan.FromSeconds(30), // the default allows five minutes}; TokenValidationResult result = await new JsonWebTokenHandler() .ValidateTokenAsync(File.ReadAllText("token.txt").Trim(), parameters);if (!result.IsValid) throw result.Exception; // never read claims from an invalid result var jwt = (JsonWebToken)result.SecurityToken;Console.WriteLine($"valid ES256 token for {jwt.Subject}, expires {jwt.ValidTo:O}"); using System.Buffers.Text;using System.Security.Cryptography;using System.Text; static class Jws{ /// <summary>The ES256 signature check alone: JWS signatures are raw r‖s, .NET's default format.</summary> public static bool Es256SignatureValid(string token, ECDsa publicKey) { string[] parts = token.Split('.'); if (parts.Length != 3) throw new FormatException("not a compact JWS"); byte[] signingInput = Encoding.ASCII.GetBytes($"{parts[0]}.{parts[1]}"); // the base64url text byte[] signature = Base64Url.DecodeFromChars(parts[2]); // .NET 9+: base64url without padding return publicKey.VerifyData(signingInput, signature, HashAlgorithmName.SHA256); }} How it works
#:packagepins the NuGet package for a file-based app (dotnet run verify.cs, .NET 10). In a project, add the same package withdotnet add package Microsoft.IdentityModel.JsonWebTokens --version 8.23.0.ECDsaSecurityKeywraps anECDsaloaded from the SPKI PEM;ValidAlgorithms = [SecurityAlgorithms.EcdsaSha256]refuses every headeralgexceptES256.ValidateTokenAsyncchecks signature, issuer and lifetime and returns a result object instead of throwing. Read claims only afterIsValidis true.- The BCL-only variant shows the mechanism: the signed bytes are the ASCII text
header.payload,Base64Url.DecodeFromChars(.NET 9+) decodes the unpadded segment, andVerifyDatawith its default format reads the 64-byter‖sdirectly.
Gotchas
- Audience validation is on by default. A token without
aud— or a configuration withoutValidAudience— fails withIDX10208/SecurityTokenInvalidAudienceException(the run shows it). Turn it off only when your tokens genuinely carry no audience. - A forged token and a token with a disallowed
algboth fail withIDX10517: Signature validation failed. The token's kid is missingwhen the token has nokid. It does not mean you must add akid; it means no key validated the signature. The detailed reason is hidden as PII untilIdentityModelEventSource.ShowPII = true(development only). ClockSkewdefaults to five minutes, so a token "expires" up to five minutes late. Set it explicitly.JsonWebTokenHandler.ReadJsonWebTokenand the olderJwtSecurityTokenHandler.ReadJwtTokenonly decode — they check neither signature nor expiry. Do not authorize anything with their output.Convert.FromBase64Stringrejects JWT segments (-,_and missing padding). UseBase64UrlfromSystem.Buffers.Text, orBase64UrlEncoderfrom the IdentityModel package.