1. Overview
In this tutorial we’ll discuss how to get our Spring Security OAuth2 implementation to make use of JSON Web Tokens.
We’re also continuing to built on top of the previous article in this OAuth series.
2. Maven Configuration
First, we need to add spring-security-jwt dependency to our pom.xml:
<dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-jwt</artifactId> </dependency>
Note that we need to add spring-security-jwt dependency to both Authorization Server and Resource Server.
3. Authorization Server
Next, we will configure our Authorization Server to use JwtTokenStore – as follows:
@Configuration @EnableAuthorizationServer public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.tokenStore(tokenStore()) .accessTokenConverter(accessTokenConverter()) .authenticationManager(authenticationManager); } @Bean public TokenStore tokenStore() { return new JwtTokenStore(accessTokenConverter()); } @Bean public JwtAccessTokenConverter accessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setSigningKey("123"); return converter; } @Bean @Primary public DefaultTokenServices tokenServices() { DefaultTokenServices defaultTokenServices = new DefaultTokenServices(); defaultTokenServices.setTokenStore(tokenStore()); defaultTokenServices.setSupportRefreshToken(true); return defaultTokenServices; } }
Note that we used a symmetric key in our JwtAccessTokenConverter to sign our tokens – which means we will need to use the same exact key for the Resources Server as well.
4. Resource Server
Now, let’s take a look at our Resource Server configuration – which is very similar to the config of the Authorization Server:
@Configuration @EnableResourceServer public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter { @Override public void configure(ResourceServerSecurityConfigurer config) { config.tokenServices(tokenServices()); } @Bean public TokenStore tokenStore() { return new JwtTokenStore(accessTokenConverter()); } @Bean public JwtAccessTokenConverter accessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setSigningKey("123"); return converter; } @Bean @Primary public DefaultTokenServices tokenServices() { DefaultTokenServices defaultTokenServices = new DefaultTokenServices(); defaultTokenServices.setTokenStore(tokenStore()); return defaultTokenServices; } }
Keep in mind that we’re defining these two servers as entirely separate and independently deployable. That’s the reason we need to declare some of the same beans again here, in the new configuration.
5. Custom Claims in the Token
Let’s now set up some infrastructure to be able to add a few custom claims in the Access Token. The standard claims provided by the framework are all well and good, but most of the time we’ll need some extra information in the token to utilize on the client side.
We’ll define a TokenEnhancer to customize our Access Token with these additional claims.
In the following example, we will add an extra field “organization” to our Access Token – with this CustomTokenEnhancer:
public class CustomTokenEnhancer implements TokenEnhancer { @Override public OAuth2AccessToken enhance( OAuth2AccessToken accessToken, OAuth2Authentication authentication) { Map<String, Object> additionalInfo = new HashMap<>(); additionalInfo.put("organization", authentication.getName() + randomAlphabetic(4)); ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo); return accessToken; } }
Then, we’ll wire that into our Authorization Server configuration – as follows:
@Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain(); tokenEnhancerChain.setTokenEnhancers( Arrays.asList(tokenEnhancer(), accessTokenConverter())); endpoints.tokenStore(tokenStore()) .tokenEnhancer(tokenEnhancerChain) .authenticationManager(authenticationManager); } @Bean public TokenEnhancer tokenEnhancer() { return new CustomTokenEnhancer(); }
With this new configuration up and running – here’s what a token token payload would look like:
{ "user_name": "john", "scope": [ "foo", "read", "write" ], "organization": "johnIiCh", "exp": 1458126622, "authorities": [ "ROLE_USER" ], "jti": "e0ad1ef3-a8a5-4eef-998d-00b26bc2c53f", "client_id": "fooClientIdPassword" }
5.1. Use the Access Token in the JS Client
Finally, we’ll want to make use of the token information over in our AngualrJS client application. We’ll use the angular-jwt library for that.
So what we’re going to do is we’re going to make use of the “organization” claim in our index.html:
<p class="navbar-text navbar-right">{{organization}}</p> <script type="text/javascript" src="https://cdn.rawgit.com/auth0/angular-jwt/master/dist/angular-jwt.js"> </script> <script> var app = angular.module('myApp', ["ngResource","ngRoute", "ngCookies", "angular-jwt"]); app.controller('mainCtrl', function($scope, $cookies, jwtHelper,...) { $scope.organiztion = ""; function getOrganization(){ var token = $cookies.get("access_token"); var payload = jwtHelper.decodeToken(token); $scope.organization = payload.organization; } ... });
6. Access Extra Claims on Resource Server
But, how can we access that information over on the resource server side?
What we’ll do here is – extract the extra claims from the access token:
public Map<String, Object> getExtraInfo(OAuth2Authentication auth) { OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) auth.getDetails(); OAuth2AccessToken accessToken = tokenStore .readAccessToken(details.getTokenValue()); return accessToken.getAdditionalInformation(); }
In the following section, we’ll discuss how to add that extra information to our Authentication details by using a custom AccessTokenConverter
6.1. Custom AccessTokenConverter
Let’s create CustomAccessTokenConverter and set Authentication details with access token claims:
@Component public class CustomAccessTokenConverter extends DefaultAccessTokenConverter { @Override public OAuth2Authentication extractAuthentication(Map<String, ?> claims) { OAuth2Authentication authentication = super.extractAuthentication(claims); authentication.setDetails(claims); return authentication; } }
Note: DefaultAccessTokenConverter used to set Authentication details to Null.
6.2. Configure JwtTokenStore
Next, we’ll configure our JwtTokenStore to use our CustomAccessTokenConverter:
@Configuration @EnableResourceServer public class OAuth2ResourceServerConfigJwt extends ResourceServerConfigurerAdapter { @Autowired private CustomAccessTokenConverter customAccessTokenConverter; @Bean public TokenStore tokenStore() { return new JwtTokenStore(accessTokenConverter()); } @Bean public JwtAccessTokenConverter accessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setAccessTokenConverter(customAccessTokenConverter); } // ... }
6.3. Extra Claims available in the Authentication Object
Now that the Authorization Server added some extra claims in the token, we can now access on the Resource Server side, directly in the Authentication object:
public Map<String, Object> getExtraInfo(Authentication auth) { OAuth2AuthenticationDetails oauthDetails = (OAuth2AuthenticationDetails) auth.getDetails(); return (Map<String, Object>) oauthDetails .getDecodedDetails(); }
6.4. Authentication Details Test
Let’s make sure our Authentication object contains that extra information:
@RunWith(SpringRunner.class) @SpringBootTest( classes = ResourceServerApplication.class, webEnvironment = WebEnvironment.RANDOM_PORT) public class AuthenticationClaimsIntegrationTest { @Autowired private JwtTokenStore tokenStore; @Test public void whenTokenDoesNotContainIssuer_thenSuccess() { String tokenValue = obtainAccessToken("fooClientIdPassword", "john", "123"); OAuth2Authentication auth = tokenStore.readAuthentication(tokenValue); Map<String, Object> details = (Map<String, Object>) auth.getDetails(); assertTrue(details.containsKey("organization")); } private String obtainAccessToken( String clientId, String username, String password) { Map<String, String> params = new HashMap<>(); params.put("grant_type", "password"); params.put("client_id", clientId); params.put("username", username); params.put("password", password); Response response = RestAssured.given() .auth().preemptive().basic(clientId, "secret") .and().with().params(params).when() .post("http://localhost:8081/spring-security-oauth-server/oauth/token"); return response.jsonPath().getString("access_token"); } }
Note: we obtained the access token with extra claims from the Authorization Server, then we read the Authentication object from it which contains extra information “organization” in the details object.
7. Asymmetric KeyPair
In our previous configuration we used symmetric keys to sign our token:
@Bean public JwtAccessTokenConverter accessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setSigningKey("123"); return converter; }
We can also use asymmetric keys (Public and Private keys) to do the signing process.
7.1. Generate JKS Java KeyStore File
Let’s first generate the keys – and more specifically a .jks file – using the command line tool keytool:
keytool -genkeypair -alias mytest -keyalg RSA -keypass mypass -keystore mytest.jks -storepass mypass
The command will generate a file called mytest.jks which contains our keys -the Public and Private keys.
Also make sure keypass and storepass are the same.
7.2. Export Public Key
Next, we need to export our Public key from generated JKS, we can use the following command to do so:
keytool -list -rfc --keystore mytest.jks | openssl x509 -inform pem -pubkey
A sample response will look like this:
-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgIK2Wt4x2EtDl41C7vfp OsMquZMyOyteO2RsVeMLF/hXIeYvicKr0SQzVkodHEBCMiGXQDz5prijTq3RHPy2 /5WJBCYq7yHgTLvspMy6sivXN7NdYE7I5pXo/KHk4nz+Fa6P3L8+L90E/3qwf6j3 DKWnAgJFRY8AbSYXt1d5ELiIG1/gEqzC0fZmNhhfrBtxwWXrlpUDT0Kfvf0QVmPR xxCLXT+tEe1seWGEqeOLL5vXRLqmzZcBe1RZ9kQQm43+a9Qn5icSRnDfTAesQ3Cr lAWJKl2kcWU1HwJqw+dZRSZ1X4kEXNMyzPdPBbGmU6MHdhpywI7SKZT7mX4BDnUK eQIDAQAB -----END PUBLIC KEY----- -----BEGIN CERTIFICATE----- MIIDCzCCAfOgAwIBAgIEGtZIUzANBgkqhkiG9w0BAQsFADA2MQswCQYDVQQGEwJ1 czELMAkGA1UECBMCY2ExCzAJBgNVBAcTAmxhMQ0wCwYDVQQDEwR0ZXN0MB4XDTE2 MDMxNTA4MTAzMFoXDTE2MDYxMzA4MTAzMFowNjELMAkGA1UEBhMCdXMxCzAJBgNV BAgTAmNhMQswCQYDVQQHEwJsYTENMAsGA1UEAxMEdGVzdDCCASIwDQYJKoZIhvcN AQEBBQADggEPADCCAQoCggEBAICCtlreMdhLQ5eNQu736TrDKrmTMjsrXjtkbFXj Cxf4VyHmL4nCq9EkM1ZKHRxAQjIhl0A8+aa4o06t0Rz8tv+ViQQmKu8h4Ey77KTM urIr1zezXWBOyOaV6Pyh5OJ8/hWuj9y/Pi/dBP96sH+o9wylpwICRUWPAG0mF7dX eRC4iBtf4BKswtH2ZjYYX6wbccFl65aVA09Cn739EFZj0ccQi10/rRHtbHlhhKnj iy+b10S6ps2XAXtUWfZEEJuN/mvUJ+YnEkZw30wHrENwq5QFiSpdpHFlNR8CasPn WUUmdV+JBFzTMsz3TwWxplOjB3YacsCO0imU+5l+AQ51CnkCAwEAAaMhMB8wHQYD VR0OBBYEFOGefUBGquEX9Ujak34PyRskHk+WMA0GCSqGSIb3DQEBCwUAA4IBAQB3 1eLfNeq45yO1cXNl0C1IQLknP2WXg89AHEbKkUOA1ZKTOizNYJIHW5MYJU/zScu0 yBobhTDe5hDTsATMa9sN5CPOaLJwzpWV/ZC6WyhAWTfljzZC6d2rL3QYrSIRxmsp /J1Vq9WkesQdShnEGy7GgRgJn4A8CKecHSzqyzXulQ7Zah6GoEUD+vjb+BheP4aN hiYY1OuXD+HsdKeQqS+7eM5U7WW6dz2Q8mtFJ5qAxjY75T0pPrHwZMlJUhUZ+Q2V FfweJEaoNB9w9McPe1cAiE+oeejZ0jq0el3/dJsx3rlVqZN+lMhRJJeVHFyeb3XF lLFCUGhA7hxn2xf3x1JW -----END CERTIFICATE-----
We take only our Public key and copy it to our resource server src/main/resources/public.txt:
-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgIK2Wt4x2EtDl41C7vfp OsMquZMyOyteO2RsVeMLF/hXIeYvicKr0SQzVkodHEBCMiGXQDz5prijTq3RHPy2 /5WJBCYq7yHgTLvspMy6sivXN7NdYE7I5pXo/KHk4nz+Fa6P3L8+L90E/3qwf6j3 DKWnAgJFRY8AbSYXt1d5ELiIG1/gEqzC0fZmNhhfrBtxwWXrlpUDT0Kfvf0QVmPR xxCLXT+tEe1seWGEqeOLL5vXRLqmzZcBe1RZ9kQQm43+a9Qn5icSRnDfTAesQ3Cr lAWJKl2kcWU1HwJqw+dZRSZ1X4kEXNMyzPdPBbGmU6MHdhpywI7SKZT7mX4BDnUK eQIDAQAB -----END PUBLIC KEY-----
7.3. Maven Configuration
Next, we don’t want the JKS file to be picked up by the maven filtering process – so we’ll make sure to exclude it in the pom.xml:
<build> <resources> <resource> <directory>src/main/resources</directory> <filtering>true</filtering> <excludes> <exclude>*.jks</exclude> </excludes> </resource> </resources> </build>
If we’re using Spring Boot, we need to make sure that our JKS file is added to application classpath via the Spring Boot Maven Plugin – addResources:
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <addResources>true</addResources> </configuration> </plugin> </plugins> </build>
7.4. Authorization Server
Now, we will configure JwtAccessTokenConverter to use our KeyPair from mytest.jks – as follows:
@Bean public JwtAccessTokenConverter accessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("mytest.jks"), "mypass".toCharArray()); converter.setKeyPair(keyStoreKeyFactory.getKeyPair("mytest")); return converter; }
7.5. Resource Server
Finally, we need to configure our resource server to use Public key – as follows:
@Bean public JwtAccessTokenConverter accessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); Resource resource = new ClassPathResource("public.txt"); String publicKey = null; try { publicKey = IOUtils.toString(resource.getInputStream()); } catch (final IOException e) { throw new RuntimeException(e); } converter.setVerifierKey(publicKey); return converter; }
8. Conclusion
In this quick article we focused on setting up our Spring Security OAuth2 project to use JSON Web Tokens.
The full implementation of this tutorial can be found in the github project – this is an Eclipse based project, so it should be easy to import and run as it is.