Verify JWT in Java (Spring Security & jjwt)
Verify JWT signatures in Java with Spring Security OAuth2 Resource Server and jjwt. Sign and verify RS256 tokens, validate iss/aud/exp, verify JWTs against a JWKS endpoint, and fix the Spring Security 'Audiences in jwt are not allowed' error.
Java verifies JWTs in two contexts: Spring Security’s OAuth2 Resource Server (the default for Spring Boot apps validating tokens from an external IdP) and jjwt (the library you use when you need to sign tokens or verify outside Spring). This page covers both, plus the Spring Security aud error that produces the most-searched JWT Java troubleshooting query.
Dependencies
<!-- Spring Boot resource server (verifies JWTs from an external IdP) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<!-- jjwt (sign + verify, used on auth servers and outside Spring) -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.6</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
Verify a JWT in Spring Boot (RS256 + JWKS)
application.yml:
spring:
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: https://YOUR_DOMAIN.auth0.com/.well-known/jwks.json
issuer-uri: https://YOUR_DOMAIN.auth0.com/
SecurityConfig.java:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
return http.build();
}
@Bean
public JwtDecoder jwtDecoder() {
NimbusJwtDecoder decoder = NimbusJwtDecoder.withJwkSetUri(
"https://YOUR_DOMAIN.auth0.com/.well-known/jwks.json"
).build();
decoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(
"https://YOUR_DOMAIN.auth0.com/"
));
return decoder;
}
}
Spring’s NimbusJwtDecoder fetches the JWKS, caches the keys (default TTL 60 minutes), verifies the RS256 signature, and validates exp, nbf, and iss automatically. You read claims from the Jwt in your controller:
@GetMapping("/me")
public Map<String, Object> me(@AuthenticationPrincipal Jwt jwt) {
return Map.of("sub", jwt.getSubject(), "iss", jwt.getIssuer().toString());
}
Sign and verify an RS256 token (jjwt)
import io.jsonwebtoken.Jwts;
import java.security.PrivateKey;
import java.security.PublicKey;
// Sign
String token = Jwts.builder()
.subject("user_123")
.issuer("https://auth.example.com")
.audience().add("https://api.example.com").and()
.expiration(new Date(System.currentTimeMillis() + 15 * 60 * 1000))
.signWith(privateKey, Jwts.SIG.RS256) // hardcoded algorithm
.header().keyId("rsa-key-v1").and()
.compact();
// Verify — hardcoded key, explicit iss + aud
var claims = Jwts.parser()
.verifyWith(publicKey)
.requireIssuer("https://auth.example.com")
.requireAudience("https://api.example.com")
.build()
.parseSignedClaims(token)
.getPayload();
jjwt throws SignatureException (bad signature), ExpiredJwtException (expired), or IncorrectClaimException (wrong iss/aud). Catch these at your API boundary and return 401.
Audiences in jwt are not allowed
This exact error message is the single highest-growth JWT Java search query. It comes from Spring Security’s Jwt claim validation, and it means the aud claim in your token does not contain the audience Spring’s decoder is configured to expect.
Why it happens
Spring’s NimbusJwtDecoder does not enforce aud by default. The error appears when an OAuth2TokenValidator that checks aud has been registered — either explicitly by you, or implicitly by JwtDecoders.fromIssuerLocation(issuerUrl) (which reads the issuer’s OIDC metadata and may register an audience validator), or by adding spring.security.oauth2.resourceserver.jwt.audiences in newer Spring Boot versions. When the validator runs and your token’s aud does not include the configured value, Spring rejects the token with this message (or the closely related The aud claim is not valid).
The fix — align the audience
Step 1. Find what aud your token actually carries. Paste it into the JWT decoder and read the aud claim from the payload. It is usually the API identifier you configured at your IdP (e.g. https://api.example.com, or an Auth0 API identifier like https://YOUR_DOMAIN.auth0.com/api/v2/).
Step 2. Configure Spring to expect that exact audience. In application.yml (Spring Boot 3.1+):
spring:
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: https://YOUR_DOMAIN.auth0.com/.well-known/jwks.json
audiences: # the aud Spring will accept
- https://api.example.com
Step 3. If you cannot use the audiences property (older Spring Boot, or you need a custom validator), register the validator manually:
@Bean
public JwtDecoder jwtDecoder() {
NimbusJwtDecoder decoder = NimbusJwtDecoder.withJwkSetUri(
"https://YOUR_DOMAIN.auth0.com/.well-known/jwks.json"
).build();
OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(
JwtValidators.createDefaultWithIssuer("https://YOUR_DOMAIN.auth0.com/"),
new JwtClaimValidator<>("aud", aud -> {
// aud is a String or List<String>; accept if it contains the expected value
Collection<String> audiences = aud instanceof Collection
? (Collection<String>) aud
: List.of((String) aud);
return audiences.contains("https://api.example.com");
})
);
decoder.setJwtValidator(withAudience);
return decoder;
}
Common causes checklist
- Token aud is the Auth0 API identifier, Spring expects a different string. The IdP’s API identifier and Spring’s expected audience must match exactly (case-sensitive). This is the most common cause.
- Token has no
audat all. If the IdP issues tokens withoutaud(some server-to-server flows do), Spring’s audience validator rejects them. Either configure the IdP to includeaud, or remove the audience validator and rely onissalone (weaker — only do this if you control the issuer and the resource server is the only consumer). audis an array, Spring is configured with a single string. TheJwtClaimValidatorabove handles both. TheaudiencesYAML property also handles array-valuedaud.- Multiple resource servers share one IdP but expect different audiences. Each resource server must configure its own expected
aud; do not disable the check globally.
Verify a JWT against a JWKS endpoint (jjwt)
For jjwt outside Spring, use a JwkLocator (jjwt 0.12+) or fetch the JWKS with Nimbus directly:
// jjwt 0.12+ — JwkLocator fetches and caches the JWKS
var jwkSet = Jwts.parser()
.keyLocator(Jwts.keyLocator()
.jwkSetUri(new URI("https://YOUR_DOMAIN.auth0.com/.well-known/jwks.json"))
.build())
.requireIssuer("https://YOUR_DOMAIN.auth0.com/")
.requireAudience("https://api.example.com")
.build()
.parseSignedClaims(token)
.getPayload();
In Spring Boot, prefer NimbusJwtDecoder (above) over jjwt for verification — Spring handles the JWKS caching, retry, and key rotation for you.
Continue reading
- JWT decoder → - paste a token to decode and verify it in your browser
- JWT Signing Algorithms → - HS256, RS256, ES256, EdDSA with key sizes and code
- Registered Claims Reference → - iss, sub, aud, exp, jti, and every RFC 7519 claim
- JWT Security Vulnerabilities → - alg:none, algorithm confusion, kid injection, and every fix
- Verify JWT in Node.js → - jsonwebtoken and jose code
- Verify JWT in Python → - PyJWT, SimpleJWT, and FastAPI code