1. Overview

In this quick tutorial, we'll work with a Spring Security OAuth2 implementation and we'll learn how to verify JWT claims using the new JwtClaimsSetVerifier – introduced in Spring Security OAuth 2.2.0.RELEASE.

2. Maven Configuration

First, we need to add the latest version of spring-security-oauth2 into our pom.xml:

<dependency>
    <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
    <version>2.2.0.RELEASE</version>
</dependency>

3. Token Store Configuration

Next, let's configure our TokenStore in the Resource Server:

@Bean
public TokenStore tokenStore() {
    return new JwtTokenStore(accessTokenConverter());
}

@Bean
public JwtAccessTokenConverter accessTokenConverter() {
    JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
    converter.setSigningKey("123");
    converter.setJwtClaimsSetVerifier(jwtClaimsSetVerifier());
    return converter;
}

Note how we're adding the new verifier to our JwtAccessTokenConverter.

For more details on how to configure JwtTokenStore, check out the writeup about using JWT with Spring Security OAuth.

Now, in the following sections, we'll discuss different types of claim verifier and how to make them work together.

4. IssuerClaimVerifier

We'll start simple – by verifying the Issuer “iss” claim using IssuerClaimVerifier – as follows:

@Bean
public JwtClaimsSetVerifier issuerClaimVerifier() {
    try {
        return new IssuerClaimVerifier(new URL("http://localhost:8081"));
    } catch (MalformedURLException e) {
        throw new RuntimeException(e);
    }
}

In this example, we added a simple IssuerClaimVerifier to verify our issuer. If the JWT token contains a different value for issuer “iss” claim, a simple InvalidTokenException will be thrown.

Naturally, if the token does contain the issuer “iss” claim, no exception will be thrown and the token is considered valid.

5. Custom Claim Verifier

But, what's interesting here is that we can also build our custom claim verifier:

@Bean
public JwtClaimsSetVerifier customJwtClaimVerifier() {
    return new CustomClaimVerifier();
}

Here's a simple implementation of what this can look like – to check if the user_name claim exists in our JWT token:

public class CustomClaimVerifier implements JwtClaimsSetVerifier {
    @Override
    public void verify(Map<String, Object> claims) throws InvalidTokenException {
        String username = (String) claims.get("user_name");
        if ((username == null) || (username.length() == 0)) {
            throw new InvalidTokenException("user_name claim is empty");
        }
    }
}

Notice how we're simply implementing the JwtClaimsSetVerifier interface here, and then provide a completely custom implementation for the verify method – which gives us full flexibility for any kind of check we need.

6. Combine Multiple Claim Verifiers

Finally, let's see how to combine multiple claim verifier using DelegatingJwtClaimsSetVerifier – as follows:

@Bean
public JwtClaimsSetVerifier jwtClaimsSetVerifier() {
    return new DelegatingJwtClaimsSetVerifier(Arrays.asList(
      issuerClaimVerifier(), customJwtClaimVerifier()));
}

DelegatingJwtClaimsSetVerifier takes a list of JwtClaimsSetVerifier objects and delegates the claim verification process to these verifiers.

7. Simple Integration Test

Now that we're done with the implementation, let's test our claims verifiers with a simple integration test:

@RunWith(SpringRunner.class)
@SpringBootTest(
  classes = ResourceServerApplication.class, 
  webEnvironment = WebEnvironment.RANDOM_PORT)
public class JwtClaimsVerifierIntegrationTest {

    @Autowired
    private JwtTokenStore tokenStore;

    ...
}

We'll start with a token that doesn't contain an issuer (but contains a user_name) – which should be valid:

@Test
public void whenTokenDontContainIssuer_thenSuccess() {
    String tokenValue = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9....";
    OAuth2Authentication auth = tokenStore.readAuthentication(tokenValue);
    
    assertTrue(auth.isAuthenticated());
}

The reason this is valid is simple – the first verifier is only active if an issuer claim exists in the token. If that claim doesn't exist – the verifier doesn't kick in.

Next, let's have a look at a token which contains a valid issuer (http://localhost:8081) and a user_name as well. This should also be valid:

@Test
public void whenTokenContainValidIssuer_thenSuccess() {
    String tokenValue = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9....";
    OAuth2Authentication auth = tokenStore.readAuthentication(tokenValue);
    
    assertTrue(auth.isAuthenticated());
}

When the token contains an invalid issuer (http://localhost:8082) – then it's going to be verified and determined to be invalid:

@Test(expected = InvalidTokenException.class)
public void whenTokenContainInvalidIssuer_thenException() {
    String tokenValue = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9....";
    OAuth2Authentication auth = tokenStore.readAuthentication(tokenValue);
    
    assertTrue(auth.isAuthenticated());
}

Next, when the token doesn't contain an user_name claim, then it's going to be invalid:

@Test(expected = InvalidTokenException.class)
public void whenTokenDontContainUsername_thenException() {
    String tokenValue = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9....";
    OAuth2Authentication auth = tokenStore.readAuthentication(tokenValue);
    
    assertTrue(auth.isAuthenticated());
}

And finally, when the token contains an empty user_name claim, then it's also invalid:

@Test(expected = InvalidTokenException.class)
public void whenTokenContainEmptyUsername_thenException() {
    String tokenValue = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9....";
    OAuth2Authentication auth = tokenStore.readAuthentication(tokenValue);
    
    assertTrue(auth.isAuthenticated());
}

8. Conclusion

In this quick article, we had a look at the new verifier functionality in the Spring Security OAuth.

As always, the full source code is available over on GitHub.