This walk-through will show how simple it is to integrate Approov in a stateless API server using Java and the Spring framework.
We will see the requirements, dependencies and a step by step walk-through of the code necessary to implement Approov in a Java Spring stateless API.
Before we tackle the integration of Approov we first need to know how the Approov validation is processed on the server side, and how to set up the environment to follow this walk-through.
Note that this article assumes a basic understanding of the Approov mechanics. If you need an overview of that, please read first the Approov in Detail page.
Approov Validation Process
Before we dive into the code we need to understand the Approov validation process on the back-end side.
The Approov Token
API calls protected by Approov will typically include a header holding an Approov JWT token. This token must be checked to ensure it has not expired and that it is properly signed with the secret shared between the back-end and the Approov cloud service.
We will use the `io.jsonwebtoken.*` package to help us in the validation of the Approov JWT token.
NOTE
Just to be sure that we are on the same page, a JWT token has 3 parts which are separated by dots and represented as a string in the format of header.payload.signature. Read more about JWT tokens.
The Approov Token Binding
When an Approov token contains the key pay, its value is a base64 encoded sha256 hash of some unique identifier in the request, that we may want to bind with the Approov token, in order to enhance the security on that request, like an Authorization token.
Dummy example for the JWT token middle part, the payload:
{
"exp": 123456789, # required - the timestamp for when the token expires.
"pay":"f3U2fniBJVE04Tdecj0d6orV9qT9t52TjfHxdUqDBgY=" # optional - a sha256 hash of the token binding, encoded with base64.
}
The token binding in an Approov token is the one in the pay key:
"pay":"f3U2fniBJVE04Tdecj0d6orV9qT9t52TjfHxdUqDBgY="
ALERT: Please bear in mind that the token binding is not meant to pass application data to the API server.
System Clock
In order to correctly check for the expiration times of the Approov tokens it is important that the system clock for the Java server is synchronized automatically over the network with an authoritative time source. In Linux this is usual done with an NTP server.
Requirements
We will use Java 11.0.3 with the Spring Boot 2.1.3.RELEASE, and Gradle 5.2.1 to compile, build and run this Approov integration.
Postman is the tool we recommend to be used when simulating the queries against the API, but feel free to use another tool if you prefer.
Docker is only required for developers who want to use the Java docker stack provided by the stack bash script, which is a wrapper around docker commands.
The Postman Collection
Import this Postman collection that contains all the API endpoints for the Approov Shapes API Server and we strongly recommend you to follow this walk-through after finishing the Approov integration that we are about to start.
The Approov tokens used in the headers of this Postman collection were generated by the Approov CLI Tool and they cover all necessary scenarios, but feel free to use the script to generate some more valid and invalid tokens, with different expiry times and token bindings. Some examples of genrating Approov tokens can be found here, and for examples of generating Approov token binding you can go here.
The Docker Stack
We recommend the use of the included Docker stack to play with this Approov Integration. For details on how to use it you need to follow the setup instructions in the Approov Shapes API Server walk-through, but feel free to use your local environment to play with this Approov integration.
For example, to get a shell inisde the docker container execute from your host terminal:
./stack shell
Now that you have a shell inside the docker container you can start by checking what versions of java and gradle is running:
In the docker container shell run:
java --version
and the output should look like:
openjdk 11.0.3 2019-04-16
OpenJDK Runtime Environment (build 11.0.3+1-Debian-1bpo91)
OpenJDK 64-Bit Server VM (build 11.0.3+1-Debian-1bpo91, mixed mode, sharing)
For the gradle version just run:
gradle --version
and the output should be similar to this:
------------------------------------------------------------
Gradle 5.2.1
------------------------------------------------------------
Build time: 2019-02-08 19:00:10 UTC
Revision: f02764e074c32ee8851a4e1877dd1fea8ffb7183
Kotlin DSL: 1.1.3
Kotlin: 1.3.20
Groovy: 2.5.4
Ant: Apache Ant(TM) version 1.9.13 compiled on July 10 2018
JVM: 11.0.3 (Oracle Corporation 11.0.3+1-Debian-1bpo91)
OS: Linux 4.15.0-47-generic amd64
Dependencies
Probably the only dependencies from the build.gradle that you do not have in your own project are these ones:
implementation 'io.jsonwebtoken:jjwt-api:0.10.5'
runtime 'io.jsonwebtoken:jjwt-impl:0.10.5',
'io.jsonwebtoken:jjwt-jackson:0.10.5'
If they are not yet in your project, please add them and rebuild your project.
How to Integrate Approov
We will learn how to integrate Approov in a skeleton generated with Spring Boot, where we added some endpoints:
- / - Not protected with Approov.
- /v2/hello - Not protected with Approov.
- /v2/shapes - Approov protected.
- /v2/forms - Approov protected, and with a check for the Approov token binding.
To integrate Approov into your own project you may want to use the package com.criticalblue.approov.jwt.authentication, which contains all the code that is project agnostic. To use this package you need to configure it from the class extending the WebSecurityConfigurerAdapter. In this demo it is named WebSecurityConfig.
Understanding the WebSecurityConfig
The WebSecurityConfig is where we will set up the security configuration for the Spring framework, and this is done by @override some of the methods for the abstract class it extends from, the WebSecurityConfigurerAdapter.
When implementing Approov it is required to always check if the signature and expiration time of the Approov token is valid, and optionally to check if the Aproov token binding matches the one in the header.
For both the required and optional checks we always need to configure the Spring framework security with the ApproovAuthenticationProvider(approovConfig).
Now we need to configure what endpoints will perform the required and optional checks, and for this we need to add ApproovSecurityContextRepository(approovConfig, checkTokenBinding) and the ApproovAuthenticationEntryPoint() to the Spring framework security context, plus the endpoint name and http verbs, where the authentication should be triggered.
The approovConfig contains several pieces of information necessary to check the Approov token, such as the Approov secret used by the Approov cloud service to sign the JWT token. For more details on what it contains you can inspect the code.
Each time we add an endpoint to be protected by an Approov token we need to indicate if the Approov token binding is to be checked or not, and this is done with the boolean flag checkTokenBinding.
In order to have endpoints that perform only the required checks in the Approov token, while at the same time having other endpoints where both the required and optional checks must take place, we need to configure the Spring framework security context with the static subclasses of the main WebSecurityConfig class, and these subclasses also need to implement the abstract WebSecurityConfigurerAdapter class. These subclasses will be annotated with a configuration order @Order(n), thus their configuration order is important. So where we define @Order(1) we are telling to the Spring framework security context to perform first the required checks on the Approov token, afterwards with @Order(2) we perform the optional check for the Approov Token binding, and then with @Order(3) we proceed as usual, which in this demo is to allow any request for the endpoints / and /v2/hello to be served without authentication of any kind.
Setup Environment
If you don't have already an `.env` file, then you need to create one in the root of your project by using this .env.example as your starting point.
The .env file must contain these five variables:
APPROOV_TOKEN_BINDING_HEADER_NAME=Authorization
APPROOV_BASE64_SECRET=h+CX0tOzdAAR9l15bWAqvq7w9olk66daIH+Xk+IAHhVVHszjDzeGobzNnqyRze3lw/WVyWrc2gZfh3XXfBOmww==
APPROOV_ABORT_REQUEST_ON_INVALID_TOKEN=true
APPROOV_ABORT_REQUEST_ON_INVALID_TOKEN_BINDING=true
APPROOV_LOGGING_ENABLED=true
The Approov base64 seccret is a dummy secret used for testing the implementation in localhost with the provided Postman Collection.
The Code
Add the package com.criticalblue.approov.jwt.authentication to your current project and then configure it from the class in your project that extends the WebSecurityConfigurerAdapter.
Let's consider as a starting point an initial WebSecurityConfig without requiring authentication for any of its endpoints:
package com.criticalblue.approov.jwt;
import com.criticalblue.approov.jwt.authentication.*;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import java.util.Arrays;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private static ApproovConfig approovConfig = ApproovConfig.getInstance();
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedMethods(Arrays.asList("GET"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/error");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors();
http
.httpBasic().disable()
.formLogin().disable()
.logout().disable()
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/").permitAll()
.antMatchers(HttpMethod.GET, "/hello").permitAll()
.antMatchers(HttpMethod.GET, "/shapes").permitAll()
.antMatchers(HttpMethod.GET, "/forms").permitAll();
// the above endpoints declaration can be resumed to:
// .antMatchers(HttpMethod.GET, "/**").permitAll()
}
}
When implemeting Approov in an existing API server you will need to support for a while the old mobile apps without Approov protection, thus a possible option is to use `v2` enpoints to implement Approov, and then remove the current ones when all your users have upgraded their mobile app.
The `/v2/shapes` endpoint it will be protected only by the required checks for an Approov token, while the `/v2/forms` endpoint will have the optional check for the token binding in the Approov token.
As already mentioned we will need to add to the WebSecurityConfig a subclass for the endpoints we want to secure with only the required checks for an Approov token, another for the endpoints secured with the required and optional checks for an Approov token, and finally a subclass for endpoints that do not require authentication at all.
So, let's add the ApiWebSecurityConfig subclass to the WebSecurityConfig class to maintain access to all endpoints without any authentication.
The WebSecurityConfig should now look like this:
package com.criticalblue.approov.jwt;
import com.criticalblue.approov.jwt.authentication.*;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import java.util.Arrays;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private static ApproovConfig approovConfig = ApproovConfig.getInstance();
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedMethods(Arrays.asList("GET"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/error");
}
@Configuration
@Order(1)
public static class ApiWebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors();
http
.httpBasic().disable()
.formLogin().disable()
.logout().disable()
.csrf().disable()
.authenticationProvider(new ApproovAuthenticationProvider(approovConfig))
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/").permitAll()
.antMatchers(HttpMethod.GET, "/v2/hello").permitAll()
.antMatchers(HttpMethod.GET, "/v2/shapes").permitAll()
.antMatchers(HttpMethod.GET, "/v2/forms").permitAll();
// the above endpoints declaration can be resumed to:
// .antMatchers(HttpMethod.GET, "/**").permitAll()
}
}
}
CORS Configuration
In order to integrate Approov we will need to use an `Approov-Token` header, thus we need to allow it in the CORS configuration.
If our Approov integration also uses the Approov token binding check, then we need to also allow the header from where we want to retrieve the value we bind to the Approov token in the mobile app, that in this demo is the Authorization header.
So, we add this two new lines of code to the CORS configuration on the WebSecurityConfig class:
configuration.addAllowedHeader("Authorization");
configuration.addAllowedHeader("Approov-Token");
That will give us this new CORS configuration:
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedMethods(Arrays.asList("GET"));
configuration.addAllowedHeader("Authorization");
configuration.addAllowedHeader("Approov-Token");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
Protecting the `/v2/shapes` endpoint
To protect the `/v2/shapes` endpoint we will add the subclass ApproovWebSecurityConfig:
@Configuration
@Order(1)
public static class ApproovWebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors();
http
.httpBasic().disable()
.formLogin().disable()
.logout().disable()
.csrf().disable()
.authenticationProvider(new ApproovAuthenticationProvider(approovConfig))
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http
.securityContext()
.securityContextRepository(new ApproovSecurityContextRepository(approovConfig, false))
.and()
.exceptionHandling()
.authenticationEntryPoint(new ApproovAuthenticationEntryPoint())
.and()
.antMatcher("/v2/shapes")
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/v2/shapes").authenticated();
// Add here more endpoints that you need to protect with the required
// checks for the Approov token.
// .and()
// .antMatcher("/another-endpoint")
// .authorizeRequests()
// .antMatchers(HttpMethod.GET, "/another-endpoint").authenticated();
}
}
and change the configuration order for subclass ApiWebSecurityConfig from `1` to `2`:
@Configuration
@Order(2)
public static class ApiWebSecurityConfig extends WebSecurityConfigurerAdapter {
// omitted code ...
// REMOVE ALSO THIS LINE
.antMatchers(HttpMethod.GET, "/v2/shapes").permitAll()
// omitted code ...
}
finally you can see we have removed the line of code allowing the endpoint `/v2/shapes` to be reached without any authentication.
Protecting the `/v2/forms` endpoint
This endpoint also requires that we perform the optional check for the Approov Token binding, thus to protect the `/v2/forms` endpoint another subclass is necessary.
Let's add the subclass ApproovTokenBindingWebSecurityConfig:
@Configuration
@Order(2)
public static class ApproovTokenBindingWebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors();
http
.httpBasic().disable()
.formLogin().disable()
.logout().disable()
.csrf().disable()
.authenticationProvider(new ApproovAuthenticationProvider(approovConfig))
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http
.securityContext()
.securityContextRepository(new ApproovSecurityContextRepository(approovConfig, true))
.and()
.exceptionHandling()
.authenticationEntryPoint(new ApproovAuthenticationEntryPoint())
.and()
.antMatcher("/v2/forms")
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/v2/forms").authenticated();
// Add here more endpoints that you need to protect with the
// required and optional checks for the Approov token.
// .and()
// .antMatcher("/another-endpoint")
// .authorizeRequests()
// .antMatchers(HttpMethod.GET, "/another-endpoint").authenticated();
}
}
If you are paying attention you will have noticed that the configuration order is the same as the subclass ApiWebSecurityConfig in the previous step, thus we need to change it again, this time from `2` to `3`:
@Configuration
@Order(3)
public static class ApiWebSecurityConfig extends WebSecurityConfigurerAdapter {
// omitted code ...
// REMOVE ALSO THIS LINE
.antMatchers(HttpMethod.GET, "/v2/forms").permitAll()
// omitted code ...
}
and finally you can see that we removed the line of code allowing the endpoint `/v2/orms` to be reached without any authentication.
Putting All-Together
After we have implemented the Approov protection for the `/v2/shapes` and `/v2/forms` endpoints the class WebSecurityConfig should look like:
package com.criticalblue.approov.jwt;
import com.criticalblue.approov.jwt.authentication.*;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import java.util.Arrays;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private static ApproovConfig approovConfig = ApproovConfig.getInstance();
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedMethods(Arrays.asList("GET"));
configuration.addAllowedHeader("Authorization");
configuration.addAllowedHeader("Approov-Token");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/error");
}
@Configuration
@Order(1)
public static class ApproovWebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors();
http
.httpBasic().disable()
.formLogin().disable()
.logout().disable()
.csrf().disable()
.authenticationProvider(new ApproovAuthenticationProvider(approovConfig))
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http
.securityContext()
.securityContextRepository(new ApproovSecurityContextRepository(approovConfig, false))
.and()
.exceptionHandling()
.authenticationEntryPoint(new ApproovAuthenticationEntryPoint())
.and()
.antMatcher("/v2/shapes")
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/v2/shapes").authenticated();
}
}
@Configuration
@Order(2)
public static class ApproovTokenBindingWebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors();
http
.httpBasic().disable()
.formLogin().disable()
.logout().disable()
.csrf().disable()
.authenticationProvider(new ApproovAuthenticationProvider(approovConfig))
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http
.securityContext()
.securityContextRepository(new ApproovSecurityContextRepository(approovConfig, true))
.and()
.exceptionHandling()
.authenticationEntryPoint(new ApproovAuthenticationEntryPoint())
.and()
.antMatcher("/v2/forms")
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/v2/forms").authenticated();
}
}
@Configuration
@Order(3)
public static class ApiWebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors();
http
.httpBasic().disable()
.formLogin().disable()
.logout().disable()
.csrf().disable()
.authenticationProvider(new ApproovAuthenticationProvider(approovConfig))
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/**").permitAll();
}
}
}
The Code Difference
If we compare the initial implementation with the final result for the class WebSecurityConfig we will see that the difference is small, thus revealling that the Approov integration into an existing server is simple, easy and is done with just a few lines of code.
If you have not done it already, now is the time to follow the Approov Shapes API Server walk-through to gain an appreciation for how it all works together.
Approov in Action
Let's see how to query the Java Spring stateless API from Postman with the collection we told you to install in the requirements section.
NOTE:
For your convenience the Postman collection includes a token that only expires in the very distant future for this call "Approov Token with valid signature and expire time". For the call "Expired Approov Token with valid signature" an expired token is also included.
Postman view with an Approov token correctly signed and not expired:
Postman view with token correctly signed but this time it is expired:
Shell view with the logs for the above requests:
Request Overview:
We used this helper script to generate an Approov Token that was valid for 1 minute.
In Postman we performed 2 requests with the same token and the first one was successful, but the second request, performed 2 minutes later, failed with a 401 response because the token had expired as we can see by the log messages in the shell view.
Play Time for the Approov Shapes API Server
Now that you have had a taste of Approov in action, and if you have not done so already, it is time to follow the Approov Shapes API Server walk-through to play and get an understanding for how all this works in practice.
The Approov Shapes API Server contains endpoints with and without the Approov protection. The protected endpoints differ in the sense that one uses the optional token binding in the Approov token.
We will demonstrate how to call each API endpoint with screen-shots from Postman and from the shell. Postman is used here as an easy way to demonstrate how you can play with the Approov integration in the API server, but to see a real demo of how Approov would work in production you can request a demo here.
Production
In order to protect the communication between your mobile app and the API server is important to only communicate over a secure communication channel, also known as https.
Please bear in mind that https on its own is not enough, certificate pinning must be also used to pin the connection between the mobile app and the API server in order to prevent Man in the Middle Attacks.
We do not use HTTPS and certificate pinning in this Approov integration example because we want to be able to run the Approov Shapes API Server in localhost.
However in production is mandatory to configure and implement certificate pinning, that is made easy by using the dynamic pinning feature built-in the Appoov CLI tool, that allows to update the pins without the need to release a new version of your mobile app.