<dependency> <groupId>com.nimbusds</groupId> <artifactId>nimbus-jose-jwt</artifactId> <version>10.10</version></dependency> 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, 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
- The public key is loaded with plain JCA (
X509EncodedKeySpecreads the SPKI PEM body) and wrapped as anECKeyonCurve.P_256— the JWK form Nimbus selects keys from. JWSVerificationKeySelectorwithJWSAlgorithm.ES256is the algorithm pin: a token whose header names any other algorithm finds no key and is rejected before the signature is computed.DefaultJWTClaimsVerifierchecksexpandnbfwhenever they are present (with 60 seconds of allowed clock skew), compares the exact-match claims (isshere) and requires the listed claims (sub,exp) to be present.processreturns the verifiedJWTClaimsSetor throws:BadJWSExceptionfor a bad signature,BadJOSEExceptionwhen the header asks for another algorithm, and aBadJWTExceptionsubclass such asExpiredJWTExceptionwhen a claim check fails — the run above triggers all three.- The JDK-only variant shows the mechanism: the signed bytes are the ASCII text
header.payload, and the third segment decodes withBase64.getUrlDecoder()to the 64-byter‖sthat the P1363 algorithm reads directly.
Gotchas
- Verifying a JWS signature with
SHA256withECDSAthrowsSignatureException: Invalid encoding for signature: that algorithm expects DER, and no valid JWT carries a DER signature. Base64.getDecoder()rejects JWT segments withIllegal base64 character 5f(the_of base64url). UseBase64.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. PreferDefaultJWTProcessor, 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 bykid.