Table of Contents
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 build on the Spring REST API + OAuth2 + Angular article in this OAuth series.
2. The OAuth2 Authorization Server
Previously, the Spring Security OAuth stack offered the possibility of setting up an Authorization Server as a Spring Application. We then had to configure it to use JwtTokenStore so that we could use JWT tokens.
However, the OAuth stack has been deprecated by Spring and now we’ll be using Keycloak as our Authorization Server.
So this time, we’ll set up our Authorization Server as an embedded Keycloak server in a Spring Boot app. It issues JWT tokens by default, so there is no need for any other configuration in this regard.
3. Resource Server
Now let’s take a look at how to configure our Resource Server to use JWT.
We’ll do this in an application.yml file:
server: port: 8081 servlet: context-path: /resource-server spring: security: oauth2: resourceserver: jwt: issuer-uri: http://localhost:8083/auth/realms/maixuanviet jwk-set-uri: http://localhost:8083/auth/realms/maixuanviet/protocol/openid-connect/certs
JWTs include all the information within the Token, so the Resource Server needs to verify the Token’s signature to make sure the data has not been modified. The jwk-set-uri property contains the public key that the server can use for this purpose.
The issuer-uri property points to the base Authorization Server URI, which can also be used to verify the iss claim as an added security measure.
Additionally, if the jwk-set-uri property is not set, the Resource Server will attempt to use the issuer-uri to determine the location of this key from the Authorization Server metadata endpoint.
It is important to note, adding the issuer-uri property mandates that we should have the Authorization Server running before we can start the Resource Server application.
Now let’s see how we can configure JWT support using Java configuration:
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.cors() .and() .authorizeRequests() .antMatchers(HttpMethod.GET, "/user/info", "/api/foos/**") .hasAuthority("SCOPE_read") .antMatchers(HttpMethod.POST, "/api/foos") .hasAuthority("SCOPE_write") .anyRequest() .authenticated() .and() .oauth2ResourceServer() .jwt(); } }
Here we are overriding the default Http Security configuration; we need to specify explicitly that we want this to behave as a Resource Server and that we’ll be using JWT formatted Access Tokens using the methods oauth2ResourceServer() and jwt(), respectively.
The above JWT configuration is what the default Spring Boot instance is providing us with. This can also be customized as we’ll see shortly.
4. Custom Claims in the Token
Now let’s set up some infrastructure to be able to add a few custom claims in the Access Token returned by the Authorization Server. 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.
Let’s take an example of a custom claim, organization, that will contain the name of a given user’s organization.
4.1. Authorization Server Configuration
For this we need to add a couple of configurations to our realm definition file, maixuanviet-realm.json:
- Add an attribute organization to our user john@test.com:
"attributes" : { "organization" : "maixuanviet" },
Add a protocolMapper called organization to the jwtClient configuration:
"protocolMappers": [{ "id": "06e5fc8f-3553-4c75-aef4-5a4d7bb6c0d1", "name": "organization", "protocol": "openid-connect", "protocolMapper": "oidc-usermodel-attribute-mapper", "consentRequired": false, "config": { "userinfo.token.claim": "true", "user.attribute": "organization", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "organization", "jsonType.label": "String" } }],
For a standalone Keycloak setup, this can also be done using the Admin console.
It’s important to remember that the JSON configuration above is specific to Keycloak, and can differ for other OAuth servers.
With this new configuration up and running, we’ll get an extra attribute, organization = maixuanviet, in the token payload for john@test.com:
{ jti: "989ce5b7-50b9-4cc6-bc71-8f04a639461e" exp: 1585242462 nbf: 0 iat: 1585242162 iss: "http://localhost:8083/auth/realms/maixuanviet" sub: "a5461470-33eb-4b2d-82d4-b0484e96ad7f" typ: "Bearer" azp: "jwtClient" auth_time: 1585242162 session_state: "384ca5cc-8342-429a-879c-c15329820006" acr: "1" scope: "profile write read" organization: "maixuanviet" preferred_username: "john@test.com" }
4.2. Use the Access Token in the Angular Client
Next we’ll want to make use of the Token information in our Angular Client application. We’ll use the angular2-jwt library for that.
We’ll make use of the organization claim in our AppService, and add a function getOrganization:
getOrganization(){ var token = Cookie.get("access_token"); var payload = this.jwtHelper.decodeToken(token); this.organization = payload.organization; return this.organization; }
This function makes use of JwtHelperService from the angular2-jwt library to decode the Access Token and get our custom claim. Now all we need to do is display it in our AppComponent:
@Component({ selector: 'app-root', template: `<nav class="navbar navbar-default"> <div class="container-fluid"> <div class="navbar-header"> <a class="navbar-brand" href="/">Spring Security Oauth - Authorization Code</a> </div> </div> <div class="navbar-brand"> <p>{{organization}}</p> </div> </nav> <router-outlet></router-outlet>` }) export class AppComponent implements OnInit { public organization = ""; constructor(private service: AppService) { } ngOnInit() { this.organization = this.service.getOrganization(); } }
5. Access Extra Claims in the Resource Server
But how can we access that information over on the Resource Server side?
5.1. Access Authentication Server Claims
That’s really simple, we just need to extract it from the org.springframework.security.oauth2.jwt.Jwt‘s AuthenticationPrincipal, as we would do for any other attribute in UserInfoController:
@GetMapping("/user/info") public Map<String, Object> getUserInfo(@AuthenticationPrincipal Jwt principal) { Map<String, String> map = new Hashtable<String, String>(); map.put("user_name", principal.getClaimAsString("preferred_username")); map.put("organization", principal.getClaimAsString("organization")); return Collections.unmodifiableMap(map); }
5.2. Configuration to Add/Remove/Rename Claims
Now what if we want to add more claims on the Resource Server side? Or remove or rename some?
Let’s say we want to modify the organization claim coming in from the Authentication Server to get the value in uppercase. However, if the claim is not present on a user, we need to set its value as unknown.
To achieve this, we’ll have to add a class that implements the Converter interface and uses MappedJwtClaimSetConverter to convert claims:
public class OrganizationSubClaimAdapter implements Converter<Map<String, Object>, Map<String, Object>> { private final MappedJwtClaimSetConverter delegate = MappedJwtClaimSetConverter.withDefaults(Collections.emptyMap()); public Map<String, Object> convert(Map<String, Object> claims) { Map<String, Object> convertedClaims = this.delegate.convert(claims); String organization = convertedClaims.get("organization") != null ? (String) convertedClaims.get("organization") : "unknown"; convertedClaims.put("organization", organization.toUpperCase()); return convertedClaims; } }
Then, in our SecurityConfig class, we need to add our own JwtDecoder instance to override the one provided by Spring Boot and set our OrganizationSubClaimAdapter as its claims converter:
@Bean public JwtDecoder customDecoder(OAuth2ResourceServerProperties properties) { NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri( properties.getJwt().getJwkSetUri()).build(); jwtDecoder.setClaimSetConverter(new OrganizationSubClaimAdapter()); return jwtDecoder; }
Now when we hit our /user/info API for the user mike@other.com, we’ll get the organization as UNKNOWN.
Note that overriding the default JwtDecoder bean configured by Spring Boot should be done carefully to ensure all the necessary configuration is still included.
6. Loading Keys From a Java Keystore
In our previous configuration, we used the Authorization Server’s default public key to verify our token’s integrity.
We can also use a keypair and certificate stored in a Java Keystore file to do the signing process.
6.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.
6.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-----
6.3. Maven Configuration
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 the 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>
6.4. Authorization Server
Now we will configure Keycloak to use our Keypair from mytest.jks by adding it to the realm definition JSON file’s KeyProvider section as follows:
{ "id": "59412b8d-aad8-4ab8-84ec-e546900fc124", "name": "java-keystore", "providerId": "java-keystore", "subComponents": {}, "config": { "keystorePassword": [ "mypass" ], "keyAlias": [ "mytest" ], "keyPassword": [ "mypass" ], "active": [ "true" ], "keystore": [ "src/main/resources/mytest.jks" ], "priority": [ "101" ], "enabled": [ "true" ], "algorithm": [ "RS256" ] } },
Here we have set the priority to 101, greater than any other Keypair for our Authorization Server, and set active to true. This is done to ensure that our Resource Server will pick this particular Keypair from the jwk-set-uri property we specified earlier.
Again, this configuration is specific to Keycloak and may differ for other OAuth Server implementations.
7. Conclusion
In this brief article we focused on setting up our Spring Security OAuth2 project to use JSON Web Tokens.
The full implementation of this article can be found over on GitHub.