Skip to content
ecdsa.com

Sign an ES256 JWT · .NET

How to sign a JWT with ES256 in C#

To sign a JWT with ES256 in C#, JsonWebTokenHandler from Microsoft.IdentityModel.JsonWebTokens creates the token from a SecurityTokenDescriptor whose SigningCredentials wrap an ECDsa key loaded from PEM. The handler produces the 64-byte JWS signature; the run checks the token with OpenSSL against the public key.

Tested with Microsoft.IdentityModel.JsonWebTokens 8.23.0 (.NET SDK 10.0.402).

app.cs — ES256 with JsonWebTokenHandler (a .NET 10 file-based app)
#:package Microsoft.IdentityModel.JsonWebTokens@8.23.0
using System.Security.Cryptography;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
using var ecdsa = ECDsa.Create();
ecdsa.ImportFromPem(File.ReadAllText("key.pem")); // PKCS#8 or "EC PRIVATE KEY"
var signingKey = new ECDsaSecurityKey(ecdsa) { KeyId = "2026-10" }; // becomes the kid header
var now = DateTime.UtcNow;
string token = new JsonWebTokenHandler().CreateToken(new SecurityTokenDescriptor
{
Issuer = "https://issuer.example.com",
Audience = "api://orders",
Claims = new Dictionary<string, object> { ["sub"] = "user-42" },
IssuedAt = now,
NotBefore = now,
Expires = now.AddMinutes(15),
SigningCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.EcdsaSha256), // "ES256"
});
Console.WriteLine(token);

How it works

  1. ECDsa.ImportFromPem reads PKCS#8 or EC PRIVATE KEY PEM; ECDsaSecurityKey wraps it, and its KeyId becomes the kid header.
  2. SecurityAlgorithms.EcdsaSha256 is the string "ES256".
  3. Issuer, Audience, IssuedAt, NotBefore and Expires map to iss, aud, iat, nbf and exp; other claims go in Claims.
  4. JsonWebTokenHandler is the current API; JwtSecurityTokenHandler in System.IdentityModel.Tokens.Jwt is its predecessor, still found in older ASP.NET examples.

Gotchas

  • The handler does not check that the key fits the algorithm: a P-384 key with EcdsaSha256 produces a token with a 96-byte signature that every ES256 verifier rejects. Use P-256 keys for ES256.
  • Leaving out Expires does not create a token without expiry: by default the handler adds iat, nbf and an exp 60 minutes later. Set the lifetime explicitly so it is visible in code.
  • DateTime.Now is local time; use DateTime.UtcNow (or DateTimeOffset.UtcNow) for token times.
  • The credentials keep a reference to the ECDsa, not a copy: once it is disposed, the next CreateToken throws ObjectDisposedException: Cannot access a disposed object. Keep the key alive as long as the credentials, for example as a singleton.

Related recipes