Validating a JWT properly
The checks that are easy to skip, and what each one lets an attacker do.
Open this lesson in the learning hubKey points
- Verifying the signature is necessary and not sufficient. A token can be perfectly signed and still be the wrong token for you.
- Check the audience. Without it, a valid token issued for a different service in the same estate is accepted here - which is exactly what token exchange exists to prevent.
- Check the issuer, and pin it. Accepting any issuer means anyone who can stand up an OIDC provider can mint tokens your service trusts.
- Never let the token choose its own algorithm. The historic
alg: noneattack, and the RS256-to-HS256 confusion where the public key is used as an HMAC secret, both come from trusting the header. - Cache the JWKS, but honour the
kidand refresh on an unknown key. Fetching per request is a denial-of-service on your own identity provider; never refreshing breaks every rotation. - Allow a small clock skew - thirty seconds is typical. Without it, minor clock drift between services produces intermittent, unreproducible 401s that look like anything but a clock problem.
Example
spring:
security:
oauth2:
resourceserver:
jwt:
# Discovers the JWKS endpoint and pins the expected issuer.
issuer-uri: https://auth.example.com/realms/prod
# Pin the algorithm. Do NOT let the token header decide.
jws-algorithms: RS256
---
// Audience is not checked by default - you must add it.
@Bean
JwtDecoder jwtDecoder(OAuth2ResourceServerProperties props) {
NimbusJwtDecoder decoder = JwtDecoders.fromIssuerLocation(props.getJwt().getIssuerUri());
OAuth2TokenValidator<Jwt> withIssuer =
JwtValidators.createDefaultWithIssuer(props.getJwt().getIssuerUri());
OAuth2TokenValidator<Jwt> withAudience = jwt ->
jwt.getAudience().contains("orders-api")
? OAuth2TokenValidatorResult.success()
: OAuth2TokenValidatorResult.failure(new OAuth2Error(
"invalid_token", "Wrong audience", null));
// Tolerate small clock drift between services.
OAuth2TokenValidator<Jwt> withSkew = new JwtTimestampValidator(Duration.ofSeconds(30));
decoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(
withIssuer, withAudience, withSkew));
return decoder;
}
/*
* WHAT EACH MISSING CHECK COSTS:
*
* no signature check anyone forges any token critical
* no issuer check any provider mints tokens you trust critical
* no audience check a token for service B works on A high
* alg from the header alg:none, or RS256->HS256 confusion critical
* no expiry check a leaked token is valid forever high
* JWKS never refreshed key rotation breaks all auth outage
* JWKS fetched per req you DoS your own identity provider outage
*
* And the one that is not a vulnerability but wastes days:
* no clock skew intermittent 401s nobody can reproduce
*/
A valid signature only proves the token is genuine - audience, issuer and a pinned algorithm are what prove it is genuinely for you.
This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the Spring Security course, and every lesson in it is listed on the Spring Security contents page.