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 contain 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.