Skip to content
ecdsa.com

Verify an ES256 JWT · Java

How to verify an ES256 JWT in Java

To verify an ES256 JWT in Java, use Nimbus JOSE+JWT — the library behind Spring Security's resource server — and configure its processor to accept ES256 only. Under the hood the check is plain JCA: a JWS signature is already raw r‖s, so the JDK's SHA256withECDSAinP1363Format verifies it with no DER conversion, as the second snippet shows.

Tested with nimbus-jose-jwt 10.10 (OpenJDK 25.0.4.1 (Temurin), Maven 3.9.16) and OpenJDK 25.0.4.1 (SunEC provider, JDK-only variant).

pom.xml — the one dependency (Gradle: com.nimbusds:nimbus-jose-jwt:10.10)
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>10.10</version>
</dependency>
VerifyJwt.java — ES256 pinned, signature and claims checked by Nimbus
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.jwk.Curve;
import com.nimbusds.jose.jwk.ECKey;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.proc.JWSVerificationKeySelector;
import com.nimbusds.jose.proc.SecurityContext;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.proc.DefaultJWTClaimsVerifier;
import com.nimbusds.jwt.proc.DefaultJWTProcessor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyFactory;
import java.security.interfaces.ECPublicKey;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.Set;
public class VerifyJwt {
static JWTClaimsSet verify(String token, String publicKeyPem) throws Exception {
byte[] spki = Base64.getMimeDecoder().decode(publicKeyPem.replaceAll("-----[^-]+-----", ""));
ECPublicKey publicKey = (ECPublicKey) KeyFactory.getInstance("EC").generatePublic(new X509EncodedKeySpec(spki));
ECKey jwk = new ECKey.Builder(Curve.P_256, publicKey).build();
DefaultJWTProcessor<SecurityContext> processor = new DefaultJWTProcessor<>();
// ES256 only: a header naming any other algorithm is rejected before the signature check
processor.setJWSKeySelector(new JWSVerificationKeySelector<>(JWSAlgorithm.ES256, new ImmutableJWKSet<>(new JWKSet(jwk))));
// exp and nbf are always checked; iss must match, sub and exp must be present
processor.setJWTClaimsSetVerifier(new DefaultJWTClaimsVerifier<>(
new JWTClaimsSet.Builder().issuer("https://ecdsa.com").build(), Set.of("sub", "exp")));
return processor.process(token, null);
}
public static void main(String[] args) throws Exception {
JWTClaimsSet claims = verify(Files.readString(Path.of("token.txt")).strip(), Files.readString(Path.of("pub.pem")));
System.out.println("valid ES256 token for " + claims.getSubject() + ", expires " + claims.getExpirationTime().toInstant());
}
}
the signature check alone, with nothing but the JDK
// The signature check alone, JDK only: a JWS signature already is r‖s (IEEE P1363)
static boolean es256SignatureValid(String token, PublicKey publicKey) throws Exception {
String[] parts = token.split("\\.");
if (parts.length != 3) throw new IllegalArgumentException("not a compact JWS");
Signature verifier = Signature.getInstance("SHA256withECDSAinP1363Format");
verifier.initVerify(publicKey);
verifier.update((parts[0] + "." + parts[1]).getBytes(StandardCharsets.US_ASCII)); // the base64url text
return verifier.verify(Base64.getUrlDecoder().decode(parts[2]));
}

How it works

  1. The public key is loaded with plain JCA (X509EncodedKeySpec reads the SPKI PEM body) and wrapped as an ECKey on Curve.P_256 — the JWK form Nimbus selects keys from.
  2. JWSVerificationKeySelector with JWSAlgorithm.ES256 is the algorithm pin: a token whose header names any other algorithm finds no key and is rejected before the signature is computed.
  3. DefaultJWTClaimsVerifier checks exp and nbf whenever they are present (with 60 seconds of allowed clock skew), compares the exact-match claims (iss here) and requires the listed claims (sub, exp) to be present.
  4. process returns the verified JWTClaimsSet or throws: BadJWSException for a bad signature, BadJOSEException when the header asks for another algorithm, and a BadJWTException subclass such as ExpiredJWTException when a claim check fails — the run above triggers all three.
  5. The JDK-only variant shows the mechanism: the signed bytes are the ASCII text header.payload, and the third segment decodes with Base64.getUrlDecoder() to the 64-byte r‖s that the P1363 algorithm reads directly.

Gotchas

  • Verifying a JWS signature with SHA256withECDSA throws SignatureException: Invalid encoding for signature: that algorithm expects DER, and no valid JWT carries a DER signature.
  • Base64.getDecoder() rejects JWT segments with Illegal base64 character 5f (the _ of base64url). Use Base64.getUrlDecoder(), which also accepts the missing = padding.
  • The JDK-only check proves the signature and nothing else — an expired token or one for another audience still returns true. Claim checks need a JSON parser, which is why production code goes through the library.
  • SignedJWT.parse(token).verify(verifier) is also signature-only. Prefer DefaultJWTProcessor, which pins the algorithm and validates the claims in one place.
  • For keys published by an identity provider, build the key source with JWKSourceBuilder.create(jwksUrl).build() instead of a fixed key, so rotated keys are fetched by kid.

Related recipes