A JWT resource server authenticates a bearer token by verifying its signature and configured claims before applying endpoint authorization.
Spring Security JWT resource server: validate trust before checking scope
The downloadable Spring source kit pins Java 21 and Spring Boot 4.0.8 with its managed dependencies. Run mvn test to check the named fixture.
Define what this API trusts
ReceiptTokenDecoder trusts one RSA public key, one issuer and an audience containing receipt-api. The test creates temporary RSA keys in memory, signs local tokens, and exercises the actual security filter chain through MockMvc. A trusted token with receipt.read reaches the endpoint. Wrong signatures, issuers and audiences are rejected with 401, as is a token expired well beyond the validator clock-skew allowance.
The fixed public key is a fixture trust input. It is not a private signing key embedded in the application, a JWKS rotation implementation or a remote issuer lookup. The test signs its own tokens solely to make accepted and rejected inputs reproducible. Never accept an unverified issuer claim as a reason to fetch keys from an arbitrary caller-selected URL.
Authentication does not grant every operation
The security chain requires SCOPE_receipt.read for the GET route. A valid token with receipt.write is authenticated but receives 403 because it lacks the required authority. A request with no token receives 401. The test keeps these outcomes separate.
The route returns a fixed teaching receipt. A general read scope does not establish ownership of a particular tenant or receipt. Service authorization can enforce another boundary, but resource-level permission still needs trusted identity and domain state. This page implements no login screen, authorization server, OIDC session or token refresh flow.
Checked source
package in.aitrove.contracts;
import java.security.interfaces.RSAPublicKey;
import java.util.List;
import org.springframework.security.oauth2.core.*;
import org.springframework.security.oauth2.jwt.*;
public final class ReceiptTokenDecoder {
public static final String ISSUER="https://issuer.fixture.invalid";
public static JwtDecoder create(RSAPublicKey publicKey) {
NimbusJwtDecoder decoder=NimbusJwtDecoder.withPublicKey(publicKey).build();
var audience=new JwtClaimValidator<List<String>>("aud",values->values!=null&&values.contains("receipt-api"));
decoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(JwtValidators.createDefaultWithIssuer(ISSUER),audience));
return decoder;
}
}Test the boundary
JwtResourceServerContractTest checks this contract in the source kit. Excerpts belong to the named classes; use the downloadable files for imports, configuration and assertions.
Costs and boundaries
The tests check seven local request outcomes with the pinned Spring Security libraries and in-memory keys. Signature verification has CPU cost per checked token; key retrieval, rotation, revocation, identity-store access and distributed clock behavior are outside this fixture. Stateless session configuration does not remove application authorization obligations.
Common Mistakes
- Do not decode claims and call that signature verification.
- Validate the API audience as well as the issuer.
- Distinguish an invalid token from a valid token lacking an authority.
Read next
Spring Security filter chain: authentication, CSRF and request order, Spring method authorization: test the proxied service boundary, Spring PasswordEncoder: salted verification rather than reversible storage, Java SecureRandom: token entropy, encoding and comparison boundaries.
Continue with the new boundary checks
Continue with Spring method authorization: reject a cross-tenant read.
Continue with checked tenant security
Continue with Spring Security JWT tenant principal: map only a validated claim, Spring JWT tenant claims: reject missing or malformed ownership before conversion.
