Skip to content
ecdsa.com

Sign an ES256 JWT · Java

How to sign a JWT with ES256 in Java

To sign a JWT with ES256 in Java, Nimbus JOSE + JWT (com.nimbusds:nimbus-jose-jwt:10.10, the library behind Spring Security's JWT support) signs a SignedJWT with an ECDSASigner built from a JCA private key. It converts the JDK's DER signature into the 64-byte JWS form; the run checks the token with OpenSSL.

Tested with com.nimbusds:nimbus-jose-jwt 10.10 (OpenJDK 25.0.4.1 (Temurin)).

SignJwt.java — ES256 with Nimbus JOSE + JWT
import com.nimbusds.jose.JOSEObjectType;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.crypto.ECDSASigner;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyFactory;
import java.security.interfaces.ECPrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Base64;
import java.util.Date;
public class SignJwt {
public static void main(String[] args) throws Exception {
String pem = Files.readString(Path.of("key.pem"));
byte[] pkcs8 = Base64.getMimeDecoder().decode(pem.replaceAll("-----[^-]+-----", ""));
ECPrivateKey privateKey = (ECPrivateKey) KeyFactory.getInstance("EC").generatePrivate(new PKCS8EncodedKeySpec(pkcs8));
Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS);
JWTClaimsSet claims = new JWTClaimsSet.Builder()
.issuer("https://issuer.example.com")
.subject("user-42")
.audience("api://orders")
.issueTime(Date.from(now))
.expirationTime(Date.from(now.plusSeconds(15 * 60)))
.build();
JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.ES256)
.type(JOSEObjectType.JWT)
.keyID("2026-10")
.build();
SignedJWT jwt = new SignedJWT(header, claims);
jwt.sign(new ECDSASigner(privateKey)); // produces the 64-byte JWS form, not the JCA's DER
System.out.println(jwt.serialize());
}
}

How it works

  1. The PKCS#8 PEM body is decoded and loaded with KeyFactory.getInstance("EC"), giving an ECPrivateKey for ECDSASigner.
  2. JWTClaimsSet.Builder sets the claims; truncatedTo(ChronoUnit.SECONDS) keeps iat and exp exactly 900 seconds apart.
  3. JWSHeader.Builder(JWSAlgorithm.ES256) with type(JOSEObjectType.JWT) and keyID writes typ and kid; without them the header holds alg only.
  4. jwt.sign(new ECDSASigner(privateKey)), then serialize(), produces the compact token.

Gotchas

  • The JCA's SHA256withECDSA returns DER (71 bytes in a test run), which is not a JWS signature. Nimbus transcodes it; hand-written code must use SHA256withECDSAinP1363Format, which returns the 64-byte form directly.
  • A P-384 key cannot sign ES256: JOSEException: The ES256 algorithm is not allowed or supported by the JWS signer: Supported algorithms: [ES384].
  • KeyFactory cannot read BEGIN EC PRIVATE KEY (SEC 1) files; convert them with openssl pkcs8 -topk8 -nocrypt.
  • Nimbus writes a single audience as a string, while some other libraries write an array. Both are valid JWT, but a verifier that compares aud as a raw string or a raw list needs the form it expects.

Related recipes