import com.nimbusds.jose.JWSAlgorithm;import com.nimbusds.jose.jwk.source.JWKSource;import com.nimbusds.jose.jwk.source.JWKSourceBuilder;import com.nimbusds.jose.proc.JWSVerificationKeySelector;import com.nimbusds.jose.proc.SecurityContext;import com.nimbusds.jwt.JWTClaimsSet;import com.nimbusds.jwt.proc.ConfigurableJWTProcessor;import com.nimbusds.jwt.proc.DefaultJWTClaimsVerifier;import com.nimbusds.jwt.proc.DefaultJWTProcessor;import java.net.URI;import java.util.Set; public class JwtVerifier { private final ConfigurableJWTProcessor<SecurityContext> processor = new DefaultJWTProcessor<>(); public JwtVerifier() throws Exception { // Cached, refreshed on a schedule and when a token names an unknown kid JWKSource<SecurityContext> keys = JWKSourceBuilder .create(URI.create("https://issuer.example.com/.well-known/jwks.json").toURL()) .retrying(true) .build(); // Only ES256: the token's alg header must match, and the key is chosen by kid processor.setJWSKeySelector(new JWSVerificationKeySelector<>(JWSAlgorithm.ES256, keys)); processor.setJWTClaimsSetVerifier(new DefaultJWTClaimsVerifier<>( "api://orders", // required audience new JWTClaimsSet.Builder().issuer("https://issuer.example.com").build(), // exact-match claims Set.of("sub", "exp"))); // required claims } public JWTClaimsSet verify(String token) throws Exception { return processor.process(token, null); // throws BadJOSEException with the reason }} import java.nio.file.Files;import java.nio.file.Path; public class Main { public static void main(String[] args) throws Exception { JwtVerifier verifier = new JwtVerifier(); for (String line : Files.readAllLines(Path.of("tokens.txt"))) { String[] parts = line.split(" "); try { System.out.println(parts[0] + ": accepted, sub=" + verifier.verify(parts[1]).getSubject()); } catch (Exception e) { System.out.println(parts[0] + ": rejected: " + e.getClass().getSimpleName() + ": " + e.getMessage()); } } }} How it works
JWKSourceBuilder.create(url)builds a source that caches the JWKS for 5 minutes, refreshes it ahead of expiry, and refetches when a token names an unknownkid— at most once per 30 seconds.retrying(true)retries a failed fetch once.JWSVerificationKeySelector(JWSAlgorithm.ES256, keys)accepts only ES256 tokens and returns the key whosekidand type fit.DefaultJWTClaimsVerifier(audience, exactMatchClaims, requiredClaims)checksaud,issand thatsubandexpare present;expandnbfare always checked when present.processreturns theJWTClaimsSetor throws aBadJOSEExceptionsubclass that says what failed.
Gotchas
- The default claims verifier checks only time claims: with just a key selector, the token for
api://billingis accepted by the orders API. Always set aDefaultJWTClaimsVerifierwith the audience and issuer. - An unknown
kidand a token for another algorithm produce the same message,Signed JWT rejected: Another algorithm expected, or no matching key(s) found— compare the token header with the JWKS to tell which. - The JVM uses its own trust store (
cacerts), not the operating system's. An issuer with a private CA needs that CA imported there, or a customResourceRetrieverwith its ownSSLContext. - Spring Security's resource server wraps this processor (
NimbusJwtDecoder). Withissuer-uriit finds the JWKS through the issuer's discovery document and validatesiss— the audience still has to be configured, for the same reason as here.