diff --git a/.env.example b/.env.example index a1fcfe4..cc9c5d1 100644 --- a/.env.example +++ b/.env.example @@ -1,20 +1,11 @@ -########### -# SERVER -########### +# HTTP port the backend listens on +HTTP_PORT=8080 -HTTP_REDIRECT=false -HTTP_PORT=8002 -HTTPS_PORT=8003 +# Approov secret: approov secret -get base64url +APPROOV_BASE64URL_SECRET=approov_base64url_secret_here +# Localhost +SERVER_HOSTNAME=0.0.0.0 -############ -# APPROOV -############ - -# For production usage the secret is always retrieved with the Approov CLI tool, -# that can be also used to generate valid and invalid tokens for testing purposes. -# Please check the Approov docs at https://approov.io/docs/latest/approov-cli-tool-reference/#token-commands. -# -# For following along this Hello server examples you just need to use the dummy -# secret provided in the README.md#the-dummyd-secret at the root of this repo. -APPROOV_BASE64_SECRET=approov_base64_secret_here +# Command that starts your server inside the container +APP_START_CMD=/app/gradlew bootRun --args="--server.port=${HTTP_PORT} --server.address=${SERVER_HOSTNAME}" diff --git a/.gitignore b/.gitignore index 93d1061..2e537d5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,9 @@ HELP.md .gradle build/ !gradle/wrapper/gradle-wrapper.jar +.DS_Store +src/.DS_Store +src/main/.DS_Store ### STS ### .apt_generated @@ -31,3 +34,8 @@ nbdist/ .local/ .env +.config/ + +# macOS +.DS_Store +**/.DS_Store \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..eb9d106 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +# syntax=docker/dockerfile:1 +# Builds the quickstart backend container image and configures scripts/install-prerequisites.sh and scripts/build.sh +# as the entrypoint used both locally and when deployed via Docker. +FROM eclipse-temurin:21-jdk + +ENV APP_HOME=/workspace \ + RUN_MODE=container + +WORKDIR /app + +COPY . . + +# Provide APP_START_CMD via --env-file. +CMD ["bash", "scripts/build.sh"] diff --git a/EXAMPLES.md b/EXAMPLES.md deleted file mode 100644 index b26ce5b..0000000 --- a/EXAMPLES.md +++ /dev/null @@ -1,111 +0,0 @@ -# Approov Integrations Examples - -[Approov](https://approov.io) is an API security solution used to verify that requests received by your backend services originate from trusted versions of your mobile apps, and here you can find the Hello servers examples that are the base for the Approov [quickstarts](/docs) for the Java Spring framework. - -For more information about how Approov works and why you should use it you can read the [Approov Overview](/OVERVIEW.md) at the root of this repo. - -If you are looking for the Approov quickstarts to integrate Approov in your Java Spring API server then you can find them [here](/docs). - - -## Hello Server Examples - -To learn more about each Hello server example you need to read the README for each one at: - -* [Unprotected Server](./servers/hello/src/unprotected-server) -* [Approov Protected Server - Token Check](./servers/hello/src/approov-protected-server/token-check) -* [Approov Protected Server - Token Binding Check](./servers/hello/src/approov-protected-server/token-binding-check) - - -## Docker Stack - -The docker stack provided via the `docker-compose.yml` file in this folder is used for development proposes and if you are familiar with docker then feel free to also use it to follow along the examples. - -If you decide to use the docker stack then you need to bear in mind that the Postman collections, used to test the servers examples, will connect to port `8002` therefore you cannot start all docker compose services at once, for example with `docker-compose up`, instead you need to run one at a time as exemplified below in [Command Examples](#command-examples). - -### Setup - -#### For Gradle - -The docker compose file is mapping the folder `~/.gradle` inside the docker container to `./.local/.gradle` in your computer in order to persist the gradle distribution that is downloaded and installed on the first invocation of `./gradlew`. - -Create the folder in your computer with this bash command: - -```bash -mkdir -p .local/.gradle -``` - -#### For Approov - -To run the Approov protected servers you need to provide a `.env` file with the Approov Base64 secret, therefore you need to copy the file `.env.example` to `.env`: - -```bash -cp .env.example .env -``` - -Now, add [the dummy secret](/TESTING.md#the-dummy-secret) to the `env` file to be used only for test proposes on this examples. - -### Command Examples - -To run each of the Hello servers with docker compose you just need to follow the respective example below. - -#### For the unprotected server - -Run the container attached to your machine bash shell: - -```bash -sudo docker-compose up unprotected-server -``` - -or get a bash shell inside the container: - -```bash -sudo docker-compose run --rm --service-ports unprotected-server zsh -``` - -#### For the Approov Token Check - -Run the container attached to the shell: - -```bash -sudo docker-compose up approov-token-check -``` - -or get a bash shell inside the container: - -```bash -sudo docker-compose run --rm --service-ports approov-token-check zsh -``` - -#### For the Approov Token Binding Check - -Run the container attached to the shell: - -```bash -sudo docker-compose up approov-token-binding-check -``` - -or get a bash shell inside the container: - -```bash -sudo docker-compose run --rm --service-ports approov-token-binding-check zsh -``` - -## Issues - -If you find any issue while following our instructions then just report it [here](https://github.com/approov/quickstart-java-spring-token-check/issues), with the steps to reproduce it, and we will sort it out and/or guide you to the correct path. - - -## Useful Links - -If you wish to explore the Approov solution in more depth, then why not try one of the following links as a jumping off point: - -* [Approov Free Trial](https://approov.io/signup)(no credit card needed) -* [Approov Get Started](https://approov.io/product/demo) -* [Approov QuickStarts](https://approov.io/docs/latest/approov-integration-examples/) -* [Approov Docs](https://approov.io/docs) -* [Approov Blog](https://approov.io/blog/) -* [Approov Resources](https://approov.io/resource/) -* [Approov Customer Stories](https://approov.io/customer) -* [Approov Support](https://approov.io/contact) -* [About Us](https://approov.io/company) -* [Contact Us](https://approov.io/contact) diff --git a/OVERVIEW.md b/OVERVIEW.md deleted file mode 100644 index 40f2398..0000000 --- a/OVERVIEW.md +++ /dev/null @@ -1,55 +0,0 @@ -# Approov Overview - -[Approov](https://approov.io) is an API security solution used to verify that requests received by your backend services originate from trusted versions of your mobile apps. - - -## Why? - -You can learn more about Approov, the motives for adopting it, and more detail on how it works by following this [link](https://approov.io/product). In brief, Approov: - -* Ensures that accesses to your API come from official versions of your apps; it blocks accesses from republished, modified, or tampered versions -* Protects the sensitive data behind your API; it prevents direct API abuse from bots or scripts scraping data and other malicious activity -* Secures the communication channel between your app and your API with [Approov Dynamic Certificate Pinning](https://approov.io/docs/latest/approov-usage-documentation/#approov-dynamic-pinning). This has all the benefits of traditional pinning but without the drawbacks -* Removes the need for an API key in the mobile app -* Provides DoS protection against targeted attacks that aim to exhaust the API server resources to prevent real users from reaching the service or to at least degrade the user experience. - - -## How it works? - -This is a brief overview of how the Approov cloud service and the backend server fit together from a backend perspective. For a complete overview of how the mobile app and backend fit together with the Approov cloud service and the Approov SDK we recommend to read the [Approov overview](https://approov.io/product) page on our website. - -### Approov Cloud Service - -The Approov cloud service attests that a device is running a legitimate and tamper-free version of your mobile app. - -* If the integrity check passes then a valid token is returned to the mobile app -* If the integrity check fails then a legitimate looking token will be returned - -In either case, the app, unaware of the token's validity, adds it to every request it makes to the Approov protected API(s). - -### The Backend Server - -The backend server ensures that the token supplied in the `Approov-Token` header is present and valid. The validation is done by using a shared secret known only to the Approov cloud service and the backend server. - -The request is handled such that: - -* If the Approov Token is valid, the request is allowed to be processed by the API endpoint -* If the Approov Token is invalid, an HTTP 401 Unauthorized response is returned - -You can choose to log JWT verification failures, but we left it out on purpose so that you can have the choice of how you prefer to do it and decide the right amount of information you want to log. - - -## Useful Links - -If you wish to explore the Approov solution in more depth, then why not try one of the following links as a jumping off point: - -* [Approov Free Trial](https://approov.io/signup)(no credit card needed) -* [Approov Get Started](https://approov.io/product/demo) -* [Approov QuickStarts](https://approov.io/docs/latest/approov-integration-examples/) -* [Approov Docs](https://approov.io/docs) -* [Approov Blog](https://approov.io/blog/) -* [Approov Resources](https://approov.io/resource/) -* [Approov Customer Stories](https://approov.io/customer) -* [Approov Support](https://approov.io/contact) -* [About Us](https://approov.io/company) -* [Contact Us](https://approov.io/contact) diff --git a/QUICKSTARTS.md b/QUICKSTARTS.md deleted file mode 100644 index 4545959..0000000 --- a/QUICKSTARTS.md +++ /dev/null @@ -1,33 +0,0 @@ -# Approov Integration Quickstarts - -[Approov](https://approov.io) is an API security solution used to verify that requests received by your backend services originate from trusted versions of your mobile apps. - - -## The Quickstarts - -The quickstart code for the Approov backend server is split into two implementations. The first gets you up and running with basic token checking. The second uses a more advanced Approov feature, _token binding_. Token binding may be used to link the Approov token with other properties of the request, such as user authentication (more details can be found [here](https://approov.io/docs/latest/approov-usage-documentation/#token-binding)). -* [Approov token check quickstart](/docs/APPROOV_TOKEN_QUICKSTART.md) -* [Approov token check with token binding quickstart](/docs/APPROOV_TOKEN_BINDING_QUICKSTART.md) - -Both the quickstarts are built from the unprotected example server defined [here](/servers/hello/src/unprotected-server). - - -## Issues - -If you find any issue while following our instructions then just report it [here](https://github.com/approov/quickstart-java-spring-token-check/issues), with the steps to reproduce it, and we will sort it out and/or guide you to the correct path. - - -## Useful Links - -If you wish to explore the Approov solution in more depth, then why not try one of the following links as a jumping off point: - -* [Approov Free Trial](https://approov.io/signup)(no credit card needed) -* [Approov Get Started](https://approov.io/product/demo) -* [Approov QuickStarts](https://approov.io/docs/latest/approov-integration-examples/) -* [Approov Docs](https://approov.io/docs) -* [Approov Blog](https://approov.io/blog/) -* [Approov Resources](https://approov.io/resource/) -* [Approov Customer Stories](https://approov.io/customer) -* [Approov Support](https://approov.io/contact) -* [About Us](https://approov.io/company) -* [Contact Us](https://approov.io/contact) diff --git a/README.md b/README.md index 72af4cc..e0bc16b 100644 --- a/README.md +++ b/README.md @@ -1,189 +1,243 @@ -# Approov QuickStart - Java Spring Token Check +# Approov Backend Quickstart - Java Spring -[Approov](https://approov.io) is an API security solution used to verify that requests received by your backend services originate from trusted versions of your mobile apps. +This project provides a server-side example of Approov token verification for a protected backend API. It exposes a simple API that verifies Approov tokens before granting access to protected endpoints and demonstrates how the endpoints behave under the current Approov configuration: -This repo implements the Approov server-side request verification code with the Java Spring framework in a simple Hello API server, which performs the verification check before allowing valid traffic to be processed by the API endpoint. + - `/unprotected` - no Approov token required. + - `/token-check` - requires a valid Approov token. + - `/token-binding` - requires a valid Approov token which is bound to a header value. + - `/token-double-binding` - requires a valid Approov token which is bound to two header values. -Originally this repo was just to show the Approov token integration example on a Java Spring API as described in the article: [Approov Integration in a Java Spring API](https://approov.io/blog//approov-integration-in-a-python-flask-api), that you can still find at [/servers/shapes-api](/servers/shapes-api). +In this example, Approov protection is implemented by the [ApproovTokenVerifier](https://github.com/approov/quickstart-java-spring-token-check/blob/refactor/spring-quickstart/src/main/java/io/approov/ApproovApplication.java#L225-L353), which validates the Approov token (signature + expiry) and enforces token binding where required. The filter is wired into Spring Security in the [SecurityConfig](https://github.com/approov/quickstart-java-spring-token-check/blob/refactor/spring-quickstart/src/main/java/io/approov/ApproovApplication.java#L188-L219). +## Approov Token Verification Flow -## Approov Integration Quickstart +1. **Token Request:** + The Approov SDK inside the mobile app securely communicates with the Approov Cloud Service to obtain a short-lived [Approov Token](https://ext.approov.io/docs/latest/approov-usage-documentation/#approov-tokens) (a signed JWT). + Additionally, you can use the CLI [token commands](https://ext.approov.io/docs/latest/approov-cli-tool-reference/#token-commands) to validate tokens, generate new ones, and set the data hash. -The quickstart was tested with the following Operating Systems: +2. **Token Attachment:** + The app attaches this token to every API request using the `Approov-Token` HTTP header. -* Ubuntu 20.04 -* MacOS Big Sur -* Windows 10 WSL2 - Ubuntu 20.04 +3. **Server Validation:** + The [server verifies](https://ext.approov.io/docs/latest/approov-usage-documentation/#approov-architecture) the token using the shared Approov secret, checking its: + - Signature authenticity + - Expiration (`exp` claim) + - Other claims if configured -First, setup the [Approov CLI](https://approov.io/docs/latest/approov-installation/index.html#initializing-the-approov-cli). +4. **Token Binding (Optional):** + [Token binding](https://ext.approov.io/docs/latest/approov-usage-documentation/#token-binding) is configured by the app via the Approov SDK, which hashes a chosen binding value (for example the `Authorization` header) and embeds it into the Approov token. + The protected API then computes the same hash from the incoming request and verifies that it matches the `pay` claim, preventing token reuse or replay attacks. For local testing, you can also generate example tokens with a binding using the Approov CLI. -Now, register the API domain for which Approov will issues tokens: +5. **Request Decision:** + If all checks pass → the request is trusted and processed `200 OK`. + If validation fails → the server responds with `401 Unauthorized`. + +## Requirements: + +1. ***Approov account*** - If you're new, sign up for an [Approov trial account](https://approov.io/signup). +2. ***Approov CLI initialized*** - Follow the [installation guide](https://ext.approov.io/docs/latest/approov-installation/#initializing-the-approov-cli) and confirm `approov whoami` works. +3. ***Install curl*** - Ensure the `curl` CLI is available. +4. ***Create .env file*** - copy `.env.example` so there is a place to store the secret key. + ```bash + cp .env.example .env + ``` + +5. ***Configure secret*** - fetch the secret and add it to `.env` (`APPROOV_BASE64URL_SECRET`): + ```bash + approov secret -get base64url + ``` + +6. ***Register API domain*** - point Approov at your backend API (default example.com): + ```bash + approov api -add example.com + ``` + +7. ***Install Docker and Docker Compose*** - follow the official guide: [Docker docs](https://docs.docker.com/get-started/get-docker/) + +## Try it yourself using Docker + +*If you have all requirements, you can run* ```bash -approov api -add api.example.com +bash run-server.sh ``` -> **NOTE:** By default a symmetric key (HS256) is used to sign the Approov token on a valid attestation of the mobile app for each API domain it's added with the Approov CLI, so that all APIs will share the same secret and the backend needs to take care to keep this secret secure. -> -> A more secure alternative is to use asymmetric keys (RS256 or others) that allows for a different keyset to be used on each API domain and for the Approov token to be verified with a public key that can only verify, but not sign, Approov tokens. -> -> To implement the asymmetric key you need to change from using the symmetric HS256 algorithm to an asymmetric algorithm, for example RS256, that requires you to first [add a new key](https://approov.io/docs/latest/approov-usage-documentation/#adding-a-new-key), and then specify it when [adding each API domain](https://approov.io/docs/latest/approov-usage-documentation/#keyset-key-api-addition). Please visit [Managing Key Sets](https://approov.io/docs/latest/approov-usage-documentation/#managing-key-sets) on the Approov documentation for more details. +This script: +- Builds and starts the container via `scripts/build.sh` (`docker build` + `docker run`) and waits for `/approov-state` to be ready. -Next, enable your Approov `admin` role with: +*Once finished, press `Ctrl+C` to stop log tailing; the container keeps running unless you stop it. Use `docker ps` to find the container name and `docker stop ` to stop it.* + +### Automated and Manual Testing + +*When the server is running (in a different terminal), validate the endpoints via the automated bash script or by running the manual checks below* ```bash -eval `approov role admin` -```` +bash test.sh +``` + +This script: +- Verifies that the `approov` and `curl` commands are installed. +- Checks Approov status by calling `/approov-state` (enabled vs disabled). +- Runs endpoint tests against `/unprotected` (no token), `/token-check` (valid/invalid Approov tokens), `/token-binding` (token bound to `Authorization`), and `/token-double-binding` (token bound to `Authorization` + `Content-Digest`). +- Logs full request/response details to `.config/logs/.log`. + +#### *1. Unprotected Endpoint (No Approov)* + +- The client sends a normal HTTP request. +- The server **does not verify** any Approov token or extra authentication header. +- This means **any client** (even tampered or unauthorized) can call the API if they know the URL. -For the Windows powershell: +*The following example shows how the API responds when no Approov protection is applied.* ```bash -set APPROOV_ROLE=admin:___YOUR_APPROOV_ACCOUNT_NAME_HERE___ -```` +curl -iX GET http://localhost:8080/unprotected +``` + +The response will be `200 OK` for this request: +```text +HTTP/1.1 200 OK +Content-Type: application/json +Cache-Control: no-cache +``` + +#### *2. Approov Token Check* + +- The client includes an `Approov-Token` (a short-lived JWT) in each API request header. +- The server verifies this token using the **Approov secret key** that is securely configured on the backend and checks: + - Token verification - confirms the token is signed by the Approov secret. + - Expiration (`exp` claim) - ensures the token is still valid. +- If the token is valid → request is trusted. +- If invalid → server returns `401 Unauthorized`. +- **Purpose**: Protect API endpoints so that only authentic, unmodified Approov-integrated apps can access them. + +***The following example shows how the API responds when an Approov token is required.*** -Now, get your Approov Secret with the [Approov CLI](https://approov.io/docs/latest/approov-installation/index.html#initializing-the-approov-cli): +*Generate a valid Approov token:* ```bash -approov secret -get base64 +approov token -genExample example.com ``` -Next, add the [Approov secret](https://approov.io/docs/latest/approov-usage-documentation/#account-secret-key-export) to your project `.env` file: +*Use the generated token in the `Approov-Token` header and `/token-check` endpoint.* -```env -APPROOV_BASE64_SECRET=approov_base64_secret_here +```bash +curl -iX GET http://localhost:8080/token-check \ + -H "Approov-Token: valid_approov_token_here" +``` + +The response will be `200 OK` for this request: + +```text +HTTP/1.1 200 OK +Content-Type: application/json +Cache-Control: no-cache ``` -Now, to check the Approov token you need to add the [jwtk/jjwt](https://github.com/jwtk/jjwt) package to your `build.gradle` dependencies: +*If you use an invalid or missing token, the server will respond with `401 Unauthorized`.* + +#### *3. Approov Token Binding Check* + +- The client sends two headers on authenticated API calls: + - `Approov-Token` + - `Authorization` – your auth token value (e.g., `ExampleAuthToken==`) +- The server verifies the token and ensures that the bound value matches what the app used. +- Prevents token replay - the Approov token cannot be reused or stolen for another session. +- **Use case:** Stronger protection for authenticated API calls tied to a specific user or device. -```gradle -dependencies { +***The following example shows how the API responds when an Approov token with binding is required.*** - // omitted.. +*Generate a valid Approov token bound to the `Authorization` header:* - implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.2' - runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2', - 'io.jsonwebtoken:jjwt-jackson:0.11.2' -} +```bash +approov token -setDataHashInToken ExampleAuthToken== -genExample example.com +``` + +*Use the generated token with binding in the Approov-Token and Authorization headers when calling the /token-binding endpoint.* + +```bash +curl -iX GET http://localhost:8080/token-binding \ + -H "Approov-Token: valid_approov_token_here" \ + -H "Authorization: ExampleAuthToken==" ``` -Next, add the package `com.criticalblue.approov.jwt.authentication` to your current project by copying (from this repo) the entire [authentication](/servers/hello/src/approov-protected-server/token-check/src/main/java/com/criticalblue/approov/jwt/authentication) folder into your project. - - -Now, use it from the class in your project that extends the `WebSecurityConfigurerAdapter`. For example: - -```java -package com.yourcompany.projectname; - -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 - // @IMPORTANT Approov token check must be at Order 1. Any other type of - // Authentication (User, API Key, etc.) for the request should go - // after with @Order(2) - @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() - // @APPROOV The Approov Token check is triggered here. - .authenticationProvider(new ApproovAuthenticationProvider(approovConfig)) - .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); - - http - .securityContext() - // @APPROOV The Approov Token check is configured here. - .securityContextRepository(new ApproovSecurityContextRepository(approovConfig)) - .and() - .exceptionHandling() - // @APPROOV The Approov Token check is done here - .authenticationEntryPoint(new ApproovAuthenticationEntryPoint()) - .and() - // @APPROOV This matcher will require the Approov token for - // all API endpoints. - .antMatcher("/") - .authorizeRequests() - .antMatchers(HttpMethod.GET, "/**").authenticated(); - } - } -} +The response will be `200 OK` for this request: + +```text +HTTP/1.1 200 OK +Content-Type: application/json +Cache-Control: no-cache ``` -> **NOTE:** When the Approov token validation fails we return a `401` with an empty body, because we don't want to give clues to an attacker about the reason the request failed, and you can go even further by returning a `400`. +*If you use an invalid or missing header or token, the server will respond with `401 Unauthorized`.* -Not enough details in the bare bones quickstart? No worries, check the [detailed quickstarts](QUICKSTARTS.md) that contain a more comprehensive set of instructions, including how to test the Approov integration. +#### Approov Token Binding Check with Two Different Bound Values +- The client sends three headers on authenticated API calls: + - `Approov-Token` + - `Authorization` + - `Content-Digest` It is combined with the `Authorization` header to create a stronger binding. +- Both are included in the hash inside the Approov token. This means the server verifies a single hash that covers both authentication credentials. +- **Use case:** Stronger protection then single binding by tying both headers together. -## More Information +***The following example shows how the API responds when an Approov token with two bindings is required.*** -* [Approov Overview](OVERVIEW.md) -* [Detailed Quickstarts](QUICKSTARTS.md) -* [Examples](EXAMPLES.md) -* [Testing](TESTING.md) +*Generate a valid Approov token bound to the `Authorization` and `Content-Digest` headers:* -### System Clock +```bash +approov token -setDataHashInToken ExampleAuthToken==ContentDigest== -genExample example.com +``` -In order to correctly check for the expiration times of the Approov tokens is very important that the backend server is synchronizing automatically the system clock over the network with an authoritative time source. In Linux this is usually done with a NTP server. +*Use the generated token with two bindings in the Approov-Token and Authorization headers when calling the `/token-double-binding` endpoint.* +```bash +curl -iX GET http://localhost:8080/token-double-binding \ + -H "Approov-Token: valid_approov_token_here" \ + -H "Authorization: ExampleAuthToken==" \ + -H "Content-Digest: ContentDigest==" +``` -## Issues +The response will be `200 OK` for this request. -If you find any issue while following our instructions then just report it [here](https://github.com/approov/quickstart-java-spring-token-check/issues), with the steps to reproduce it, and we will sort it out and/or guide you to the correct path. +```text +HTTP/1.1 200 OK +Content-Type: application/json +Cache-Control: no-cache +``` +*If you use an invalid or missing header or token, the server will respond with `401 Unauthorized`.* -## Useful Links +## Enable or Disable Approov Protection -If you wish to explore the Approov solution in more depth, then why not try one of the following links as a jumping off point: +When the example server is running on `localhost:8080`, you can toggle Approov protection with these commands: + +```bash +curl -X POST http://localhost:8080/approov/disable # disable the Approov service + +curl -X POST http://localhost:8080/approov/enable # enable the Approov service + +curl -X GET http://localhost:8080/approov-state # check current state +``` + +*You can rerun the tests with Approov disabled to observe how the application behaves when the Approov protection is ***no longer active***.* + +## Reporting Issues + +**Environments where the quickstart was tested:** +```text +* Runtime: Java 21 (JVM 21.0.9) +* Framework: Spring Boot 3.2.5 +* Build Tool: Gradle 8.7 +``` + +If you encounter any problems while following this guide, or have any other concerns, please let us know by opening an issue [here](https://github.com/approov/quickstart-java-spring-token-check/issues) and we will be happy to assist you. + +## Useful Links -* [Approov Free Trial](https://approov.io/signup)(no credit card needed) -* [Approov QuickStarts](https://approov.io/docs/latest/approov-integration-examples/) -* [Approov Get Started](https://approov.io/product/demo) -* [Approov Docs](https://approov.io/docs) -* [Approov Blog](https://approov.io/blog/) +* [Approov QuickStarts](https://approov.io/resource/quickstarts/) +* [Approov Docs](https://ext.approov.io/docs) +* [Approov Blog](https://approov.io/blog) * [Approov Resources](https://approov.io/resource/) * [Approov Customer Stories](https://approov.io/customer) -* [Approov Support](https://approov.io/contact) +* [Approov Support](https://approov.io/info/technical-support) * [About Us](https://approov.io/company) -* [Contact Us](https://approov.io/contact) +* [Contact Us](https://approov.io/info/contact) \ No newline at end of file diff --git a/TESTING.md b/TESTING.md deleted file mode 100644 index beb54dd..0000000 --- a/TESTING.md +++ /dev/null @@ -1,43 +0,0 @@ -# Approov Integration Testing - -[Approov](https://approov.io) is an API security solution used to verify that requests received by your backend services originate from trusted versions of your mobile apps. - -## Testing the Approov Integration - -Each Quickstart has at their end a dedicated section for testing, that will walk you through the necessary steps to use the Approov CLI to generate valid and invalid tokens to test your Approov integration without the need to rely on the genuine mobile app(s) using your backend. - -* [Approov Token](/docs/APPROOV_TOKEN_QUICKSTART.md#test-your-approov-integration) test examples. -* [Approov Token Binding](/docs/APPROOV_TOKEN_BINDING_QUICKSTART.md#test-your-approov-integration) test examples. - -### Testing with Postman - -A ready-to-use Postman collection can be found [here](https://raw.githubusercontent.com/approov/postman-collections/master/quickstarts/hello-world/hello-world.postman_collection.json). It contains a comprehensive set of example requests to send to the backend server for testing. The collection contains requests with valid and invalid Approov tokens, and with and without token binding. - -### Testing with Curl - -An alternative to the Postman collection is to use cURL to make the API requests. Check some examples [here](https://github.com/approov/postman-collections/blob/master/quickstarts/hello-world/hello-world.postman_curl_requests_examples.md). - -### The Dummy Secret - -The valid Approov tokens in the Postman collection and cURL requests examples were signed with a dummy secret that was generated with `openssl rand -base64 64 | tr -d '\n'; echo`, therefore not a production secret retrieved with `approov secret -get base64`, thus in order to use it you need to set the `APPROOV_BASE64_SECRET`, in the `.env` file for each [Approov integration example](/src/approov-protected-server), to the following value: `h+CX0tOzdAAR9l15bWAqvq7w9olk66daIH+Xk+IAHhVVHszjDzeGobzNnqyRze3lw/WVyWrc2gZfh3XXfBOmww==`. - - -## Issues - -If you find any issue while following our instructions then just report it [here](https://github.com/approov/quickstart-java-spring-token-check/issues), with the steps to reproduce it, and we will sort it out and/or guide you to the correct path. - - -## Useful Links - -If you wish to explore the Approov solution in more depth, then why not try one of the following links as a jumping off point: - -* [Approov Free Trial](https://approov.io/signup)(no credit card needed) -* [Approov Get Started](https://approov.io/product/demo) -* [Approov QuickStarts](https://approov.io/docs/latest/approov-integration-examples/) -* [Approov Docs](https://approov.io/docs) -* [Approov Blog](https://approov.io/blog/) -* [Approov Resources](https://approov.io/resource/) -* [Approov Customer Stories](https://approov.io/customer) -* [Approov Support](https://approov.io/contact) -* [About Us](https://approov.io/company) -* [Contact Us](https://approov.io/contact) diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..d3b780b --- /dev/null +++ b/build.gradle @@ -0,0 +1,27 @@ +plugins { + id 'org.springframework.boot' version '3.2.5' + id 'java' +} + +apply plugin: 'io.spring.dependency-management' + +group = 'io.approov' +version = '0.0.1-SNAPSHOT' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'io.jsonwebtoken:jjwt-api:0.13.0' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.13.0' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.13.0' +} diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 72e2d8d..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,49 +0,0 @@ -version: "2.3" - -services: - - unprotected-server: - image: approov/openjdk:11.0.3 - build: - context: ./docker - env_file: - - .env - networks: - - default - command: sh -c "./gradlew build && ./gradlew bootRun" - ports: - - ${HOST_IP:-127.0.0.1}:${HTTP_PORT:-8002}:${HTTP_PORT:-8002} - volumes: - - ./servers/hello/src/unprotected-server:/home/developer/workspace - - ./.local/.gradle:/home/developer/.gradle - - approov-token-check: - image: approov/openjdk:11.0.3 - build: - context: ./docker - env_file: - - .env - networks: - - default - command: sh -c "./gradlew build && ./gradlew bootRun" - ports: - - ${HOST_IP:-127.0.0.1}:${HTTP_PORT:-8002}:${HTTP_PORT:-8002} - volumes: - - ./servers/hello/src/approov-protected-server/token-check:/home/developer/workspace - - ./.local/.gradle:/home/developer/.gradle - - approov-token-binding-check: - image: approov/openjdk:11.0.3 - build: - context: ./docker - env_file: - - .env - networks: - - default - command: sh -c "./gradlew build && ./gradlew bootRun" - ports: - - ${HOST_IP:-127.0.0.1}:${HTTP_PORT:-8002}:${HTTP_PORT:-8002} - volumes: - - ./servers/hello/src/approov-protected-server/token-binding-check:/home/developer/workspace - - ./.local/.gradle:/home/developer/.gradle - diff --git a/docker/Dockerfile b/docker/Dockerfile deleted file mode 100644 index aec047e..0000000 --- a/docker/Dockerfile +++ /dev/null @@ -1,96 +0,0 @@ -FROM openjdk:11.0.3 - -ARG CONTAINER_USER="developer" -ARG CONTAINER_UID="1000" -ARG ZSH_THEME="robbyrussell" -ARG GRADLE_VERSION=5.2.1 - - -# Will not prompt for questions -ENV DEBIAN_FRONTEND=noninteractive \ - CONTAINER_USER="${CONTAINER_USER}" \ - CONTAINER_UID="${CONTAINER_UID}" \ - ROOT_CA_DIR=/root-ca/ \ - ROOT_CA_KEY="self-signed-root-ca.key" \ - ROOT_CA_PEM="self-signed-root-ca.pem" \ - ROOT_CA_NAME="ApproovStackRootCA" \ - PROXY_CA_FILENAME="FirewallProxyCA.crt" \ - PROXY_CA_PEM="certificates/FirewallProxyCA.crt" \ - PROXY_CA_NAME="FirewallProxy" \ - NO_AT_BRIDGE=1 \ - DISPLAY=":0" \ - GRADLE_HOME=/opt/gradle/gradle-"${GRADLE_VERSION}" \ - PATH=/opt/gradle/gradle-"${GRADLE_VERSION}"/bin:${PATH} - -COPY ./setup ${ROOT_CA_DIR} - -RUN apt update && \ - apt -y upgrade && \ - - apt -y install \ - python3 \ - python3-pip \ - locales \ - tzdata \ - ca-certificates \ - inotify-tools \ - libnss3-tools \ - zip \ - zsh \ - curl \ - git \ - maven && \ - - printf "\n\n----------> FORCING INSTALLATION OF MISSING DEPENDENCIES <------------\n\n" && \ - apt -y -f install && \ - - printf "\n\n----------> FIXING INOTIFY WATCHES <------------\n\n" && \ - #https://github.com/guard/listen/wiki/Increasing-the-amount-of-inotify-watchers - printf "fs.inotify.max_user_watches=524288\n" >> /etc/sysctl.conf && \ - - printf "\n\n----------> ADDING LOCALE <------------\n\n" && \ - echo "en_GB.UTF-8 UTF-8" > /etc/locale.gen && \ - locale-gen en_GB.UTF-8 && \ - dpkg-reconfigure locales && \ - - printf "\n\n----------> ADDING A USER <------------\n\n" && \ - useradd -m -u ${CONTAINER_UID} -s /usr/bin/zsh ${CONTAINER_USER} && \ - - printf "\n\n----------> INSTALLING CUSTOM CERTIFICATES <------------\n\n" && \ - cd ${ROOT_CA_DIR} && \ - ./setup-root-certificate.sh "${ROOT_CA_KEY}" "${ROOT_CA_PEM}" "${ROOT_CA_NAME}" && \ - ./add-proxy-certificate.sh "${PROXY_CA_PEM}" && \ - - printf "\n\n----------> INSTALLING GRADLE <------------\n\n" && \ - curl -o gradle.zip -fsSL https://services.gradle.org/distributions/gradle-"${GRADLE_VERSION}"-bin.zip && \ - unzip -d /opt/gradle gradle.zip && \ - rm -f gradle.zip && \ - gradle --version && \ - - printf "\n\n----------> INSTALLING OH MY ZSH <------------\n\n" && \ - bash -c "$(curl -fsSL https://raw.githubusercontent.com/robbyrussell/oh-my-zsh/master/tools/install.sh)" && \ - chsh -s /usr/bin/zsh && \ - cp -R /root/.oh-my-zsh /home/"${CONTAINER_USER}" && \ - cp /root/.zsh* /home/"${CONTAINER_USER}" && \ - sed -i "s/\/root/\/home\/${CONTAINER_USER}/g" /home/"${CONTAINER_USER}"/.zshrc && \ - chown -R "${CONTAINER_USER}":"${CONTAINER_USER}" /home/"${CONTAINER_USER}" && \ - - printf "\n\n----------> CLEANUP <------------\n\n" && \ - rm -rvf /var/lib/apt/lists/* - -ENV LANG=en_GB.UTF-8 \ - LANGUAGE=en_GB:en \ - LC_ALL=en_GB.UTF-8 - -USER ${CONTAINER_USER} - -RUN pip3 install \ - pyjwt \ - docopt - -# pip install will put the executables under ~/.local/bin -ENV PATH=/home/"${CONTAINER_USER}"/.local/bin:$PATH - -WORKDIR /home/${CONTAINER_USER}/workspace - -CMD ["zsh"] diff --git a/docker/setup/adb-setup-certificate.sh b/docker/setup/adb-setup-certificate.sh deleted file mode 100755 index 2b87a9b..0000000 --- a/docker/setup/adb-setup-certificate.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash - -# https://stackoverflow.com/a/48814971/6454622 - -set -eu - -CA_PEM=${1?Missing certificate file name} - -cert_name=$(openssl x509 -inform PEM -subject_hash_old -in ${CA_PEM} | head -1) -cat ${CA_PEM} > $cert_name -openssl x509 -inform PEM -text -in ${CA_PEM} -out nul >> $cert_name - -adb shell mount -o rw,remount,rw /system -adb push $cert_name /system/etc/security/cacerts/ -adb shell mount -o ro,remount,ro /system diff --git a/docker/setup/add-certificate-to-browser.sh b/docker/setup/add-certificate-to-browser.sh deleted file mode 100755 index cce7176..0000000 --- a/docker/setup/add-certificate-to-browser.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/bin/bash - -set -eu - -### -# https://thomas-leister.de/en/how-to-import-ca-root-certificate/ -### - - -### Script installs root.cert.pem to certificate trust store of applications using NSS -### (e.g. Firefox, Thunderbird, Chromium) -### Mozilla uses cert8, Chromium and Chrome use cert9 - -### -### Requirement: apt install libnss3-tools -### - -CA_PEM="${1?Missing file path for the PEM certificate}" -CA_NAME="${2?Missing Certificate Name}" -BROWSER_CONFIG_DIR="${3:-/home/node}" - -printf "\n>>> ADDING CERTIFICATE TO BROWSERS TRUSTED STORE <<<\n" - -if [ -f "${CA_PEM}" ] - then - printf "\n--> CERTIFICATE FILE: ${CA_PEM}\n" - printf "\n--> CERTIFICATE NAME: ${CA_NAME}\n" - printf "\n--> BROWSER CONFIG DIR: ${BROWSER_CONFIG_DIR}\n" - - ### - ### For cert8 (legacy - DBM) - ### - for certDB in $(find ${BROWSER_CONFIG_DIR} -name "cert8.db") - do - certdir=$(dirname ${certDB}); - certutil -A -n "${CA_NAME}" -t "TCu,Cu,Tu" -i ${CA_PEM} -d dbm:${certdir} - done - - ### - ### For cert9 (SQL) - ### - for certDB in $(find ${BROWSER_CONFIG_DIR} -name "cert9.db") - do - certdir=$(dirname ${certDB}); - certutil -A -n "${CA_NAME}" -t "TCu,Cu,Tu" -i ${CA_PEM} -d sql:${certdir} - done - else - printf "\n>>> CERTIFICATE FILE NOT FOUND FOR: ${CA_PEM}\n" -fi diff --git a/docker/setup/add-certificate-to-node-server.sh b/docker/setup/add-certificate-to-node-server.sh deleted file mode 100755 index 6d84496..0000000 --- a/docker/setup/add-certificate-to-node-server.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash - -set -eu - -CA_PEM_FILE="${1?Missing name for certificate file}" -CA_EXTENSION="${CA_PEM_FILE##*.}" - -if [ "${CA_EXTENSION}" != "pem" ] - then - printf "\nFATAL ERROR: Certificate must use .pem extension\n\n" - exit 1 -fi - -if [ -f "${CA_PEM_FILE}" ] - then - printf "\n>>> ADDING A CERTIFICATE TO NODE SERVER <<<\n" - - # Add certificate to node, so that we can use npm install - printf "cafile=${CA_PEM_FILE}" >> /root/.npmrc - printf "cafile=${CA_PEM_FILE}" >> /home/${CONTAINER_USER}/.npmrc; - - printf "\n >>> CERTICATE ADDED SUCCESEFULY<<<\n" - - else - printf "\n >>> NO CERTIFICATE TO ADD <<<\n" -fi - diff --git a/docker/setup/add-proxy-certificate.sh b/docker/setup/add-proxy-certificate.sh deleted file mode 100755 index f102483..0000000 --- a/docker/setup/add-proxy-certificate.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash - -set -eu - -PROXY_CA_PEM="${1?Missing name for Proxy CRT file}" - -if [ -f "${PROXY_CA_PEM}" ] - then - printf "\n>>> ADDING A PROXY CERTIFICATE TO THE TRUSTED STORE <<<\n" - - # add certificate tpo the trust store - cp -v ${PROXY_CA_PEM} /usr/local/share/ca-certificates - update-ca-certificates - - # verifies the certificate - openssl x509 -in ${PROXY_CA_PEM} -text -noout > "${PROXY_CA_PEM}.txt" - - else - printf "\n >>> FATAL ERROR: Certificate not found in path ${PROXY_CA_PEM} <<<\n" -fi diff --git a/docker/setup/certificates/.gitignore b/docker/setup/certificates/.gitignore deleted file mode 100644 index d6b7ef3..0000000 --- a/docker/setup/certificates/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore diff --git a/docker/setup/create-domain-certificate.sh b/docker/setup/create-domain-certificate.sh deleted file mode 100755 index cd42c42..0000000 --- a/docker/setup/create-domain-certificate.sh +++ /dev/null @@ -1,59 +0,0 @@ -#!/bin/bash - -set -eu - -### -# inspired https://fabianlee.org/2018/02/17/ubuntu-creating-a-trusted-ca-and-san-certificate-using-openssl-on-ubuntu/ -### - - -DOMAIN="${1:-example.com}" -ROOT_CA_KEY="${2?Missing Name for root certificate KEY file}" -ROOT_CA_PEM="${3?Missing Name for root certificate PEM file}" - -DOMAIN_CA_KEY="${DOMAIN}.key" -DOMAIN_CA_CSR="${DOMAIN}.csr" -DOMAIN_CA_CRT="${DOMAIN}.crt" -DOMAIN_CA_TXT="${DOMAIN}.txt" -CONFIG_FILE="${DOMAIN}.cnf" - - -printf "\n>>> MERGINGING CONFIGURATION FROM ${DOMAIN_CA_TXT} INTO ${CONFIG_FILE} <<<\n" -cat openssl.cnf ${DOMAIN_CA_TXT} > ${CONFIG_FILE} - - -printf "\n>>> GENERATING KEY FOR DOMAIN CERTIFICATE: ${DOMAIN_CA_KEY} <<<\n" - -# generate the private/public RSA key pair for the domain -openssl genrsa -out ${DOMAIN_CA_KEY} 4096 - -printf "\n>>> GENERATING CSR FOR DOMAIN CERTIFICATE: ${DOMAIN_CA_CSR} <<<\n" - -# create the server certificate signing request: -openssl req \ - -subj "/CN=${DOMAIN}" \ - -extensions v3_req \ - -sha256 \ - -new \ - -key ${DOMAIN_CA_KEY} \ - -out ${DOMAIN_CA_CSR} - -printf "\n>>> GENERATING CRT FOR DOMAIN CERTIFICATE: ${DOMAIN_CA_CRT} <<<\n" - -# generate the server certificate using the: server signing request, the CA signing key, and CA cert. -openssl x509 \ - -req \ - -extensions v3_req \ - -days 3650 \ - -sha256 \ - -in ${DOMAIN_CA_CSR} \ - -CA ${ROOT_CA_PEM} \ - -CAkey ${ROOT_CA_KEY} \ - -CAcreateserial \ - -out ${DOMAIN_CA_CRT} \ - -extfile ${CONFIG_FILE} - -# verifies the certificate -openssl x509 -in ${DOMAIN_CA_CRT} -text -noout > ${DOMAIN}.txt - -printf "\n >>> CERTIFICATE CREATED FOR DOMAIN: ${DOMAIN} <<<\n" diff --git a/docker/setup/localhost.txt b/docker/setup/localhost.txt deleted file mode 100644 index 9ffb34d..0000000 --- a/docker/setup/localhost.txt +++ /dev/null @@ -1,15 +0,0 @@ -[ v3_req ] - -# Extensions to add to a certificate request - -basicConstraints = CA:FALSE -keyUsage = nonRepudiation, digitalSignature, keyEncipherment - -#extendedKeyUsage=serverAuth -subjectAltName = @alt_names - - -[ alt_names ] - -DNS.1 = localhost -DNS.2 = *.localhost diff --git a/docker/setup/openssl.cnf b/docker/setup/openssl.cnf deleted file mode 100644 index a25e990..0000000 --- a/docker/setup/openssl.cnf +++ /dev/null @@ -1,353 +0,0 @@ -# -# OpenSSL example configuration file. -# This is mostly being used for generation of certificate requests. -# - -# This definition stops the following lines choking if HOME isn't -# defined. -HOME = . -RANDFILE = $ENV::HOME/.rnd - -# Extra OBJECT IDENTIFIER info: -#oid_file = $ENV::HOME/.oid -oid_section = new_oids - -# To use this configuration file with the "-extfile" option of the -# "openssl x509" utility, name here the section containing the -# X.509v3 extensions to use: -# extensions = -# (Alternatively, use a configuration file that has only -# X.509v3 extensions in its main [= default] section.) - -[ new_oids ] - -# We can add new OIDs in here for use by 'ca', 'req' and 'ts'. -# Add a simple OID like this: -# testoid1=1.2.3.4 -# Or use config file substitution like this: -# testoid2=${testoid1}.5.6 - -# Policies used by the TSA examples. -tsa_policy1 = 1.2.3.4.1 -tsa_policy2 = 1.2.3.4.5.6 -tsa_policy3 = 1.2.3.4.5.7 - -#################################################################### -[ ca ] -default_ca = CA_default # The default ca section - -#################################################################### -[ CA_default ] - -dir = ./demoCA # Where everything is kept -certs = $dir/certs # Where the issued certs are kept -crl_dir = $dir/crl # Where the issued crl are kept -database = $dir/index.txt # database index file. -#unique_subject = no # Set to 'no' to allow creation of - # several certs with same subject. -new_certs_dir = $dir/newcerts # default place for new certs. - -certificate = $dir/cacert.pem # The CA certificate -serial = $dir/serial # The current serial number -crlnumber = $dir/crlnumber # the current crl number - # must be commented out to leave a V1 CRL -crl = $dir/crl.pem # The current CRL -private_key = $dir/private/cakey.pem# The private key -RANDFILE = $dir/private/.rand # private random number file - -x509_extensions = usr_cert # The extensions to add to the cert - -# Comment out the following two lines for the "traditional" -# (and highly broken) format. -name_opt = ca_default # Subject Name options -cert_opt = ca_default # Certificate field options - -# Extension copying option: use with caution. -# copy_extensions = copy - -# Extensions to add to a CRL. Note: Netscape communicator chokes on V2 CRLs -# so this is commented out by default to leave a V1 CRL. -# crlnumber must also be commented out to leave a V1 CRL. -# crl_extensions = crl_ext - -default_days = 365 # how long to certify for -default_crl_days= 30 # how long before next CRL -default_md = default # use public key default MD -preserve = no # keep passed DN ordering - -# A few difference way of specifying how similar the request should look -# For type CA, the listed attributes must be the same, and the optional -# and supplied fields are just that :-) -policy = policy_match - -# For the CA policy -[ policy_match ] -countryName = match -stateOrProvinceName = match -organizationName = match -organizationalUnitName = optional -commonName = supplied -emailAddress = optional - -# For the 'anything' policy -# At this point in time, you must list all acceptable 'object' -# types. -[ policy_anything ] -countryName = optional -stateOrProvinceName = optional -localityName = optional -organizationName = optional -organizationalUnitName = optional -commonName = supplied -emailAddress = optional - -#################################################################### -[ req ] -default_bits = 2048 -default_keyfile = privkey.pem -distinguished_name = req_distinguished_name -attributes = req_attributes -x509_extensions = v3_ca # The extensions to add to the self signed cert - -# Passwords for private keys if not present they will be prompted for -# input_password = secret -# output_password = secret - -# This sets a mask for permitted string types. There are several options. -# default: PrintableString, T61String, BMPString. -# pkix : PrintableString, BMPString (PKIX recommendation before 2004) -# utf8only: only UTF8Strings (PKIX recommendation after 2004). -# nombstr : PrintableString, T61String (no BMPStrings or UTF8Strings). -# MASK:XXXX a literal mask value. -# WARNING: ancient versions of Netscape crash on BMPStrings or UTF8Strings. -string_mask = utf8only - -req_extensions = v3_req # The extensions to add to a certificate request - -[ req_distinguished_name ] -countryName = Country Name (2 letter code) -countryName_default = AU -countryName_min = 2 -countryName_max = 2 - -stateOrProvinceName = State or Province Name (full name) -stateOrProvinceName_default = Some-State - -localityName = Locality Name (eg, city) - -0.organizationName = Organization Name (eg, company) -0.organizationName_default = Internet Widgits Pty Ltd - -# we can do this but it is not needed normally :-) -#1.organizationName = Second Organization Name (eg, company) -#1.organizationName_default = World Wide Web Pty Ltd - -organizationalUnitName = Organizational Unit Name (eg, section) -#organizationalUnitName_default = - -commonName = Common Name (e.g. server FQDN or YOUR name) -commonName_max = 64 - -emailAddress = Email Address -emailAddress_max = 64 - -# SET-ex3 = SET extension number 3 - -[ req_attributes ] -challengePassword = A challenge password -challengePassword_min = 4 -challengePassword_max = 20 - -unstructuredName = An optional company name - -[ usr_cert ] - -# These extensions are added when 'ca' signs a request. - -# This goes against PKIX guidelines but some CAs do it and some software -# requires this to avoid interpreting an end user certificate as a CA. - -basicConstraints=CA:FALSE - -# Here are some examples of the usage of nsCertType. If it is omitted -# the certificate can be used for anything *except* object signing. - -# This is OK for an SSL server. -# nsCertType = server - -# For an object signing certificate this would be used. -# nsCertType = objsign - -# For normal client use this is typical -# nsCertType = client, email - -# and for everything including object signing: -# nsCertType = client, email, objsign - -# This is typical in keyUsage for a client certificate. -# keyUsage = nonRepudiation, digitalSignature, keyEncipherment - -# This will be displayed in Netscape's comment listbox. -nsComment = "OpenSSL Generated Certificate" - -# PKIX recommendations harmless if included in all certificates. -subjectKeyIdentifier=hash -authorityKeyIdentifier=keyid,issuer - -# This stuff is for subjectAltName and issuerAltname. -# Import the email address. -# subjectAltName=email:copy -# An alternative to produce certificates that aren't -# deprecated according to PKIX. -# subjectAltName=email:move - -# Copy subject details -# issuerAltName=issuer:copy - -#nsCaRevocationUrl = http://www.domain.dom/ca-crl.pem -#nsBaseUrl -#nsRevocationUrl -#nsRenewalUrl -#nsCaPolicyUrl -#nsSslServerName - -# This is required for TSA certificates. -# extendedKeyUsage = critical,timeStamping - -[ v3_req ] - -# Extensions to add to a certificate request - -basicConstraints = CA:FALSE -keyUsage = nonRepudiation, digitalSignature, keyEncipherment - -#extendedKeyUsage=serverAuth -#subjectAltName = @alt_names - - -[ v3_ca ] - - -# Extensions for a typical CA - - -# PKIX recommendation. - -subjectKeyIdentifier=hash - -authorityKeyIdentifier=keyid:always,issuer - -#basicConstraints = critical,CA:true -basicConstraints = critical, CA:TRUE, pathlen:3 - - -# Key usage: this is typical for a CA certificate. However since it will -# prevent it being used as an test self-signed certificate it is best -# left out by default. -# keyUsage = cRLSign, keyCertSign -keyUsage = critical, cRLSign, keyCertSign - -# Some might want this also -nsCertType = sslCA, emailCA - -# Include email address in subject alt name: another PKIX recommendation -# subjectAltName=email:copy -# Copy issuer details -# issuerAltName=issuer:copy - -# DER hex encoding of an extension: beware experts only! -# obj=DER:02:03 -# Where 'obj' is a standard or added object -# You can even override a supported extension: -# basicConstraints= critical, DER:30:03:01:01:FF - -[ crl_ext ] - -# CRL extensions. -# Only issuerAltName and authorityKeyIdentifier make any sense in a CRL. - -# issuerAltName=issuer:copy -authorityKeyIdentifier=keyid:always - -[ proxy_cert_ext ] -# These extensions should be added when creating a proxy certificate - -# This goes against PKIX guidelines but some CAs do it and some software -# requires this to avoid interpreting an end user certificate as a CA. - -basicConstraints=CA:FALSE - -# Here are some examples of the usage of nsCertType. If it is omitted -# the certificate can be used for anything *except* object signing. - -# This is OK for an SSL server. -# nsCertType = server - -# For an object signing certificate this would be used. -# nsCertType = objsign - -# For normal client use this is typical -# nsCertType = client, email - -# and for everything including object signing: -# nsCertType = client, email, objsign - -# This is typical in keyUsage for a client certificate. -# keyUsage = nonRepudiation, digitalSignature, keyEncipherment - -# This will be displayed in Netscape's comment listbox. -nsComment = "OpenSSL Generated Certificate" - -# PKIX recommendations harmless if included in all certificates. -subjectKeyIdentifier=hash -authorityKeyIdentifier=keyid,issuer - -# This stuff is for subjectAltName and issuerAltname. -# Import the email address. -# subjectAltName=email:copy -# An alternative to produce certificates that aren't -# deprecated according to PKIX. -# subjectAltName=email:move - -# Copy subject details -# issuerAltName=issuer:copy - -#nsCaRevocationUrl = http://www.domain.dom/ca-crl.pem -#nsBaseUrl -#nsRevocationUrl -#nsRenewalUrl -#nsCaPolicyUrl -#nsSslServerName - -# This really needs to be in place for it to be a proxy certificate. -proxyCertInfo=critical,language:id-ppl-anyLanguage,pathlen:3,policy:foo - -#################################################################### -[ tsa ] - -default_tsa = tsa_config1 # the default TSA section - -[ tsa_config1 ] - -# These are used by the TSA reply generation only. -dir = ./demoCA # TSA root directory -serial = $dir/tsaserial # The current serial number (mandatory) -crypto_device = builtin # OpenSSL engine to use for signing -signer_cert = $dir/tsacert.pem # The TSA signing certificate - # (optional) -certs = $dir/cacert.pem # Certificate chain to include in reply - # (optional) -signer_key = $dir/private/tsakey.pem # The TSA private key (optional) -signer_digest = sha256 # Signing digest to use. (Optional) -default_policy = tsa_policy1 # Policy if request did not specify it - # (optional) -other_policies = tsa_policy2, tsa_policy3 # acceptable policies (optional) -digests = sha1, sha256, sha384, sha512 # Acceptable message digests (mandatory) -accuracy = secs:1, millisecs:500, microsecs:100 # (optional) -clock_precision_digits = 0 # number of digits after dot. (optional) -ordering = yes # Is ordering defined for timestamps? - # (optional, default: no) -tsa_name = yes # Must the TSA name be included in the reply? - # (optional, default: no) -ess_cert_id_chain = no # Must the ESS cert id chain be included? - # (optional, default: no) diff --git a/docker/setup/setup-root-certificate.sh b/docker/setup/setup-root-certificate.sh deleted file mode 100755 index db1a30c..0000000 --- a/docker/setup/setup-root-certificate.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/bin/bash - -set -eu - -### -# inspired https://fabianlee.org/2018/02/17/ubuntu-creating-a-trusted-ca-and-san-certificate-using-openssl-on-ubuntu/ -### - - -ROOT_CA_KEY="${1?Missing Name for root certificate KEY file}" -ROOT_CA_PEM="${2?Missing Name for root certificate PEM file}" -ROOT_CA_NAME="${3?Missing Certificate Name}" -CONFIG_FILE="${4:-openssl.cnf}" - -if [ ! -f ROOT_CA_PEM ] - then - printf "\n>>> CREATING A ROOT CERTIFICATE <<<\n" - - openssl req \ - -new \ - -newkey rsa:4096 \ - -days 3650 \ - -nodes \ - -x509 \ - -extensions v3_ca \ - -subj "/C=US/ST=CA/L=SF/O=${ROOT_CA_NAME}/CN=${ROOT_CA_NAME}" \ - -keyout ${ROOT_CA_KEY} \ - -out ${ROOT_CA_PEM} \ - -config ${CONFIG_FILE} - - printf "\n>>> ADDING ROOT CERTIFICATE TO THE TRUSTED STORE <<<\n" - - # add certificate to the trust store - cp ${ROOT_CA_PEM} /usr/local/share/ca-certificates/self-signed-root-ca.crt - update-ca-certificates - - # verifies the certificate - openssl x509 -in ${ROOT_CA_PEM} -text -noout > "${ROOT_CA_NAME}.txt" - - printf "\n >>> ROOT CERTICATE CREATED SUCCESEFULY<<<\n" - - else - printf "\n >>> ROOT CERTICATE ALREADY EXISTS <<<\n" -fi diff --git a/docs/APPROOV_TOKEN_BINDING_QUICKSTART.md b/docs/APPROOV_TOKEN_BINDING_QUICKSTART.md deleted file mode 100644 index 3564287..0000000 --- a/docs/APPROOV_TOKEN_BINDING_QUICKSTART.md +++ /dev/null @@ -1,313 +0,0 @@ -# Approov Token Binding Quickstart - -This quickstart is for developers familiar with Java who are looking for a quick intro into how they can add [Approov](https://approov.io) into an existing project. Therefore this will guide you through the necessary steps for adding Approov with token binding to an existing Java Spring API server. - -## TOC - Table of Contents - -* [Why?](#why) -* [How it Works?](#how-it-works) -* [Requirements](#requirements) -* [Approov Setup](#approov-setup) -* [Approov Token Check](#approov-token-binding-check) -* [Try the Approov Integration Example](#try-the-approov-integration-example) - - -## Why? - -To lock down your API server to your mobile app. Please read the brief summary in the [Approov Overview](/OVERVIEW.md#why) at the root of this repo or visit our [website](https://approov.io/product) for more details. - -[TOC](#toc---table-of-contents) - - -## How it works? - -For more background, see the [Approov Overview](/OVERVIEW.md#how-it-works) at the root of this repo. - -The main functionality for the Approov token check is in the file [ApproovAuthentication.java](/servers/hello/src/approov-protected-server/token-binding-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovAuthentication.java). Take a look at the `verifyApproovToken()` function to see the simple code for the check. - -The Approov token binding check can be found in the file [ApproovTokenBindingAuthentication.java](/servers/hello/src/approov-protected-server/token-binding-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovTokenBindingAuthentication.java). Take a look at the `verifyApproovTokenBinding()` function to see the simple code for the check. - -[TOC](#toc---table-of-contents) - - -## Requirements - -To complete this quickstart you will need both Java, Java Spring, and the Approov CLI tool installed. - -* [OpenJDK](https://openjdk.java.net/install/) -* [Java Spring](https://docs.spring.io/spring-boot/docs/current/reference/html/getting-started.html#getting-started.installing) -* [Approov CLI](https://approov.io/docs/latest/approov-installation/#approov-tool) - Learn how to use it [here](https://approov.io/docs/latest/approov-cli-tool-reference/) - -[TOC](#toc---table-of-contents) - - -## Approov Setup - -To use Approov with the Java Spring API server we need a small amount of configuration. First, Approov needs to know the API domain that will be protected. Second, the Java Spring API server needs the Approov Base64 encoded secret that will be used to verify the tokens generated by the Approov cloud service. - -### Configure API Domain - -Approov needs to know the domain name of the API for which it will issue tokens. - -Add it with: - -```bash -approov api -add your.api.domain.com -``` - -> **NOTE:** By default a symmetric key (HS256) is used to sign the Approov token on a valid attestation of the mobile app for each API domain it's added with the Approov CLI, so that all APIs will share the same secret and the backend needs to take care to keep this secret secure. -> -> A more secure alternative is to use asymmetric keys (RS256 or others) that allows for a different keyset to be used on each API domain and for the Approov token to be verified with a public key that can only verify, but not sign, Approov tokens. -> -> To implement the asymmetric key you need to change from using the symmetric HS256 algorithm to an asymmetric algorithm, for example RS256, that requires you to first [add a new key](https://approov.io/docs/latest/approov-usage-documentation/#adding-a-new-key), and then specify it when [adding each API domain](https://approov.io/docs/latest/approov-usage-documentation/#keyset-key-api-addition). Please visit [Managing Key Sets](https://approov.io/docs/latest/approov-usage-documentation/#managing-key-sets) on the Approov documentation for more details. - -Adding the API domain also configures the [dynamic certificate pinning](https://approov.io/docs/latest/approov-usage-documentation/#dynamic-pinning) setup, out of the box. - -> **NOTE:** By default the pin is extracted from the public key of the leaf certificate served by the domain, as visible to the box issuing the Approov CLI command and the Approov servers. - -### Approov Secret - -Approov tokens are signed with a symmetric secret. To verify tokens, we need to grab the secret using the [Approov secret command](https://approov.io/docs/latest/approov-cli-tool-reference/#secret-command) and plug it into the Java Spring API server environment to check the signatures of the [Approov Tokens](https://www.approov.io/docs/latest/approov-usage-documentation/#approov-tokens) that it processes. - -First, enable your Approov `admin` role with: - -```bash -eval `approov role admin` -```` - -For the Windows powershell: - -```bash -set APPROOV_ROLE=admin:___YOUR_APPROOV_ACCOUNT_NAME_HERE___ -```` - -Next, retrieve the Approov secret with: - -```bash -approov secret -get base64 -``` - -#### Set the Approov Secret - -Open the `.env` file and add the Approov secret to the var: - -```text -APPROOV_BASE64_SECRET=approov_base64_secret_here -``` - -[TOC](#toc---table-of-contents) - - -## Approov Token Check - -To check the Approov token we will use the [jwtk/jjwt](https://github.com/jwtk/jjwt) package. - -Add to your `build.gradle` dependencies: - -```gradle -dependencies { - - // omitted.. - - implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.2' - runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2', - 'io.jsonwebtoken:jjwt-jackson:0.11.2' -} -``` - -### The Approov Package - -From this repo add the package `com.criticalblue.approov.jwt.authentication` to your current project by copying the entire [authentication](/servers/hello/src/approov-protected-server/token-binding-check/src/main/java/com/criticalblue/approov/jwt/authentication) folder into your project. - -Next, use it from the class in your project that extends the `WebSecurityConfigurerAdapter`. For example: - -```java -package com.yourcompany.projectname; - -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 - // @IMPORTANT Approov token check must be at Order 1. Any other type of - // Authentication (User, API Key, etc.) for the request should go - // after this one with @Order(2). - @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() - // @APPROOV The Approov Token check is triggered here. - .authenticationProvider(new ApproovAuthenticationProvider(approovConfig)) - .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); - - http - .securityContext() - // @APPROOV The Approov Token check is configured here. - .securityContextRepository(new ApproovSecurityContextRepository(approovConfig)) - .and() - .exceptionHandling() - // @APPROOV The Approov Token check is done here - .authenticationEntryPoint(new ApproovAuthenticationEntryPoint()) - .and() - // @APPROOV This matcher will require the Approov token for - // all API endpoints. - .antMatcher("/") - .authorizeRequests() - .antMatchers(HttpMethod.GET, "/**").authenticated(); - } - } -} -``` - -> **NOTE:** When the Approov token validation fails we return a `401` with an empty body, because we don't want to give clues to an attacker about the reason the request failed, and you can go even further by returning a `400`. - -A full working example for a simple Hello World server can be found at [/servers/hello/src/approov-protected-server/token-binding-check](/servers/hello/src/approov-protected-server/token-binding-check). - -To see the code difference between the token check and the token binding check on the Hello servers use the git diff command from the root of this repo: - -```bash -git diff --no-index ./servers/hello/src/approov-protected-server/token-check/src/main/java/com/criticalblue/approov/jwt/ ./servers/hello/src/approov-protected-server/token-binding-check/src/main/java/com/criticalblue/approov/jwt/ -``` - -[TOC](#toc---table-of-contents) - - -## Test your Approov Integration - -The following examples below use cURL, but you can also use the [Postman Collection](/README.md#testing-with-postman) to make the API requests. Just remember that you need to adjust the urls and tokens defined in the collection to match your deployment. Alternatively, the above README also contains instructions for using the preset _dummy_ secret to test your Approov integration. - -#### With Valid Approov Tokens - -Generate a valid token example from the Approov Cloud service: - -``` -approov token -setDataHashInToken 'Bearer authorizationtoken' -genExample your.api.domain.com -``` - -Then make the request with the generated token: - -```text -curl -i --request GET 'https://your.api.domain.com/v1/shapes' \ - --header 'Authorization: Bearer authorizationtoken' \ - --header 'Approov-Token: APPROOV_TOKEN_EXAMPLE_HERE' -``` - -The request should be accepted. For example: - -```text -HTTP/1.1 200 OK - -... - -{"message": "Hello, World!"} -``` - -#### With Invalid Approov Tokens - -##### No Authorization Token - -Let's just remove the Authorization header from the request: - -```text -curl -i --request GET 'https://your.api.domain.com/v1/shapes' \ - --header 'Approov-Token: APPROOV_TOKEN_EXAMPLE_HERE' -``` - -The above request should fail with an Unauthorized error. For example: - -```text -HTTP/1.1 401 Unauthorized - -... - -{} -``` - -##### Same Approov Token with a Different Authorization Token - -Make the request with the same generated token, but with another random authorization token: - -``` -curl -i --request GET 'https://your.api.domain.com/v1/shapes' \ - --header 'Authorization: Bearer anotherauthorizationtoken' \ - --header 'Approov-Token: APPROOV_TOKEN_EXAMPLE_HERE' -``` - -The above request should also fail with an Unauthorized error. For example: - -```text -HTTP/1.1 401 Unauthorized - -... - -{} -``` - -[TOC](#toc---table-of-contents) - - -## Issues - -If you find any issue while following our instructions then just report it [here](https://github.com/approov/quickstart-java-spring-token-check/issues), with the steps to reproduce it, and we will sort it out and/or guide you to the correct path. - -[TOC](#toc---table-of-contents) - - -## Useful Links - -If you wish to explore the Approov solution in more depth, then why not try one of the following links as a jumping off point: - -* [Approov Free Trial](https://approov.io/signup)(no credit card needed) -* [Approov Get Started](https://approov.io/product/demo) -* [Approov QuickStarts](https://approov.io/docs/latest/approov-integration-examples/) -* [Approov Docs](https://approov.io/docs) -* [Approov Blog](https://approov.io/blog/) -* [Approov Resources](https://approov.io/resource/) -* [Approov Customer Stories](https://approov.io/customer) -* [Approov Support](https://approov.io/contact) -* [About Us](https://approov.io/company) -* [Contact Us](https://approov.io/contact) - -[TOC](#toc---table-of-contents) diff --git a/docs/APPROOV_TOKEN_QUICKSTART.md b/docs/APPROOV_TOKEN_QUICKSTART.md deleted file mode 100644 index 79eb163..0000000 --- a/docs/APPROOV_TOKEN_QUICKSTART.md +++ /dev/null @@ -1,297 +0,0 @@ -# Approov Token Quickstart - -This quickstart is for developers familiar with Java who are looking for a quick intro into how they can add [Approov](https://approov.io) into an existing project. Therefore this will guide you through the necessary steps for adding Approov to an existing Java Spring API server. - -## TOC - Table of Contents - -* [Why?](#why) -* [How it Works?](#how-it-works) -* [Requirements](#requirements) -* [Approov Setup](#approov-setup) -* [Approov Token Check](#approov-token-check) -* [Try the Approov Integration Example](#try-the-approov-integration-example) - - -## Why? - -To lock down your API server to your mobile app. Please read the brief summary in the [Approov Overview](/OVERVIEW.md#why) at the root of this repo or visit our [website](https://approov.io/product) for more details. - -[TOC](#toc---table-of-contents) - - -## How it works? - -For more background, see the [Approov Overview](/OVERVIEW.md#how-it-works) at the root of this repo. - -The main functionality for the Approov token check is in the file [ApproovAuthentication.java](/servers/hello/src/approov-protected-server/token-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovAuthentication.java). Take a look at the `verifyApproovToken()` function to see the simple code for the check. - -[TOC](#toc---table-of-contents) - - -## Requirements - -To complete this quickstart you will need both Java, Java Spring, and the Approov CLI tool installed. - -* [OpenJDK](https://openjdk.java.net/install/) -* [Java Spring](https://docs.spring.io/spring-boot/docs/current/reference/html/getting-started.html#getting-started.installing) -* [Approov CLI](https://approov.io/docs/latest/approov-installation/#approov-tool) - Learn how to use it [here](https://approov.io/docs/latest/approov-cli-tool-reference/) - -[TOC](#toc---table-of-contents) - - -## Approov Setup - -To use Approov with the Java Spring API server we need a small amount of configuration. First, Approov needs to know the API domain that will be protected. Second, the Java Spring API server needs the Approov Base64 encoded secret that will be used to verify the tokens generated by the Approov cloud service. - -### Configure API Domain - -Approov needs to know the domain name of the API for which it will issue tokens. - -Add it with: - -```bash -approov api -add your.api.domain.com -``` - -> **NOTE:** By default a symmetric key (HS256) is used to sign the Approov token on a valid attestation of the mobile app for each API domain it's added with the Approov CLI, so that all APIs will share the same secret and the backend needs to take care to keep this secret secure. -> -> A more secure alternative is to use asymmetric keys (RS256 or others) that allows for a different keyset to be used on each API domain and for the Approov token to be verified with a public key that can only verify, but not sign, Approov tokens. -> -> To implement the asymmetric key you need to change from using the symmetric HS256 algorithm to an asymmetric algorithm, for example RS256, that requires you to first [add a new key](https://approov.io/docs/latest/approov-usage-documentation/#adding-a-new-key), and then specify it when [adding each API domain](https://approov.io/docs/latest/approov-usage-documentation/#keyset-key-api-addition). Please visit [Managing Key Sets](https://approov.io/docs/latest/approov-usage-documentation/#managing-key-sets) on the Approov documentation for more details. - -Adding the API domain also configures the [dynamic certificate pinning](https://approov.io/docs/latest/approov-usage-documentation/#dynamic-pinning) setup, out of the box. - -> **NOTE:** By default the pin is extracted from the public key of the leaf certificate served by the domain, as visible to the box issuing the Approov CLI command and the Approov servers. - -### Approov Secret - -Approov tokens are signed with a symmetric secret. To verify tokens, we need to grab the secret using the [Approov secret command](https://approov.io/docs/latest/approov-cli-tool-reference/#secret-command) and plug it into the Java Spring API server environment to check the signatures of the [Approov Tokens](https://www.approov.io/docs/latest/approov-usage-documentation/#approov-tokens) that it processes. - -First, enable your Approov `admin` role with: - -```bash -eval `approov role admin` -```` - -For the Windows powershell: - -```bash -set APPROOV_ROLE=admin:___YOUR_APPROOV_ACCOUNT_NAME_HERE___ -```` - -Next, retrieve the Approov secret with: - -```bash -approov secret -get base64 -``` - -#### Set the Approov Secret - -Open the `.env` file and add the Approov secret to the var: - -```text -APPROOV_BASE64_SECRET=approov_base64_secret_here -``` - -[TOC](#toc---table-of-contents) - - -## Approov Token Check - -To perform the token check in your project you will need to add the Approov and Jwt packages. - -### The JWT Package - -To check the Approov token we will use the [jwtk/jjwt](https://github.com/jwtk/jjwt) package. - -Add to your `build.gradle` dependencies: - -```gradle -dependencies { - - // omitted.. - - implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.2' - runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2', - 'io.jsonwebtoken:jjwt-jackson:0.11.2' -} -``` - -### The Approov Package - -From this repo add the package `com.criticalblue.approov.jwt.authentication` to your current project by copying the entire [authentication](/servers/hello/src/approov-protected-server/token-check/src/main/java/com/criticalblue/approov/jwt/authentication) folder into your project. - -Next, use it from the class in your project that extends the `WebSecurityConfigurerAdapter`. For example: - -```java -package com.yourcompany.projectname; - -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 - // @IMPORTANT Approov token check must be at Order 1. Any other type of - // Authentication (User, API Key, etc.) for the request should go - // after with @Order(2) - @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() - // @APPROOV The Approov Token check is triggered here. - .authenticationProvider(new ApproovAuthenticationProvider(approovConfig)) - .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); - - http - .securityContext() - // @APPROOV The Approov Token check is configured here. - .securityContextRepository(new ApproovSecurityContextRepository(approovConfig)) - .and() - .exceptionHandling() - // @APPROOV The Approov Token check is done here - .authenticationEntryPoint(new ApproovAuthenticationEntryPoint()) - .and() - // @APPROOV This matcher will require the Approov token for - // all API endpoints. - .antMatcher("/") - .authorizeRequests() - .antMatchers(HttpMethod.GET, "/**").authenticated(); - } - } -} -``` - -> **NOTE:** When the Approov token validation fails we return a `401` with an empty body, because we don't want to give clues to an attacker about the reason the request failed, and you can go even further by returning a `400`. - -A full working example for a simple Hello World server can be found at [/servers/hello/src/approov-protected-server/token-check](/servers/hello/src/approov-protected-server/token-check). - -To see the code difference between the token check and no token check on the Hello servers use the git diff command from the root of this repo: - -```bash -git diff --no-index ./servers/hello/src/unprotected-server/src/main/java/com/criticalblue/approov/jwt/ ./servers/hello/src/approov-protected-server/token-check/src/main/java/com/criticalblue/approov/jwt/ -``` - -[TOC](#toc---table-of-contents) - - -## Test your Approov Integration - -The following examples below use cURL, but you can also use the [Postman Collection](/README.md#testing-with-postman) to make the API requests. Just remember that you need to adjust the urls and tokens defined in the collection to match your deployment. Alternatively, the above README also contains instructions for using the preset _dummy_ secret to test your Approov integration. - -#### With Valid Approov Tokens - -Generate a valid token example from the Approov Cloud service: - -```text -approov token -genExample your.api.domain.com -``` - -Then make the request with the generated token: - -```text -curl -i --request GET 'https://your.api.domain.com' \ - --header 'Approov-Token: APPROOV_TOKEN_EXAMPLE_HERE' -``` - -The request should be accepted. For example: - -```text -HTTP/1.1 200 OK - -... - -{"message": "Hello, World!"} -``` - -#### With Invalid Approov Tokens - -Generate an invalid token example from the Approov Cloud service: - -```text -approov token -type invalid -genExample your.api.domain.com -``` - -Then make the request with the generated token: - -```text -curl -i --request GET 'https://your.api.domain.com' \ - --header 'Approov-Token: APPROOV_INVALID_TOKEN_EXAMPLE_HERE' -``` - -The above request should fail with an Unauthorized error. For example: - -```text -HTTP/1.1 401 Unauthorized - -... -{} -``` - -[TOC](#toc---table-of-contents) - - -## Issues - -If you find any issue while following our instructions then just report it [here](https://github.com/approov/quickstart-java-spring-token-check/issues), with the steps to reproduce it, and we will sort it out and/or guide you to the correct path. - -[TOC](#toc---table-of-contents) - - -## Useful Links - -If you wish to explore the Approov solution in more depth, then why not try one of the following links as a jumping off point: - -* [Approov Free Trial](https://approov.io/signup)(no credit card needed) -* [Approov Get Started](https://approov.io/product/demo) -* [Approov QuickStarts](https://approov.io/docs/latest/approov-integration-examples/) -* [Approov Docs](https://approov.io/docs) -* [Approov Blog](https://approov.io/blog/) -* [Approov Resources](https://approov.io/resource/) -* [Approov Customer Stories](https://approov.io/customer) -* [Approov Support](https://approov.io/contact) -* [About Us](https://approov.io/company) -* [Contact Us](https://approov.io/contact) - -[TOC](#toc---table-of-contents) diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..29008c5 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.welcome=never +org.gradle.daemon=false +org.gradle.logging.level=quiet \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..afba109 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/servers/hello/src/approov-protected-server/token-binding-check/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties similarity index 80% rename from servers/hello/src/approov-protected-server/token-binding-check/gradle/wrapper/gradle-wrapper.properties rename to gradle/wrapper/gradle-wrapper.properties index 6c58388..20db9ad 100644 --- a/servers/hello/src/approov-protected-server/token-binding-check/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Tue Apr 02 13:15:36 BST 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-all.zip diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..9cce075 --- /dev/null +++ b/gradlew @@ -0,0 +1,243 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/servers/hello/src/approov-protected-server/token-binding-check/gradlew.bat b/gradlew.bat similarity index 56% rename from servers/hello/src/approov-protected-server/token-binding-check/gradlew.bat rename to gradlew.bat index 0f8d593..48ce07d 100644 --- a/servers/hello/src/approov-protected-server/token-binding-check/gradlew.bat +++ b/gradlew.bat @@ -1,4 +1,20 @@ -@if "%DEBUG%" == "" @echo off +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -9,19 +25,23 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -35,7 +55,7 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% @@ -45,38 +65,25 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/run_server.sh b/run_server.sh new file mode 100755 index 0000000..5a64feb --- /dev/null +++ b/run_server.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# Host-side wrapper: sets FOLLOW_LOGS (default true) and hands execution to scripts/build.sh, +# which handles image build/run plus container log tailing. +set -euo pipefail + +FOLLOW_LOGS="${FOLLOW_LOGS:-true}" ./scripts/build.sh diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..4525491 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# Dual-purpose orchestrator: on the host it builds/runs the Docker container, +# and inside the container it starts the application command with optional +# readiness checks and log attachment. +set -euo pipefail + +requirement_check() { # verify command exists on PATH + local cmd="$1" + if ! command -v "$cmd" >/dev/null 2>&1; then + fail "Missing required command: ${cmd}" + fi +} +fail() { echo "ERROR: $*" >&2; exit 1; } # uniform error output + exit +info() { echo "info $*"; } # lightweight logging helper + +# Globals configured via environment overrides +RUN_MODE="${RUN_MODE:-host}" # host orchestrator vs container entrypoint +APP_START_CMD="${APP_START_CMD:-}" # command executed when inside container +FOLLOW_LOGS="${FOLLOW_LOGS:-true}" # toggle docker logs -f attachment +HOST_PORT="${HOST_PORT:-8080}" # host-facing port (e.g., http://localhost:3000) +WAIT_URL="${WAIT_URL:-http://localhost:${HOST_PORT}/approov-state}" # readiness probe target +WAIT_TIMEOUT="${WAIT_TIMEOUT:-60}" # how long to wait before failing readiness +WAIT_INTERVAL="${WAIT_INTERVAL:-2}" # delay between readiness checks +CONTAINER_PORT="${CONTAINER_PORT:-$HOST_PORT}" # container listener, defaults to host port +IMAGE_NAME="${IMAGE_NAME:-approov-quickstart-java-spring}" +CONTAINER_NAME="${CONTAINER_NAME:-approov-quickstart-java-spring-app}" +ENV_FILE="${ENV_FILE:-.env}" +RUNTIME_BIN_DIR="${RUNTIME_BIN_DIR:-}" # optional runtime-specific bin path + +in_container() { + [[ "$RUN_MODE" == "container" ]] || [[ -f "/.dockerenv" ]] +} + +if in_container; then + [[ -n "$APP_START_CMD" ]] || fail "APP_START_CMD must be provided to run the server" + if [[ -n "$RUNTIME_BIN_DIR" ]]; then + export PATH="${RUNTIME_BIN_DIR}:$PATH" # e.g., RUNTIME_BIN_DIR=/usr/local/go/bin to expose runtime binaries for golang + fi + info "Container starting application: ${APP_START_CMD}" + exec bash -c "$APP_START_CMD" +fi + +requirement_check docker +if ! command -v approov >/dev/null 2>&1; then + info "Approov CLI not found; continuing without CLI checks (tests may need it)" +fi + +[[ -f "$ENV_FILE" ]] || fail "$ENV_FILE not found. Run cp .env.example .env first." +[[ -f Dockerfile ]] || fail "Dockerfile not found in $(pwd)" + +if docker ps -a --format '{{.Names}}' | grep -Fxq "$CONTAINER_NAME"; then + info "Removing stale container ${CONTAINER_NAME}" + docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true +fi + +info "Building ${IMAGE_NAME}" +docker build -t "$IMAGE_NAME" . || fail "Docker build failed" + +info "Starting ${CONTAINER_NAME} on host port ${HOST_PORT}, container port ${CONTAINER_PORT}" +docker run -d \ + --name "$CONTAINER_NAME" \ + --env-file "$ENV_FILE" \ + -e RUN_MODE=container \ + -p "${HOST_PORT}:${CONTAINER_PORT}" \ + "$IMAGE_NAME" >/dev/null || fail "Failed to start container ${CONTAINER_NAME}" + +wait_for_service() { + local url="$1" timeout="$2" interval="$3" elapsed=0 + info "Waiting for application to become ready at ${url}" + until curl -fsS "$url" >/dev/null 2>&1; do + sleep "$interval" + elapsed=$((elapsed + interval)) + if (( elapsed >= timeout )); then + fail "Application did not become ready within ${timeout}s (last url: ${url})" + fi + done + info "Application is ready" +} + +wait_for_service "$WAIT_URL" "$WAIT_TIMEOUT" "$WAIT_INTERVAL" + +if [[ "$FOLLOW_LOGS" == "true" ]]; then + info "Container logs (Ctrl+C to stop):" + docker logs -f "$CONTAINER_NAME" +else + info "Skipping container logs attachment." +fi diff --git a/scripts/install-prerequisites.sh b/scripts/install-prerequisites.sh new file mode 100755 index 0000000..96b4fd4 --- /dev/null +++ b/scripts/install-prerequisites.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# Installs base OS dependencies (and optionally language runtimes) required for +# the quickstart image; intended for use inside the Docker build context. +set -euo pipefail + +requirement_check() { # helper to verify command availability + local cmd="$1" + if ! command -v "$cmd" >/dev/null 2>&1; then + fail "Missing required command: ${cmd}" + fi +} +fail() { echo "ERROR: $*" >&2; exit 1; } # helper to abort with message +info() { echo "info $*"; } # helper to print informational logs + +# ensure apt operations have privileges +[[ "$(id -u)" -eq 0 ]] || fail "Run this script as root (or via sudo) inside the build context." + +# base utilities required by every quickstart +apt-get update +apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + build-essential \ + git + +# Optional runtime install; set INSTALL_LANGUAGE_RUNTIME=true per quickstart and fill commands below. +INSTALL_LANGUAGE_RUNTIME_FLAG="${INSTALL_LANGUAGE_RUNTIME:-false}" + +if [[ "$INSTALL_LANGUAGE_RUNTIME_FLAG" == "true" ]]; then + info "Installing language/runtime specific dependencies" + # TODO: add language-specific install commands (apt packages, curl tarballs, etc.) +fi diff --git a/servers/hello/src/approov-protected-server/token-binding-check/.env.example b/servers/hello/src/approov-protected-server/token-binding-check/.env.example deleted file mode 100644 index bc74c47..0000000 --- a/servers/hello/src/approov-protected-server/token-binding-check/.env.example +++ /dev/null @@ -1,22 +0,0 @@ -########### -# SERVER -########### - -HTTP_REDIRECT=false -HTTP_PORT=8002 -HTTPS_PORT=8003 - - -############ -# APPROOV -############ - -APPROOV_TOKEN_BINDING_HEADER_NAME=Authorization - -# Feel free to play with different secrets. For development only you can create them with: -# $ openssl rand -base64 64 | tr -d '\n'; echo -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 diff --git a/servers/hello/src/approov-protected-server/token-binding-check/README.md b/servers/hello/src/approov-protected-server/token-binding-check/README.md deleted file mode 100644 index 6cbab41..0000000 --- a/servers/hello/src/approov-protected-server/token-binding-check/README.md +++ /dev/null @@ -1,119 +0,0 @@ -# Approov Token Binding Integration Example - -This Approov integration example is from where the code example for the [Approov token binding check quickstart](/docs/APPROOV_TOKEN_BINDING_QUICKSTART.md) is extracted, and you can use it as a playground to better understand how simple and easy it is to implement [Approov](https://approov.io) in a Java Spring API server. - -## TOC - Table of Contents - -* [Why?](#why) -* [How it Works?](#how-it-works) -* [Requirements](#requirements) -* [Try the Approov Integration Example](#try-the-approov-integration-example) - - -## Why? - -To lock down your API server to your mobile app. Please read the brief summary in the [Approov Overview](/OVERVIEW.md#why) at the root of this repo or visit our [website](https://approov.io/product) for more details. - -[TOC](#toc---table-of-contents) - - -## How it works? - -The Java Spring API server is very simple and only replies to the endpoint `/` with the message: - -```json -{"message": "Hello, World!"} -``` - -You can find the endpoint definition [here](./src/main/java/com/criticalblue/approov/jwt). - -Take a look at the [`verifyApproovToken()`](./src/main/java/com/criticalblue/approov/jwt/authentication/ApproovAuthentication.java) function to see the simple code for the check, and check out the [`verifyApproovTokenBinding()`](./src/main/java/com/criticalblue/approov/jwt/authentication/ApproovTokenBindingAuthentication.java) function to see how the Approov token binding is verified. - -For more background on Approov, see the [Approov Overview](/OVERVIEW.md#how-it-works) at the root of this repo. - -[TOC](#toc---table-of-contents) - - -## Requirements - -To run this example you will need to have installed: - -* [OpenJDK](https://openjdk.java.net/install/) - This server example uses version `11.0.3`. It should work with earlier or later versions but was not tested. -* [Java Spring](https://docs.spring.io/spring-boot/docs/current/reference/html/getting-started.html#getting-started.installing) - Version `2.6.4` of the Spring Framework plugin is being used. The code should work with prior versions but wasn't tested. - -[TOC](#toc---table-of-contents) - - -## Try the Approov Integration Example - -First, you need to set the dummy secret in the `/servers/hello/src/approov-protected-server/token-binding-check/.env` file as explained [here](/TESTING.md#the-dummy-secret). - -Second, you need to build the server with gradle. From the `./servers/hello/src/approov-protected-server/token-binding-check` folder execute: - -```bash -./gradlew build -``` - -Now, you can run this example from the `/servers/hello/src/approov-protected-server/token-binding-check` folder with: - -```bash -source .env && ./gradlew bootRun -``` - -Next, you can test that it works with: - -```text -curl -iX GET 'http://localhost:8002' -``` - -The response will be a `400` bad request: - -```text -HTTP/1.1 400 -Vary: Origin -Vary: Access-Control-Request-Method -Vary: Access-Control-Request-Headers -X-Content-Type-Options: nosniff -X-XSS-Protection: 1; mode=block -Cache-Control: no-cache, no-store, max-age=0, must-revalidate -Pragma: no-cache -Expires: 0 -X-Frame-Options: DENY -Content-Type: application/json -Transfer-Encoding: chunked -Date: Fri, 11 Mar 2022 19:59:11 GMT -Connection: close - -{} -``` - -The reason you got a `400` is because no Approoov token isn't provided in the headers of the request. - -Finally, you can test that the Approov integration example works as expected with this [Postman collection](/README.md#testing-with-postman) or with some more cURL requests [examples](/README.md#testing-with-curl). - -[TOC](#toc---table-of-contents) - - -## Issues - -If you find any issue while following our instructions then just report it [here](https://github.com/approov/quickstart-java-spring-token-check/issues), with the steps to reproduce it, and we will sort it out and/or guide you to the correct path. - -[TOC](#toc---table-of-contents) - - -## Useful Links - -If you wish to explore the Approov solution in more depth, then why not try one of the following links as a jumping off point: - -* [Approov Free Trial](https://approov.io/signup)(no credit card needed) -* [Approov Get Started](https://approov.io/product/demo) -* [Approov QuickStarts](https://approov.io/docs/latest/approov-integration-examples/) -* [Approov Docs](https://approov.io/docs) -* [Approov Blog](https://approov.io/blog/) -* [Approov Resources](https://approov.io/resource/) -* [Approov Customer Stories](https://approov.io/customer) -* [Approov Support](https://approov.io/contact) -* [About Us](https://approov.io/company) -* [Contact Us](https://approov.io/contact) - -[TOC](#toc---table-of-contents) diff --git a/servers/hello/src/approov-protected-server/token-binding-check/build.gradle b/servers/hello/src/approov-protected-server/token-binding-check/build.gradle deleted file mode 100644 index f0d415d..0000000 --- a/servers/hello/src/approov-protected-server/token-binding-check/build.gradle +++ /dev/null @@ -1,37 +0,0 @@ -plugins { - id 'org.springframework.boot' version '2.6.4' - id 'java' -} - -apply plugin: 'io.spring.dependency-management' - -group = 'com.criticalblue' -version = '0.0.1-SNAPSHOT' -sourceCompatibility = '1.8' - -repositories { - mavenCentral() -} - -dependencies { - implementation 'org.springframework.boot:spring-boot-starter-integration' - implementation 'org.springframework.boot:spring-boot-starter-security' - implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.springframework.security:spring-security-core' - implementation 'org.springframework.security:spring-security-web' - implementation 'org.springframework.security:spring-security-config' - - implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.2' - runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2', - // Uncomment the next line if you want to use RSASSA-PSS (PS256, PS384, PS512) algorithms: - //'org.bouncycastle:bcprov-jdk15on:1.60', - 'io.jsonwebtoken:jjwt-jackson:0.11.2' - - compileOnly 'org.jetbrains:annotations:17.0.0' - - compileOnly 'javax.servlet:servlet-api:3.1.0' - - runtimeOnly 'org.springframework.boot:spring-boot-devtools' - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'org.springframework.security:spring-security-test' -} diff --git a/servers/hello/src/approov-protected-server/token-binding-check/gradlew b/servers/hello/src/approov-protected-server/token-binding-check/gradlew deleted file mode 100755 index af6708f..0000000 --- a/servers/hello/src/approov-protected-server/token-binding-check/gradlew +++ /dev/null @@ -1,172 +0,0 @@ -#!/usr/bin/env sh - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m"' - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn () { - echo "$*" -} - -die () { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=$((i+1)) - done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=$(save "$@") - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" - -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi - -exec "$JAVACMD" "$@" diff --git a/servers/hello/src/approov-protected-server/token-binding-check/src/main/java/com/criticalblue/approov/jwt/ApiController.java b/servers/hello/src/approov-protected-server/token-binding-check/src/main/java/com/criticalblue/approov/jwt/ApiController.java deleted file mode 100644 index 9e231e0..0000000 --- a/servers/hello/src/approov-protected-server/token-binding-check/src/main/java/com/criticalblue/approov/jwt/ApiController.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.criticalblue.approov.jwt; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; -import javax.servlet.http.HttpServletRequest; -import java.util.LinkedHashMap; -import java.util.Map; - -@RestController -public class ApiController { - - private static Logger logger = LoggerFactory.getLogger(ApiController.class); - - @GetMapping("/") - public Map helloV1() { - - logger.info("Serving request for endpoint '/', that is protect by an Approov Token."); - - Map response = new LinkedHashMap<>(); - - response.put("message", "Hello, World!"); - - return response; - } -} diff --git a/servers/hello/src/approov-protected-server/token-binding-check/src/main/java/com/criticalblue/approov/jwt/Application.java b/servers/hello/src/approov-protected-server/token-binding-check/src/main/java/com/criticalblue/approov/jwt/Application.java deleted file mode 100644 index c547b68..0000000 --- a/servers/hello/src/approov-protected-server/token-binding-check/src/main/java/com/criticalblue/approov/jwt/Application.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.criticalblue.approov.jwt; - -import org.apache.catalina.Context; -import org.apache.catalina.connector.Connector; -import org.apache.tomcat.util.descriptor.web.SecurityCollection; -import org.apache.tomcat.util.descriptor.web.SecurityConstraint; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; -import org.springframework.boot.web.servlet.server.ServletWebServerFactory; -import org.springframework.context.annotation.Bean; - -@SpringBootApplication -public class Application { - - private static Logger logger = LoggerFactory.getLogger(Application.class); - - @Value("${http.port}") - private int httpPort; - - @Value("${https.port}") - private int httpsPort; - - @Value("${http.redirect}") - private boolean isToRedirectHttp; - - public static void main(String[] args) { - SpringApplication.run(Application.class, args); - } - - @Bean - public ServletWebServerFactory servletContainer() { - - TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory() { - - @Override - protected void postProcessContext(Context context) { - if (isToRedirectHttp) { - logger.info("Creating security constrain to redirect http to https."); - SecurityConstraint securityConstraint = new SecurityConstraint(); - securityConstraint.setUserConstraint("CONFIDENTIAL"); - SecurityCollection collection = new SecurityCollection(); - collection.addPattern("/*"); - securityConstraint.addCollection(collection); - context.addConstraint(securityConstraint); - } - } - }; - - tomcat.addAdditionalTomcatConnectors(createConnector()); - return tomcat; - } - - private Connector createConnector() { - Connector connector = new Connector(TomcatServletWebServerFactory.DEFAULT_PROTOCOL); - - connector.setScheme("http"); - connector.setPort(httpPort); - connector.setSecure(false); - - if (isToRedirectHttp) { - logger.info("Redirecting http to port: {}", httpsPort); - connector.setRedirectPort(httpsPort); - } - - return connector; - } -} diff --git a/servers/hello/src/approov-protected-server/token-binding-check/src/main/java/com/criticalblue/approov/jwt/CustomServletErrorAttributes.java b/servers/hello/src/approov-protected-server/token-binding-check/src/main/java/com/criticalblue/approov/jwt/CustomServletErrorAttributes.java deleted file mode 100644 index 3abac84..0000000 --- a/servers/hello/src/approov-protected-server/token-binding-check/src/main/java/com/criticalblue/approov/jwt/CustomServletErrorAttributes.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.criticalblue.approov.jwt; - -import org.springframework.boot.web.servlet.error.DefaultErrorAttributes; -import org.springframework.boot.web.error.ErrorAttributeOptions; -import org.springframework.stereotype.Component; -import org.springframework.web.context.request.WebRequest; -import java.util.Map; - -@Component -public class CustomServletErrorAttributes extends DefaultErrorAttributes { - - @Override - public Map getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) { - - Map errorAttributes = super.getErrorAttributes(webRequest, options); - - // Remove from response in order to make the response comply with the Hello API specification - errorAttributes.remove("timestamp"); - errorAttributes.remove("message"); - errorAttributes.remove("path"); - errorAttributes.remove("error"); - errorAttributes.remove("trace"); - errorAttributes.remove("status"); - - return errorAttributes; - } -} diff --git a/servers/hello/src/approov-protected-server/token-binding-check/src/main/java/com/criticalblue/approov/jwt/WebSecurityConfig.java b/servers/hello/src/approov-protected-server/token-binding-check/src/main/java/com/criticalblue/approov/jwt/WebSecurityConfig.java deleted file mode 100644 index 8ef6bcb..0000000 --- a/servers/hello/src/approov-protected-server/token-binding-check/src/main/java/com/criticalblue/approov/jwt/WebSecurityConfig.java +++ /dev/null @@ -1,72 +0,0 @@ -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 - // @IMPORTANT Approov token check must be at Order 1. Any other type of - // Authentication (User, API Key, etc.) for the request should go - // after this one with @Order(2). - @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)) - .and() - .exceptionHandling() - .authenticationEntryPoint(new ApproovAuthenticationEntryPoint()) - .and() - .antMatcher("/") - .authorizeRequests() - .antMatchers(HttpMethod.GET, "/**").authenticated(); - } - } -} diff --git a/servers/hello/src/approov-protected-server/token-binding-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovAuthentication.java b/servers/hello/src/approov-protected-server/token-binding-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovAuthentication.java deleted file mode 100644 index 80b5208..0000000 --- a/servers/hello/src/approov-protected-server/token-binding-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovAuthentication.java +++ /dev/null @@ -1,137 +0,0 @@ -package com.criticalblue.approov.jwt.authentication; - -import java.util.Collection; -import java.util.Collections; - -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.JwtException; -import io.jsonwebtoken.Jwts; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.springframework.http.HttpStatus; -import org.springframework.security.core.GrantedAuthority; - - -/** - * Validates the Approov Token is signed with the shared secret between Approov and the API server, that have not - * expired, and optionally also validates the token binding in the Approov token matches the token binding header. - * - * @see ApproovAuthenticationProvider - * @see ApproovSecurityContextRepository - */ -public class ApproovAuthentication implements ApproovJwtAuthentication { - - private static Logger logger = LoggerFactory.getLogger(ApproovAuthentication.class); - - private final ApproovTokenBindingAuthentication approovPayload = new ApproovTokenBindingAuthentication(); - - private final ApproovConfig approovConfig; - - private Claims approovTokenPayloadClaims; - - private final String tokenBindingHeader; - - private String approovToken; - - private boolean isAuthenticated = false; - - private boolean validTokenBinding; - - /** - * Constructs the Approov Authentication instance that will validate the Approov token and the token binding. - * - * @param approovConfig Extracted from the .env file in the root of the package. - * @param approovToken Extracted from the header `Approov-Token`. - * @param tokenBindingHeader Extracted by default from the request header `Authorization`. - */ - ApproovAuthentication(ApproovConfig approovConfig, String approovToken, String tokenBindingHeader) { - this.approovConfig = approovConfig; - this.approovToken = approovToken; - this.tokenBindingHeader = tokenBindingHeader; - } - - @Override - public void verifyApproovToken(byte[] approovSecret) throws ApproovAuthenticationException { - - if (approovSecret == null) { - throw new ApproovAuthenticationException("The Approov secret is null.", HttpStatus.INTERNAL_SERVER_ERROR.value()); - } - - if (approovToken == null) { - throw new ApproovAuthenticationException("The Approov token is null.", HttpStatus.FORBIDDEN.value()); - } - - approovToken = approovToken.trim(); - - if (approovToken.equals("")) { - throw new ApproovAuthenticationException("The Approov token is empty.", HttpStatus.BAD_REQUEST.value()); - } - - try { - - approovTokenPayloadClaims = Jwts.parser() - .setSigningKey(approovSecret) - .parseClaimsJws(approovToken) - .getBody(); - - logger.info("Request approved with a valid Approov token."); - - } catch (JwtException e) { - String message = "Request with an invalid Approov token: " + e.getMessage(); - throw new ApproovAuthenticationException(message, HttpStatus.UNAUTHORIZED.value()); - } - - validTokenBinding = approovPayload.checkClaimMatchesFor(tokenBindingHeader, approovTokenPayloadClaims, approovConfig); - - isAuthenticated = true; - } - - @Override - public Claims getApproovTokenPayloadClaims() { - return approovTokenPayloadClaims; - } - - @Override - public boolean isValidTokenBinding() { - return validTokenBinding; - } - - @Override - public Collection getAuthorities() { - return Collections.emptyList(); - } - - @Override - public Object getCredentials() { - return approovToken; - } - - @Override - public Object getDetails() { - return approovTokenPayloadClaims; - } - - @Override - public Object getPrincipal() { - return null; - } - - @Override - public boolean isAuthenticated() { - return isAuthenticated; - } - - @Override - public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { - if (isAuthenticated) { - throw new ApproovAuthenticationException("A new Approov Authentication instance needs to be created to set this.isAuthenticated.", HttpStatus.INTERNAL_SERVER_ERROR.value()); - } - } - - @Override - public String getName() { - return null; - } -} diff --git a/servers/hello/src/approov-protected-server/token-binding-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovAuthenticationEntryPoint.java b/servers/hello/src/approov-protected-server/token-binding-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovAuthenticationEntryPoint.java deleted file mode 100644 index cf4bacd..0000000 --- a/servers/hello/src/approov-protected-server/token-binding-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovAuthenticationEntryPoint.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.criticalblue.approov.jwt.authentication; - -import java.io.IOException; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.springframework.http.HttpStatus; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.web.AuthenticationEntryPoint; - - -/** - * When a failure occurs during the Approov token authentication process, an exception is thrown and Spring redirects - * to an authentication entry point, that have been configured in the Sring security to be this one. - * - * @see com.criticalblue.approov.jwt.WebSecurityConfig - * @see ApproovAuthentication - * @see ApproovTokenBindingAuthentication - */ -public class ApproovAuthenticationEntryPoint implements AuthenticationEntryPoint { - - private final static Logger logger = LoggerFactory.getLogger(ApproovAuthenticationEntryPoint.class); - - @Override - public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { - int httpStatusCode = HttpStatus.BAD_REQUEST.value(); - - if (authException instanceof ApproovException) { - httpStatusCode = ((ApproovException) authException).getHttpStatusCode(); - } - - final String httpStatusMessage = String.valueOf(HttpStatus.valueOf(httpStatusCode)); - final String exceptionType = String.valueOf(authException.getClass()); - final String exceptionMessage = authException.getMessage(); - - logger.error(httpStatusMessage + " | " + exceptionType + " | " + exceptionMessage + " | Stacktrace origin: " + authException.getStackTrace()[0].toString()); - response.sendError(httpStatusCode); - } -} diff --git a/servers/hello/src/approov-protected-server/token-binding-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovAuthenticationException.java b/servers/hello/src/approov-protected-server/token-binding-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovAuthenticationException.java deleted file mode 100644 index 1cc01b5..0000000 --- a/servers/hello/src/approov-protected-server/token-binding-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovAuthenticationException.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.criticalblue.approov.jwt.authentication; - -import org.springframework.http.HttpStatus; -import org.springframework.security.core.AuthenticationException; -import org.springframework.web.bind.annotation.ResponseStatus; - -/** - * Custom exception for failures when verifying the Approov token signature and - * expiration time. - * - * @see ApproovAuthentication - */ -class ApproovAuthenticationException extends AuthenticationException implements ApproovException { - - private final int httpStatusCode; - - public ApproovAuthenticationException(String msg, int httpStatusCode) { - super(msg); - this.httpStatusCode = httpStatusCode; - } - - public int getHttpStatusCode() { - return this.httpStatusCode; - } -} diff --git a/servers/hello/src/approov-protected-server/token-binding-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovAuthenticationProvider.java b/servers/hello/src/approov-protected-server/token-binding-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovAuthenticationProvider.java deleted file mode 100644 index 1dc739f..0000000 --- a/servers/hello/src/approov-protected-server/token-binding-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovAuthenticationProvider.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.criticalblue.approov.jwt.authentication; - -import org.apache.tomcat.util.codec.binary.Base64; - -import org.jetbrains.annotations.NotNull; - -import org.springframework.security.authentication.AuthenticationProvider; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; - -/** - * Used to configure the Spring framework security with the trigger for the Approov Authentication. - * - * @see com.criticalblue.approov.jwt.WebSecurityConfig - * @see ApproovAuthentication - */ -public class ApproovAuthenticationProvider implements AuthenticationProvider { - - private final byte[] approovSecret; - - /** - * Constructs the Approov Authentication provider with an instance of the Approov config. - * - * @param approovConfig Extracted from the .env file in the root of the package. - */ - public ApproovAuthenticationProvider(ApproovConfig approovConfig) { - this.approovSecret = Base64.decodeBase64(approovConfig.getApproovBase64Secret()); - } - - @Override - public boolean supports(Class authentication) { - return ApproovJwtAuthentication.class.isAssignableFrom(authentication); - } - - @Override - public Authentication authenticate(@NotNull Authentication authentication) throws AuthenticationException { - - if (!supports(authentication.getClass())) { - return null; - } - - ApproovJwtAuthentication approovTokenAuthentication = (ApproovJwtAuthentication) authentication; - - approovTokenAuthentication.verifyApproovToken(approovSecret); - - return approovTokenAuthentication; - } -} diff --git a/servers/hello/src/approov-protected-server/token-binding-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovConfig.java b/servers/hello/src/approov-protected-server/token-binding-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovConfig.java deleted file mode 100644 index 3937fa9..0000000 --- a/servers/hello/src/approov-protected-server/token-binding-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovConfig.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.criticalblue.approov.jwt.authentication; - -import org.springframework.http.HttpStatus; - -/** - * The Approov configuration that is built from the .env file in the root of the package. - */ -final public class ApproovConfig { - - private static ApproovConfig ourInstance = new ApproovConfig(); - - private String approovHeaderName = "Approov-Token"; - - private String approovBase64Secret; - - private final String approovTokenBindingHeaderName; - - /** - * Constructs the Approov Config singleton with values retrieved from the .env file in the root of the project. - */ - private ApproovConfig() { - this.approovBase64Secret = retrieveApproovBase64Secret(); - this.approovTokenBindingHeaderName = retrieveStringValueFromEnv("APPROOV_TOKEN_BINDING_HEADER_NAME", "Authorization"); - } - - public static ApproovConfig getInstance() { - return ourInstance; - } - - String getApproovHeaderName() { - return approovHeaderName; - } - - String getApproovTokenBindingHeaderName() { - return approovTokenBindingHeaderName; - } - - String getApproovBase64Secret() { - return approovBase64Secret; - } - - private String retrieveApproovBase64Secret() { - approovBase64Secret = System.getenv("APPROOV_BASE64_SECRET"); - - if (approovBase64Secret == null) { - throw new ApproovAuthenticationException("Cannot retrieve APPROOV_BASE64_SECRET from the environment.", HttpStatus.INTERNAL_SERVER_ERROR.value()); - } - - return approovBase64Secret; - } - - private String retrieveStringValueFromEnv(String key, String defaultValue) { - - String value = System.getenv(key); - - if (value == null) { - return defaultValue; - } - - return value.trim(); - } - - private boolean retrieveBooleanValueFromEnv(String key, boolean defaultValue) { - - String value = System.getenv(key); - - if (value == null) { - return defaultValue; - } - - return value.trim().equalsIgnoreCase("true"); - } -} diff --git a/servers/hello/src/approov-protected-server/token-binding-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovException.java b/servers/hello/src/approov-protected-server/token-binding-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovException.java deleted file mode 100644 index 60b7576..0000000 --- a/servers/hello/src/approov-protected-server/token-binding-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovException.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.criticalblue.approov.jwt.authentication; - -/** - * The Interface to be used in the Approov exceptions. - */ -public interface ApproovException { - - public int getHttpStatusCode(); -} diff --git a/servers/hello/src/approov-protected-server/token-binding-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovJwtAuthentication.java b/servers/hello/src/approov-protected-server/token-binding-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovJwtAuthentication.java deleted file mode 100644 index c527f03..0000000 --- a/servers/hello/src/approov-protected-server/token-binding-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovJwtAuthentication.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.criticalblue.approov.jwt.authentication; - -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.JwtException; - -import org.springframework.security.core.Authentication; - -/** - * The Interface to be used in the Approov authentication. - * - * @see ApproovAuthentication - */ -public interface ApproovJwtAuthentication extends Authentication { - - boolean isValidTokenBinding(); - - Claims getApproovTokenPayloadClaims(); - - void verifyApproovToken(byte[] secret) throws JwtException, ApproovAuthenticationException; -} diff --git a/servers/hello/src/approov-protected-server/token-binding-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovSecurityContextRepository.java b/servers/hello/src/approov-protected-server/token-binding-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovSecurityContextRepository.java deleted file mode 100644 index 18d3c62..0000000 --- a/servers/hello/src/approov-protected-server/token-binding-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovSecurityContextRepository.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.criticalblue.approov.jwt.authentication; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContext; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.web.context.HttpRequestResponseHolder; -import org.springframework.security.web.context.SecurityContextRepository; - -/** - * Used to setup the Approov Authentication Context when configuring the Spring framework security. - * - * @see com.criticalblue.approov.jwt.WebSecurityConfig - */ -public class ApproovSecurityContextRepository implements SecurityContextRepository { - - private String approovToken = null; - - final private ApproovConfig approovConfig; - - /** - * Constructs with an instance of the Approov configuration, and with a boolean flag to indicate if is to check the - * token binding in the Approov token. - * - * @param approovConfig Extracted from the .env file in the root of the project. - */ - public ApproovSecurityContextRepository(ApproovConfig approovConfig) { - this.approovConfig = approovConfig; - } - - @Override - public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) { - - String tokenBindingHeader = null; - - HttpServletRequest request = requestResponseHolder.getRequest(); - - SecurityContext context = SecurityContextHolder.createEmptyContext(); - - approovToken = request.getHeader(approovConfig.getApproovHeaderName()); - - if (approovToken == null) { - // returning an empty security context in an endpoint protected by - // Approov, will cause Spring to later throw this exception: - // org.springframework.security.access.AccessDeniedException: Access is denied - return context; - } - - tokenBindingHeader = getTokenBindingHeader(request); - - Authentication approovAuthentication = new ApproovAuthentication(approovConfig, approovToken, tokenBindingHeader); - context.setAuthentication(approovAuthentication); - - return context; - } - - private String getTokenBindingHeader(HttpServletRequest request) { - - final String headerName = approovConfig.getApproovTokenBindingHeaderName(); - - if (headerName == null) { - return null; - } - - final String tokenBindingHeader = request.getHeader(headerName); - - if (tokenBindingHeader == null) { - return null; - } - - return tokenBindingHeader.trim(); - } - - @Override - public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) { - } - - @Override - public boolean containsContext(HttpServletRequest request) { - return approovToken != null; - } -} diff --git a/servers/hello/src/approov-protected-server/token-binding-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovTokenBindingAuthentication.java b/servers/hello/src/approov-protected-server/token-binding-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovTokenBindingAuthentication.java deleted file mode 100644 index 186e3f5..0000000 --- a/servers/hello/src/approov-protected-server/token-binding-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovTokenBindingAuthentication.java +++ /dev/null @@ -1,81 +0,0 @@ -package com.criticalblue.approov.jwt.authentication; - -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; - -import io.jsonwebtoken.Claims; - -import org.apache.tomcat.util.codec.binary.Base64; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.http.HttpStatus; -import org.springframework.security.authentication.AuthenticationServiceException; - -public class ApproovTokenBindingAuthentication { - - private static Logger logger = LoggerFactory.getLogger(ApproovAuthentication.class); - - /** - * Checks the value in the key `pay` of an Approov token matches the token binding header, that by default is - * the value for the `Authorization` header. - * - * @param tokenBindingHeader Extracted from an header, that by default is the Authorization header. - * @param approovTokenPayloadClaims Extracted from the already verified Approov token. - * @param approovConfig Extracted from the .env file in the root of the package. - * @return - */ - boolean checkClaimMatchesFor(String tokenBindingHeader, Claims approovTokenPayloadClaims, ApproovConfig approovConfig) { - - if (tokenBindingHeader == null) { - throw new ApproovTokenBindingAuthenticationException("The token binding header value is null.", HttpStatus.BAD_REQUEST.value()); - } - - final String approovTokenBindingClaim = getApproovTokenBindingClaim(approovTokenPayloadClaims, approovConfig); - - boolean isValidTokenBinding = getHashBase64Encoded(tokenBindingHeader).equals(approovTokenBindingClaim); - - if (isValidTokenBinding) { - logger.info("Request approved with a valid token binding in the Approov token."); - return isValidTokenBinding; - } - - // When the token binding header does not match the value in key `pay` - // of the Approov token, the request is aborted. - throw new ApproovTokenBindingAuthenticationException("The token binding header does not match the key `pay` in the Approov token.", HttpStatus.UNAUTHORIZED.value()); - } - - private String getApproovTokenBindingClaim(Claims approovTokenPayloadClaims, ApproovConfig approovConfig) { - - if (approovTokenPayloadClaims == null) { - throw new ApproovTokenBindingAuthenticationException("Approov token payload is null.", HttpStatus.INTERNAL_SERVER_ERROR.value()); - } - - if ( ! approovTokenPayloadClaims.containsKey("pay") ) { - throw new ApproovTokenBindingAuthenticationException("The key `pay`, for the token binding, is missing in the Approov token payload.", HttpStatus.BAD_REQUEST.value()); - } - - final String approovTokenBindingClaim = approovTokenPayloadClaims.get("pay").toString(); - - if (approovTokenBindingClaim == null || approovTokenBindingClaim.trim().equals("")) { - throw new ApproovTokenBindingAuthenticationException("The token binding in the Approov token is null or empty.", HttpStatus.BAD_REQUEST.value()); - } - - return approovTokenBindingClaim; - } - - private String getHashBase64Encoded(String value) { - - final MessageDigest digest; - - try { - digest = MessageDigest.getInstance("SHA-256"); - } catch (NoSuchAlgorithmException e) { - throw new AuthenticationServiceException(e.getMessage()); - } - - byte[] hash = digest.digest(value.getBytes(StandardCharsets.UTF_8)); - return Base64.encodeBase64String(hash); - } -} diff --git a/servers/hello/src/approov-protected-server/token-binding-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovTokenBindingAuthenticationException.java b/servers/hello/src/approov-protected-server/token-binding-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovTokenBindingAuthenticationException.java deleted file mode 100644 index 563da70..0000000 --- a/servers/hello/src/approov-protected-server/token-binding-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovTokenBindingAuthenticationException.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.criticalblue.approov.jwt.authentication; - -import org.springframework.security.core.AuthenticationException; - -/** - * Custom exception for failures in the validation of the token binding in an Approov token. - * - * This exception is only thrown when `APPROOV_ABORT_REQUEST_ON_INVALID_TOKEN_BINDING` is set to `true` in the - * .env file at the root of the project. - * - * @see ApproovTokenBindingAuthentication - */ -class ApproovTokenBindingAuthenticationException extends AuthenticationException implements ApproovException { - - private final int httpStatusCode; - - ApproovTokenBindingAuthenticationException(String msg, int httpStatusCode) { - super(msg); - this.httpStatusCode = httpStatusCode; - } - - public int getHttpStatusCode() { - return httpStatusCode; - } -} diff --git a/servers/hello/src/approov-protected-server/token-binding-check/src/main/resources/application.properties b/servers/hello/src/approov-protected-server/token-binding-check/src/main/resources/application.properties deleted file mode 100644 index cda3b69..0000000 --- a/servers/hello/src/approov-protected-server/token-binding-check/src/main/resources/application.properties +++ /dev/null @@ -1,41 +0,0 @@ -######################### -# SPRING CONFIGURATION -######################### - -spring.mvc.throw-exception-if-no-handler-found: true -spring.resources.add-mappings: false - - -######################### -# LOGGER CONFIGURATION -######################### - -logging.level.root: ERROR -logging.level.org.hibernate: ERROR -logging.level.org.springframework.web: ERROR -logging.level.org.springframework.security: ERROR -logging.level.com.criticalblue.approov: INFO - - -####################### -# HTTP CONFIGURATION -####################### - -# This vars need to be set in the .env file or in the environment -http.port: ${HTTP_PORT} -http.redirect: ${HTTP_REDIRECT} - - -####################### -# HTTPS CONFIGURATION -####################### - -# Needs to be set in the .env file or in the environment -server.port: ${HTTPS_PORT} - -# Self signed certificate was generated with: -# keytool -genkeypair -alias ApproovTLS -keyalg RSA -keysize 2048 -storetype PKCS12 -keystore ApproovTLS.p12 -validity 100000 -server.ssl.key-store-type: PKCS12 -server.ssl.key-store: classpath:keystore/ApproovTLS.p12 -server.ssl.key-store-password: supersecret -server.ssl.key-alias: ApproovTLS diff --git a/servers/hello/src/approov-protected-server/token-binding-check/src/main/resources/keystore/ApproovTLS.p12 b/servers/hello/src/approov-protected-server/token-binding-check/src/main/resources/keystore/ApproovTLS.p12 deleted file mode 100644 index 72a2163..0000000 Binary files a/servers/hello/src/approov-protected-server/token-binding-check/src/main/resources/keystore/ApproovTLS.p12 and /dev/null differ diff --git a/servers/hello/src/approov-protected-server/token-binding-check/src/test/java/com/criticalblue/approov/jwt/ApplicationTests.java b/servers/hello/src/approov-protected-server/token-binding-check/src/test/java/com/criticalblue/approov/jwt/ApplicationTests.java deleted file mode 100644 index 0a8fc7d..0000000 --- a/servers/hello/src/approov-protected-server/token-binding-check/src/test/java/com/criticalblue/approov/jwt/ApplicationTests.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.criticalblue.approov.jwt; - -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.http.ResponseEntity; - -import static org.assertj.core.api.Assertions.assertThat; - -@SpringBootTest -public class ApplicationTests { - - @Test - public void contextLoads() { - } - -} diff --git a/servers/hello/src/approov-protected-server/token-check/.env.example b/servers/hello/src/approov-protected-server/token-check/.env.example deleted file mode 100644 index bc74c47..0000000 --- a/servers/hello/src/approov-protected-server/token-check/.env.example +++ /dev/null @@ -1,22 +0,0 @@ -########### -# SERVER -########### - -HTTP_REDIRECT=false -HTTP_PORT=8002 -HTTPS_PORT=8003 - - -############ -# APPROOV -############ - -APPROOV_TOKEN_BINDING_HEADER_NAME=Authorization - -# Feel free to play with different secrets. For development only you can create them with: -# $ openssl rand -base64 64 | tr -d '\n'; echo -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 diff --git a/servers/hello/src/approov-protected-server/token-check/README.md b/servers/hello/src/approov-protected-server/token-check/README.md deleted file mode 100644 index ee8246c..0000000 --- a/servers/hello/src/approov-protected-server/token-check/README.md +++ /dev/null @@ -1,120 +0,0 @@ -# Approov Token Integration Example - -This Approov integration example is from where the code example for the [Approov token check quickstart](/docs/APPROOV_TOKEN_QUICKSTART.md) is extracted, and you can use it as a playground to better understand how simple and easy it is to implement [Approov](https://approov.io) in a Java Spring API server. - -## TOC - Table of Contents - -* [Why?](#why) -* [How it Works?](#how-it-works) -* [Requirements](#requirements) -* [Try the Approov Integration Example](#try-the-approov-integration-example) - - -## Why? - -To lock down your API server to your mobile app. Please read the brief summary in the [Approov Overview](/OVERVIEW.md#why) at the root of this repo or visit our [website](https://approov.io/product) for more details. - -[TOC](#toc---table-of-contents) - - -## How it works? - -The Java Spring API server is very simple and only replies to the endpoint `/` with the message: - -```json -{"message": "Hello, World!"} -``` - -You can find the endpoint definition [here](./src/main/java/com/criticalblue/approov/jwt). - -Take a look at the [`verifyApproovToken()`](./src/main/java/com/criticalblue/approov/jwt/authentication/ApproovAuthentication.java) function to see the simple code for the check. - -For more background on Approov, see the [Approov Overview](/OVERVIEW.md#how-it-works) at the root of this repo. - - -[TOC](#toc---table-of-contents) - - -## Requirements - -To run this example you will need to have installed: - -* [OpenJDK](https://openjdk.java.net/install/) - This server example uses version `11.0.3`. It should work with earlier or later versions but was not tested. -* [Java Spring](https://docs.spring.io/spring-boot/docs/current/reference/html/getting-started.html#getting-started.installing) - Version `2.6.4` of the Spring Framework plugin is being used. The code should work with prior versions but wasn't tested. - -[TOC](#toc---table-of-contents) - - -## Try the Approov Integration Example - -First, you need to set the dummy secret in the `/servers/hello/src/approov-protected-server/token-check/.env` file as explained [here](/TESTING.md#the-dummy-secret). - -Second, you need to build the server with gradle. From the `./servers/hello/src/approov-protected-server/token-check` folder execute: - -```bash -./gradlew build -``` - -Now, you can run this example from the `/servers/hello/src/approov-protected-server/token-check` folder with: - -```bash -source .env && ./gradlew bootRun -``` - -Next, you can test that it works with: - -```text -curl -iX GET 'http://localhost:8002' -``` - -The response will be a `400` bad request: - -```text -HTTP/1.1 400 -Vary: Origin -Vary: Access-Control-Request-Method -Vary: Access-Control-Request-Headers -X-Content-Type-Options: nosniff -X-XSS-Protection: 1; mode=block -Cache-Control: no-cache, no-store, max-age=0, must-revalidate -Pragma: no-cache -Expires: 0 -X-Frame-Options: DENY -Content-Type: application/json -Transfer-Encoding: chunked -Date: Fri, 11 Mar 2022 19:59:11 GMT -Connection: close - -{} -``` - -The reason you got a `400` is because no Approoov token isn't provided in the headers of the request. - -Finally, you can test that the Approov integration example works as expected with this [Postman collection](/README.md#testing-with-postman) or with some more cURL requests [examples](/README.md#testing-with-curl). - -[TOC](#toc---table-of-contents) - - -## Issues - -If you find any issue while following our instructions then just report it [here](https://github.com/approov/quickstart-java-spring-token-check/issues), with the steps to reproduce it, and we will sort it out and/or guide you to the correct path. - -[TOC](#toc---table-of-contents) - - -## Useful Links - -If you wish to explore the Approov solution in more depth, then why not try one of the following links as a jumping off point: - -* [Approov Free Trial](https://approov.io/signup)(no credit card needed) -* [Approov Get Started](https://approov.io/product/demo) -* [Approov QuickStarts](https://approov.io/docs/latest/approov-integration-examples/) -* [Approov Docs](https://approov.io/docs) -* [Approov Blog](https://approov.io/blog/) -* [Approov Resources](https://approov.io/resource/) -* [Approov Customer Stories](https://approov.io/customer) -* [Approov Support](https://approov.io/contact) -* [About Us](https://approov.io/company) -* [Contact Us](https://approov.io/contact) - -[TOC](#toc---table-of-contents) diff --git a/servers/hello/src/approov-protected-server/token-check/build.gradle b/servers/hello/src/approov-protected-server/token-check/build.gradle deleted file mode 100644 index e1e5e22..0000000 --- a/servers/hello/src/approov-protected-server/token-check/build.gradle +++ /dev/null @@ -1,35 +0,0 @@ -plugins { - id 'org.springframework.boot' version '2.6.4' - id 'java' -} - -apply plugin: 'io.spring.dependency-management' - -group = 'com.criticalblue' -version = '0.0.1-SNAPSHOT' -sourceCompatibility = '1.8' - -repositories { - mavenCentral() -} - -dependencies { - implementation 'org.springframework.boot:spring-boot-starter-integration' - implementation 'org.springframework.boot:spring-boot-starter-security' - implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.springframework.security:spring-security-core' - implementation 'org.springframework.security:spring-security-web' - implementation 'org.springframework.security:spring-security-config' - - implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.2' - runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2', - 'io.jsonwebtoken:jjwt-jackson:0.11.2' - - compileOnly 'org.jetbrains:annotations:17.0.0' - - compileOnly 'javax.servlet:servlet-api:3.1.0' - - runtimeOnly 'org.springframework.boot:spring-boot-devtools' - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'org.springframework.security:spring-security-test' -} diff --git a/servers/hello/src/approov-protected-server/token-check/gradle/wrapper/gradle-wrapper.properties b/servers/hello/src/approov-protected-server/token-check/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 6c58388..0000000 --- a/servers/hello/src/approov-protected-server/token-check/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -#Tue Apr 02 13:15:36 BST 2019 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-all.zip diff --git a/servers/hello/src/approov-protected-server/token-check/gradlew b/servers/hello/src/approov-protected-server/token-check/gradlew deleted file mode 100755 index af6708f..0000000 --- a/servers/hello/src/approov-protected-server/token-check/gradlew +++ /dev/null @@ -1,172 +0,0 @@ -#!/usr/bin/env sh - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m"' - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn () { - echo "$*" -} - -die () { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=$((i+1)) - done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=$(save "$@") - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" - -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi - -exec "$JAVACMD" "$@" diff --git a/servers/hello/src/approov-protected-server/token-check/gradlew.bat b/servers/hello/src/approov-protected-server/token-check/gradlew.bat deleted file mode 100644 index 0f8d593..0000000 --- a/servers/hello/src/approov-protected-server/token-check/gradlew.bat +++ /dev/null @@ -1,84 +0,0 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/servers/hello/src/approov-protected-server/token-check/settings.gradle b/servers/hello/src/approov-protected-server/token-check/settings.gradle deleted file mode 100644 index 67a9126..0000000 --- a/servers/hello/src/approov-protected-server/token-check/settings.gradle +++ /dev/null @@ -1,6 +0,0 @@ -pluginManagement { - repositories { - gradlePluginPortal() - } -} -rootProject.name = 'approov-jwt' diff --git a/servers/hello/src/approov-protected-server/token-check/src/main/java/com/criticalblue/approov/jwt/ApiController.java b/servers/hello/src/approov-protected-server/token-check/src/main/java/com/criticalblue/approov/jwt/ApiController.java deleted file mode 100644 index 5585691..0000000 --- a/servers/hello/src/approov-protected-server/token-check/src/main/java/com/criticalblue/approov/jwt/ApiController.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.criticalblue.approov.jwt; - -import com.criticalblue.approov.jwt.authentication.ApproovAuthentication; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; -import javax.servlet.http.HttpServletRequest; -import java.util.LinkedHashMap; -import java.util.Map; - -@RestController -public class ApiController { - - private static Logger logger = LoggerFactory.getLogger(ApiController.class); - - @GetMapping("/") - public Map helloV1() { - - logger.info("Serving request for endpoint '/', that is protect by an Approov Token."); - - Map response = new LinkedHashMap<>(); - - response.put("message", "Hello, World!"); - - return response; - } -} diff --git a/servers/hello/src/approov-protected-server/token-check/src/main/java/com/criticalblue/approov/jwt/Application.java b/servers/hello/src/approov-protected-server/token-check/src/main/java/com/criticalblue/approov/jwt/Application.java deleted file mode 100644 index c547b68..0000000 --- a/servers/hello/src/approov-protected-server/token-check/src/main/java/com/criticalblue/approov/jwt/Application.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.criticalblue.approov.jwt; - -import org.apache.catalina.Context; -import org.apache.catalina.connector.Connector; -import org.apache.tomcat.util.descriptor.web.SecurityCollection; -import org.apache.tomcat.util.descriptor.web.SecurityConstraint; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; -import org.springframework.boot.web.servlet.server.ServletWebServerFactory; -import org.springframework.context.annotation.Bean; - -@SpringBootApplication -public class Application { - - private static Logger logger = LoggerFactory.getLogger(Application.class); - - @Value("${http.port}") - private int httpPort; - - @Value("${https.port}") - private int httpsPort; - - @Value("${http.redirect}") - private boolean isToRedirectHttp; - - public static void main(String[] args) { - SpringApplication.run(Application.class, args); - } - - @Bean - public ServletWebServerFactory servletContainer() { - - TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory() { - - @Override - protected void postProcessContext(Context context) { - if (isToRedirectHttp) { - logger.info("Creating security constrain to redirect http to https."); - SecurityConstraint securityConstraint = new SecurityConstraint(); - securityConstraint.setUserConstraint("CONFIDENTIAL"); - SecurityCollection collection = new SecurityCollection(); - collection.addPattern("/*"); - securityConstraint.addCollection(collection); - context.addConstraint(securityConstraint); - } - } - }; - - tomcat.addAdditionalTomcatConnectors(createConnector()); - return tomcat; - } - - private Connector createConnector() { - Connector connector = new Connector(TomcatServletWebServerFactory.DEFAULT_PROTOCOL); - - connector.setScheme("http"); - connector.setPort(httpPort); - connector.setSecure(false); - - if (isToRedirectHttp) { - logger.info("Redirecting http to port: {}", httpsPort); - connector.setRedirectPort(httpsPort); - } - - return connector; - } -} diff --git a/servers/hello/src/approov-protected-server/token-check/src/main/java/com/criticalblue/approov/jwt/CustomServletErrorAttributes.java b/servers/hello/src/approov-protected-server/token-check/src/main/java/com/criticalblue/approov/jwt/CustomServletErrorAttributes.java deleted file mode 100644 index 3abac84..0000000 --- a/servers/hello/src/approov-protected-server/token-check/src/main/java/com/criticalblue/approov/jwt/CustomServletErrorAttributes.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.criticalblue.approov.jwt; - -import org.springframework.boot.web.servlet.error.DefaultErrorAttributes; -import org.springframework.boot.web.error.ErrorAttributeOptions; -import org.springframework.stereotype.Component; -import org.springframework.web.context.request.WebRequest; -import java.util.Map; - -@Component -public class CustomServletErrorAttributes extends DefaultErrorAttributes { - - @Override - public Map getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) { - - Map errorAttributes = super.getErrorAttributes(webRequest, options); - - // Remove from response in order to make the response comply with the Hello API specification - errorAttributes.remove("timestamp"); - errorAttributes.remove("message"); - errorAttributes.remove("path"); - errorAttributes.remove("error"); - errorAttributes.remove("trace"); - errorAttributes.remove("status"); - - return errorAttributes; - } -} diff --git a/servers/hello/src/approov-protected-server/token-check/src/main/java/com/criticalblue/approov/jwt/WebSecurityConfig.java b/servers/hello/src/approov-protected-server/token-check/src/main/java/com/criticalblue/approov/jwt/WebSecurityConfig.java deleted file mode 100644 index 2315c11..0000000 --- a/servers/hello/src/approov-protected-server/token-check/src/main/java/com/criticalblue/approov/jwt/WebSecurityConfig.java +++ /dev/null @@ -1,69 +0,0 @@ -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)) - .and() - .exceptionHandling() - .authenticationEntryPoint(new ApproovAuthenticationEntryPoint()) - .and() - .antMatcher("/") - .authorizeRequests() - .antMatchers(HttpMethod.GET, "/**").authenticated(); - } - } -} diff --git a/servers/hello/src/approov-protected-server/token-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovAuthentication.java b/servers/hello/src/approov-protected-server/token-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovAuthentication.java deleted file mode 100644 index 89cc4a5..0000000 --- a/servers/hello/src/approov-protected-server/token-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovAuthentication.java +++ /dev/null @@ -1,122 +0,0 @@ -package com.criticalblue.approov.jwt.authentication; - -import java.util.Collection; -import java.util.Collections; - -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.JwtException; -import io.jsonwebtoken.Jwts; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.springframework.http.HttpStatus; -import org.springframework.security.core.GrantedAuthority; - - -/** - * Validates the Approov Token is signed with the shared secret between Approov and the API server, that have not - * expired, and optionally also validates the token binding in the Approov token matches the token binding header. - * - * @see ApproovAuthenticationProvider - * @see ApproovSecurityContextRepository - */ -public class ApproovAuthentication implements ApproovJwtAuthentication { - - private static Logger logger = LoggerFactory.getLogger(ApproovAuthentication.class); - - private final ApproovConfig approovConfig; - - private Claims approovTokenPayloadClaims; - - private String approovToken; - - private boolean isAuthenticated = false; - - - /** - * Constructs the Approov Authentication instance that will validate the Approov token and the token binding. - * - * @param approovConfig Extracted from the .env file in the root of the package. - * @param approovToken Extracted from the header `Approov-Token`. - */ - ApproovAuthentication(ApproovConfig approovConfig, String approovToken) { - this.approovConfig = approovConfig; - this.approovToken = approovToken; - } - - @Override - public void verifyApproovToken(byte[] approovSecret) throws ApproovAuthenticationException { - - if (approovSecret == null) { - throw new ApproovAuthenticationException("The Approov secret is null.", HttpStatus.INTERNAL_SERVER_ERROR.value()); - } - - if (approovToken == null) { - throw new ApproovAuthenticationException("The Approov token is null.", HttpStatus.FORBIDDEN.value()); - } - - approovToken = approovToken.trim(); - - if (approovToken.equals("")) { - throw new ApproovAuthenticationException("The Approov token is empty.", HttpStatus.BAD_REQUEST.value()); - } - - try { - approovTokenPayloadClaims = Jwts.parser() - .setSigningKey(approovSecret) - .parseClaimsJws(approovToken) - .getBody(); - - logger.info("Request approved with a valid Approov token."); - - } catch (JwtException e) { - String message = "Request with an invalid Approov token: " + e.getMessage(); - throw new ApproovAuthenticationException(message, HttpStatus.UNAUTHORIZED.value()); - } - - isAuthenticated = true; - } - - @Override - public Claims getApproovTokenPayloadClaims() { - return approovTokenPayloadClaims; - } - - @Override - public Collection getAuthorities() { - return Collections.emptyList(); - } - - @Override - public Object getCredentials() { - return approovToken; - } - - @Override - public Object getDetails() { - return approovTokenPayloadClaims; - } - - @Override - public Object getPrincipal() { - return null; - } - - @Override - public boolean isAuthenticated() { - return isAuthenticated; - } - - @Override - public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { - if (isAuthenticated) { - throw new ApproovAuthenticationException("A new Approov Authentication instance needs to be created to set this.isAuthenticated.", HttpStatus.INTERNAL_SERVER_ERROR.value()); - } - } - - @Override - public String getName() { - return null; - } -} diff --git a/servers/hello/src/approov-protected-server/token-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovAuthenticationEntryPoint.java b/servers/hello/src/approov-protected-server/token-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovAuthenticationEntryPoint.java deleted file mode 100644 index cf4bacd..0000000 --- a/servers/hello/src/approov-protected-server/token-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovAuthenticationEntryPoint.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.criticalblue.approov.jwt.authentication; - -import java.io.IOException; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.springframework.http.HttpStatus; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.web.AuthenticationEntryPoint; - - -/** - * When a failure occurs during the Approov token authentication process, an exception is thrown and Spring redirects - * to an authentication entry point, that have been configured in the Sring security to be this one. - * - * @see com.criticalblue.approov.jwt.WebSecurityConfig - * @see ApproovAuthentication - * @see ApproovTokenBindingAuthentication - */ -public class ApproovAuthenticationEntryPoint implements AuthenticationEntryPoint { - - private final static Logger logger = LoggerFactory.getLogger(ApproovAuthenticationEntryPoint.class); - - @Override - public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { - int httpStatusCode = HttpStatus.BAD_REQUEST.value(); - - if (authException instanceof ApproovException) { - httpStatusCode = ((ApproovException) authException).getHttpStatusCode(); - } - - final String httpStatusMessage = String.valueOf(HttpStatus.valueOf(httpStatusCode)); - final String exceptionType = String.valueOf(authException.getClass()); - final String exceptionMessage = authException.getMessage(); - - logger.error(httpStatusMessage + " | " + exceptionType + " | " + exceptionMessage + " | Stacktrace origin: " + authException.getStackTrace()[0].toString()); - response.sendError(httpStatusCode); - } -} diff --git a/servers/hello/src/approov-protected-server/token-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovAuthenticationException.java b/servers/hello/src/approov-protected-server/token-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovAuthenticationException.java deleted file mode 100644 index 1cc01b5..0000000 --- a/servers/hello/src/approov-protected-server/token-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovAuthenticationException.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.criticalblue.approov.jwt.authentication; - -import org.springframework.http.HttpStatus; -import org.springframework.security.core.AuthenticationException; -import org.springframework.web.bind.annotation.ResponseStatus; - -/** - * Custom exception for failures when verifying the Approov token signature and - * expiration time. - * - * @see ApproovAuthentication - */ -class ApproovAuthenticationException extends AuthenticationException implements ApproovException { - - private final int httpStatusCode; - - public ApproovAuthenticationException(String msg, int httpStatusCode) { - super(msg); - this.httpStatusCode = httpStatusCode; - } - - public int getHttpStatusCode() { - return this.httpStatusCode; - } -} diff --git a/servers/hello/src/approov-protected-server/token-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovAuthenticationProvider.java b/servers/hello/src/approov-protected-server/token-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovAuthenticationProvider.java deleted file mode 100644 index 1dc739f..0000000 --- a/servers/hello/src/approov-protected-server/token-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovAuthenticationProvider.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.criticalblue.approov.jwt.authentication; - -import org.apache.tomcat.util.codec.binary.Base64; - -import org.jetbrains.annotations.NotNull; - -import org.springframework.security.authentication.AuthenticationProvider; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; - -/** - * Used to configure the Spring framework security with the trigger for the Approov Authentication. - * - * @see com.criticalblue.approov.jwt.WebSecurityConfig - * @see ApproovAuthentication - */ -public class ApproovAuthenticationProvider implements AuthenticationProvider { - - private final byte[] approovSecret; - - /** - * Constructs the Approov Authentication provider with an instance of the Approov config. - * - * @param approovConfig Extracted from the .env file in the root of the package. - */ - public ApproovAuthenticationProvider(ApproovConfig approovConfig) { - this.approovSecret = Base64.decodeBase64(approovConfig.getApproovBase64Secret()); - } - - @Override - public boolean supports(Class authentication) { - return ApproovJwtAuthentication.class.isAssignableFrom(authentication); - } - - @Override - public Authentication authenticate(@NotNull Authentication authentication) throws AuthenticationException { - - if (!supports(authentication.getClass())) { - return null; - } - - ApproovJwtAuthentication approovTokenAuthentication = (ApproovJwtAuthentication) authentication; - - approovTokenAuthentication.verifyApproovToken(approovSecret); - - return approovTokenAuthentication; - } -} diff --git a/servers/hello/src/approov-protected-server/token-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovConfig.java b/servers/hello/src/approov-protected-server/token-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovConfig.java deleted file mode 100644 index 3937fa9..0000000 --- a/servers/hello/src/approov-protected-server/token-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovConfig.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.criticalblue.approov.jwt.authentication; - -import org.springframework.http.HttpStatus; - -/** - * The Approov configuration that is built from the .env file in the root of the package. - */ -final public class ApproovConfig { - - private static ApproovConfig ourInstance = new ApproovConfig(); - - private String approovHeaderName = "Approov-Token"; - - private String approovBase64Secret; - - private final String approovTokenBindingHeaderName; - - /** - * Constructs the Approov Config singleton with values retrieved from the .env file in the root of the project. - */ - private ApproovConfig() { - this.approovBase64Secret = retrieveApproovBase64Secret(); - this.approovTokenBindingHeaderName = retrieveStringValueFromEnv("APPROOV_TOKEN_BINDING_HEADER_NAME", "Authorization"); - } - - public static ApproovConfig getInstance() { - return ourInstance; - } - - String getApproovHeaderName() { - return approovHeaderName; - } - - String getApproovTokenBindingHeaderName() { - return approovTokenBindingHeaderName; - } - - String getApproovBase64Secret() { - return approovBase64Secret; - } - - private String retrieveApproovBase64Secret() { - approovBase64Secret = System.getenv("APPROOV_BASE64_SECRET"); - - if (approovBase64Secret == null) { - throw new ApproovAuthenticationException("Cannot retrieve APPROOV_BASE64_SECRET from the environment.", HttpStatus.INTERNAL_SERVER_ERROR.value()); - } - - return approovBase64Secret; - } - - private String retrieveStringValueFromEnv(String key, String defaultValue) { - - String value = System.getenv(key); - - if (value == null) { - return defaultValue; - } - - return value.trim(); - } - - private boolean retrieveBooleanValueFromEnv(String key, boolean defaultValue) { - - String value = System.getenv(key); - - if (value == null) { - return defaultValue; - } - - return value.trim().equalsIgnoreCase("true"); - } -} diff --git a/servers/hello/src/approov-protected-server/token-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovException.java b/servers/hello/src/approov-protected-server/token-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovException.java deleted file mode 100644 index 60b7576..0000000 --- a/servers/hello/src/approov-protected-server/token-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovException.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.criticalblue.approov.jwt.authentication; - -/** - * The Interface to be used in the Approov exceptions. - */ -public interface ApproovException { - - public int getHttpStatusCode(); -} diff --git a/servers/hello/src/approov-protected-server/token-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovJwtAuthentication.java b/servers/hello/src/approov-protected-server/token-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovJwtAuthentication.java deleted file mode 100644 index 1d1a3ab..0000000 --- a/servers/hello/src/approov-protected-server/token-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovJwtAuthentication.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.criticalblue.approov.jwt.authentication; - -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.JwtException; - -import org.springframework.security.core.Authentication; - -/** - * The Interface to be used in the Approov authentication. - * - * @see ApproovAuthentication - */ -public interface ApproovJwtAuthentication extends Authentication { - - Claims getApproovTokenPayloadClaims(); - - void verifyApproovToken(byte[] secret) throws JwtException, ApproovAuthenticationException; -} diff --git a/servers/hello/src/approov-protected-server/token-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovSecurityContextRepository.java b/servers/hello/src/approov-protected-server/token-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovSecurityContextRepository.java deleted file mode 100644 index 5b4e91e..0000000 --- a/servers/hello/src/approov-protected-server/token-check/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovSecurityContextRepository.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.criticalblue.approov.jwt.authentication; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContext; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.web.context.HttpRequestResponseHolder; -import org.springframework.security.web.context.SecurityContextRepository; - -/** - * Used to setup the Approov Authentication Context when configuring the Spring framework security. - * - * @see com.criticalblue.approov.jwt.WebSecurityConfig - */ -public class ApproovSecurityContextRepository implements SecurityContextRepository { - - private String approovToken = null; - - final private ApproovConfig approovConfig; - - /** - * Constructs with an instance of the Approov configuration, and with a boolean flag to indicate if is to check the - * token binding in the Approov token. - * - * @param approovConfig Extracted from the .env file in the root of the project. - */ - public ApproovSecurityContextRepository(ApproovConfig approovConfig) { - this.approovConfig = approovConfig; - } - - @Override - public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) { - - HttpServletRequest request = requestResponseHolder.getRequest(); - - SecurityContext context = SecurityContextHolder.createEmptyContext(); - - approovToken = request.getHeader(approovConfig.getApproovHeaderName()); - - if (approovToken == null) { - // returning an empty security context in an endpoint protected by - // Approov, will cause Spring to later throw this exception: - // org.springframework.security.access.AccessDeniedException: Access is denied - return context; - } - - Authentication approovAuthentication = new ApproovAuthentication(approovConfig, approovToken); - context.setAuthentication(approovAuthentication); - - return context; - } - - @Override - public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) { - } - - @Override - public boolean containsContext(HttpServletRequest request) { - return approovToken != null; - } -} diff --git a/servers/hello/src/approov-protected-server/token-check/src/main/resources/application.properties b/servers/hello/src/approov-protected-server/token-check/src/main/resources/application.properties deleted file mode 100644 index cda3b69..0000000 --- a/servers/hello/src/approov-protected-server/token-check/src/main/resources/application.properties +++ /dev/null @@ -1,41 +0,0 @@ -######################### -# SPRING CONFIGURATION -######################### - -spring.mvc.throw-exception-if-no-handler-found: true -spring.resources.add-mappings: false - - -######################### -# LOGGER CONFIGURATION -######################### - -logging.level.root: ERROR -logging.level.org.hibernate: ERROR -logging.level.org.springframework.web: ERROR -logging.level.org.springframework.security: ERROR -logging.level.com.criticalblue.approov: INFO - - -####################### -# HTTP CONFIGURATION -####################### - -# This vars need to be set in the .env file or in the environment -http.port: ${HTTP_PORT} -http.redirect: ${HTTP_REDIRECT} - - -####################### -# HTTPS CONFIGURATION -####################### - -# Needs to be set in the .env file or in the environment -server.port: ${HTTPS_PORT} - -# Self signed certificate was generated with: -# keytool -genkeypair -alias ApproovTLS -keyalg RSA -keysize 2048 -storetype PKCS12 -keystore ApproovTLS.p12 -validity 100000 -server.ssl.key-store-type: PKCS12 -server.ssl.key-store: classpath:keystore/ApproovTLS.p12 -server.ssl.key-store-password: supersecret -server.ssl.key-alias: ApproovTLS diff --git a/servers/hello/src/approov-protected-server/token-check/src/main/resources/keystore/ApproovTLS.p12 b/servers/hello/src/approov-protected-server/token-check/src/main/resources/keystore/ApproovTLS.p12 deleted file mode 100644 index 72a2163..0000000 Binary files a/servers/hello/src/approov-protected-server/token-check/src/main/resources/keystore/ApproovTLS.p12 and /dev/null differ diff --git a/servers/hello/src/approov-protected-server/token-check/src/test/java/com/criticalblue/approov/jwt/ApplicationTests.java b/servers/hello/src/approov-protected-server/token-check/src/test/java/com/criticalblue/approov/jwt/ApplicationTests.java deleted file mode 100644 index 0a8fc7d..0000000 --- a/servers/hello/src/approov-protected-server/token-check/src/test/java/com/criticalblue/approov/jwt/ApplicationTests.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.criticalblue.approov.jwt; - -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.http.ResponseEntity; - -import static org.assertj.core.api.Assertions.assertThat; - -@SpringBootTest -public class ApplicationTests { - - @Test - public void contextLoads() { - } - -} diff --git a/servers/hello/src/unprotected-server/.env.example b/servers/hello/src/unprotected-server/.env.example deleted file mode 100644 index bc74c47..0000000 --- a/servers/hello/src/unprotected-server/.env.example +++ /dev/null @@ -1,22 +0,0 @@ -########### -# SERVER -########### - -HTTP_REDIRECT=false -HTTP_PORT=8002 -HTTPS_PORT=8003 - - -############ -# APPROOV -############ - -APPROOV_TOKEN_BINDING_HEADER_NAME=Authorization - -# Feel free to play with different secrets. For development only you can create them with: -# $ openssl rand -base64 64 | tr -d '\n'; echo -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 diff --git a/servers/hello/src/unprotected-server/README.md b/servers/hello/src/unprotected-server/README.md deleted file mode 100644 index 4e6d611..0000000 --- a/servers/hello/src/unprotected-server/README.md +++ /dev/null @@ -1,95 +0,0 @@ -# Unprotected Server Example - -The unprotected example is the base reference to build the [Approov protected servers](/servers/hello/src/approov-protected-server/). This a very basic Hello World server. - - -## TOC - Table of Contents - -* [Why?](#why) -* [How it Works?](#how-it-works) -* [Requirements](#requirements) -* [Try It](#try-it) - - -## Why? - -To be the starting building block for the [Approov protected servers](/servers/hello/src/approov-protected-server/), that will show you how to lock down your API server to your mobile app. Please read the brief summary in the [Approov Overview](/OVERVIEW.md#why) at the root of this repo or visit our [website](https://approov.io/product) for more details. - -[TOC](#toc---table-of-contents) - - -## How it works? - -The Java Spring API server is very simple and only replies to the endpoint `/` with the message: - -```json -{"message": "Hello, World!"} -``` - -You can find the endpoint definition [here](./src/main/java/com/criticalblue/approov/jwt). - -[TOC](#toc---table-of-contents) - - -## Requirements - -To run this example you will need to have installed: - -* [OpenJDK](https://openjdk.java.net/install/) - This server example uses version `11.0.3`. It should work with earlier or later versions but was not tested. -* [Java Spring](https://docs.spring.io/spring-boot/docs/current/reference/html/getting-started.html#getting-started.installing) - Version `2.6.4` of the Spring Framework plugin is being used. The code should work with prior versions but wasn't tested. - -[TOC](#toc---table-of-contents) - - -## Try It - -First build the server with gradle. From the `./servers/hello/src/unprotected-server` folder execute: - -```bash -./gradlew build -``` - -Now, you can run this example from the `./servers/hello/src/unprotected-server` folder with: - -```bash -source .env && ./gradlew bootRun -``` - -Finally, you can test that it works with: - -```text -curl -X GET 'http://localhost:8002' -``` - -The response will be: - -```json -{"message":"Hello, World!"} -``` - -[TOC](#toc---table-of-contents) - - -## Issues - -If you find any issue while following our instructions then just report it [here](https://github.com/approov/quickstart-java-spring-token-check/issues), with the steps to reproduce it, and we will sort it out and/or guide you to the correct path. - -[TOC](#toc---table-of-contents) - - -## Useful Links - -If you wish to explore the Approov solution in more depth, then why not try one of the following links as a jumping off point: - -* [Approov Free Trial](https://approov.io/signup)(no credit card needed) -* [Approov Get Started](https://approov.io/product/demo) -* [Approov QuickStarts](https://approov.io/docs/latest/approov-integration-examples/) -* [Approov Docs](https://approov.io/docs) -* [Approov Blog](https://approov.io/blog/) -* [Approov Resources](https://approov.io/resource/) -* [Approov Customer Stories](https://approov.io/customer) -* [Approov Support](https://approov.io/contact) -* [About Us](https://approov.io/company) -* [Contact Us](https://approov.io/contact) - -[TOC](#toc---table-of-contents) diff --git a/servers/hello/src/unprotected-server/build.gradle b/servers/hello/src/unprotected-server/build.gradle deleted file mode 100644 index 71bdd33..0000000 --- a/servers/hello/src/unprotected-server/build.gradle +++ /dev/null @@ -1,31 +0,0 @@ -plugins { - id 'org.springframework.boot' version '2.6.4' - id 'java' -} - -apply plugin: 'io.spring.dependency-management' - -group = 'com.criticalblue' -version = '0.0.1-SNAPSHOT' -sourceCompatibility = '1.8' - -repositories { - mavenCentral() -} - -dependencies { - implementation 'org.springframework.boot:spring-boot-starter-integration' - implementation 'org.springframework.boot:spring-boot-starter-security' - implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.springframework.security:spring-security-core' - implementation 'org.springframework.security:spring-security-web' - implementation 'org.springframework.security:spring-security-config' - - compileOnly 'org.jetbrains:annotations:17.0.0' - - compileOnly 'javax.servlet:servlet-api:3.1.0' - - runtimeOnly 'org.springframework.boot:spring-boot-devtools' - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'org.springframework.security:spring-security-test' -} diff --git a/servers/hello/src/unprotected-server/gradle/wrapper/gradle-wrapper.properties b/servers/hello/src/unprotected-server/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 6c58388..0000000 --- a/servers/hello/src/unprotected-server/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -#Tue Apr 02 13:15:36 BST 2019 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-all.zip diff --git a/servers/hello/src/unprotected-server/gradlew b/servers/hello/src/unprotected-server/gradlew deleted file mode 100755 index af6708f..0000000 --- a/servers/hello/src/unprotected-server/gradlew +++ /dev/null @@ -1,172 +0,0 @@ -#!/usr/bin/env sh - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m"' - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn () { - echo "$*" -} - -die () { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=$((i+1)) - done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=$(save "$@") - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" - -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi - -exec "$JAVACMD" "$@" diff --git a/servers/hello/src/unprotected-server/gradlew.bat b/servers/hello/src/unprotected-server/gradlew.bat deleted file mode 100644 index 0f8d593..0000000 --- a/servers/hello/src/unprotected-server/gradlew.bat +++ /dev/null @@ -1,84 +0,0 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/servers/hello/src/unprotected-server/settings.gradle b/servers/hello/src/unprotected-server/settings.gradle deleted file mode 100644 index 67a9126..0000000 --- a/servers/hello/src/unprotected-server/settings.gradle +++ /dev/null @@ -1,6 +0,0 @@ -pluginManagement { - repositories { - gradlePluginPortal() - } -} -rootProject.name = 'approov-jwt' diff --git a/servers/hello/src/unprotected-server/src/main/java/com/criticalblue/approov/jwt/ApiController.java b/servers/hello/src/unprotected-server/src/main/java/com/criticalblue/approov/jwt/ApiController.java deleted file mode 100644 index 1a42cd2..0000000 --- a/servers/hello/src/unprotected-server/src/main/java/com/criticalblue/approov/jwt/ApiController.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.criticalblue.approov.jwt; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; -import javax.servlet.http.HttpServletRequest; -import java.util.LinkedHashMap; -import java.util.Map; - -@RestController -public class ApiController { - - private static Logger logger = LoggerFactory.getLogger(ApiController.class); - - @GetMapping("/") - public Map helloV1() { - - logger.info("Serving request for endpoint '/', that isn't protected by an Approov Token."); - - Map response = new LinkedHashMap<>(); - - response.put("message", "Hello, World!"); - - return response; - } -} diff --git a/servers/hello/src/unprotected-server/src/main/java/com/criticalblue/approov/jwt/Application.java b/servers/hello/src/unprotected-server/src/main/java/com/criticalblue/approov/jwt/Application.java deleted file mode 100644 index c547b68..0000000 --- a/servers/hello/src/unprotected-server/src/main/java/com/criticalblue/approov/jwt/Application.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.criticalblue.approov.jwt; - -import org.apache.catalina.Context; -import org.apache.catalina.connector.Connector; -import org.apache.tomcat.util.descriptor.web.SecurityCollection; -import org.apache.tomcat.util.descriptor.web.SecurityConstraint; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; -import org.springframework.boot.web.servlet.server.ServletWebServerFactory; -import org.springframework.context.annotation.Bean; - -@SpringBootApplication -public class Application { - - private static Logger logger = LoggerFactory.getLogger(Application.class); - - @Value("${http.port}") - private int httpPort; - - @Value("${https.port}") - private int httpsPort; - - @Value("${http.redirect}") - private boolean isToRedirectHttp; - - public static void main(String[] args) { - SpringApplication.run(Application.class, args); - } - - @Bean - public ServletWebServerFactory servletContainer() { - - TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory() { - - @Override - protected void postProcessContext(Context context) { - if (isToRedirectHttp) { - logger.info("Creating security constrain to redirect http to https."); - SecurityConstraint securityConstraint = new SecurityConstraint(); - securityConstraint.setUserConstraint("CONFIDENTIAL"); - SecurityCollection collection = new SecurityCollection(); - collection.addPattern("/*"); - securityConstraint.addCollection(collection); - context.addConstraint(securityConstraint); - } - } - }; - - tomcat.addAdditionalTomcatConnectors(createConnector()); - return tomcat; - } - - private Connector createConnector() { - Connector connector = new Connector(TomcatServletWebServerFactory.DEFAULT_PROTOCOL); - - connector.setScheme("http"); - connector.setPort(httpPort); - connector.setSecure(false); - - if (isToRedirectHttp) { - logger.info("Redirecting http to port: {}", httpsPort); - connector.setRedirectPort(httpsPort); - } - - return connector; - } -} diff --git a/servers/hello/src/unprotected-server/src/main/java/com/criticalblue/approov/jwt/CustomServletErrorAttributes.java b/servers/hello/src/unprotected-server/src/main/java/com/criticalblue/approov/jwt/CustomServletErrorAttributes.java deleted file mode 100644 index 3abac84..0000000 --- a/servers/hello/src/unprotected-server/src/main/java/com/criticalblue/approov/jwt/CustomServletErrorAttributes.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.criticalblue.approov.jwt; - -import org.springframework.boot.web.servlet.error.DefaultErrorAttributes; -import org.springframework.boot.web.error.ErrorAttributeOptions; -import org.springframework.stereotype.Component; -import org.springframework.web.context.request.WebRequest; -import java.util.Map; - -@Component -public class CustomServletErrorAttributes extends DefaultErrorAttributes { - - @Override - public Map getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) { - - Map errorAttributes = super.getErrorAttributes(webRequest, options); - - // Remove from response in order to make the response comply with the Hello API specification - errorAttributes.remove("timestamp"); - errorAttributes.remove("message"); - errorAttributes.remove("path"); - errorAttributes.remove("error"); - errorAttributes.remove("trace"); - errorAttributes.remove("status"); - - return errorAttributes; - } -} diff --git a/servers/hello/src/unprotected-server/src/main/java/com/criticalblue/approov/jwt/WebSecurityConfig.java b/servers/hello/src/unprotected-server/src/main/java/com/criticalblue/approov/jwt/WebSecurityConfig.java deleted file mode 100644 index 0e0fb02..0000000 --- a/servers/hello/src/unprotected-server/src/main/java/com/criticalblue/approov/jwt/WebSecurityConfig.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.criticalblue.approov.jwt; - -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 { - - @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 ApiSecurityConfig extends WebSecurityConfigurerAdapter { - - @Override - protected void configure(HttpSecurity http) throws Exception { - - http.cors(); - - http - .httpBasic().disable() - .formLogin().disable() - .logout().disable() - .csrf().disable() - .authorizeRequests().antMatchers("/**").permitAll(); - } - } -} diff --git a/servers/hello/src/unprotected-server/src/main/resources/application.properties b/servers/hello/src/unprotected-server/src/main/resources/application.properties deleted file mode 100644 index cda3b69..0000000 --- a/servers/hello/src/unprotected-server/src/main/resources/application.properties +++ /dev/null @@ -1,41 +0,0 @@ -######################### -# SPRING CONFIGURATION -######################### - -spring.mvc.throw-exception-if-no-handler-found: true -spring.resources.add-mappings: false - - -######################### -# LOGGER CONFIGURATION -######################### - -logging.level.root: ERROR -logging.level.org.hibernate: ERROR -logging.level.org.springframework.web: ERROR -logging.level.org.springframework.security: ERROR -logging.level.com.criticalblue.approov: INFO - - -####################### -# HTTP CONFIGURATION -####################### - -# This vars need to be set in the .env file or in the environment -http.port: ${HTTP_PORT} -http.redirect: ${HTTP_REDIRECT} - - -####################### -# HTTPS CONFIGURATION -####################### - -# Needs to be set in the .env file or in the environment -server.port: ${HTTPS_PORT} - -# Self signed certificate was generated with: -# keytool -genkeypair -alias ApproovTLS -keyalg RSA -keysize 2048 -storetype PKCS12 -keystore ApproovTLS.p12 -validity 100000 -server.ssl.key-store-type: PKCS12 -server.ssl.key-store: classpath:keystore/ApproovTLS.p12 -server.ssl.key-store-password: supersecret -server.ssl.key-alias: ApproovTLS diff --git a/servers/hello/src/unprotected-server/src/main/resources/keystore/ApproovTLS.p12 b/servers/hello/src/unprotected-server/src/main/resources/keystore/ApproovTLS.p12 deleted file mode 100644 index 72a2163..0000000 Binary files a/servers/hello/src/unprotected-server/src/main/resources/keystore/ApproovTLS.p12 and /dev/null differ diff --git a/servers/hello/src/unprotected-server/src/test/java/com/criticalblue/approov/jwt/ApplicationTests.java b/servers/hello/src/unprotected-server/src/test/java/com/criticalblue/approov/jwt/ApplicationTests.java deleted file mode 100644 index 0a8fc7d..0000000 --- a/servers/hello/src/unprotected-server/src/test/java/com/criticalblue/approov/jwt/ApplicationTests.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.criticalblue.approov.jwt; - -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.http.ResponseEntity; - -import static org.assertj.core.api.Assertions.assertThat; - -@SpringBootTest -public class ApplicationTests { - - @Test - public void contextLoads() { - } - -} diff --git a/servers/shapes-api/.env.example b/servers/shapes-api/.env.example deleted file mode 100644 index bc74c47..0000000 --- a/servers/shapes-api/.env.example +++ /dev/null @@ -1,22 +0,0 @@ -########### -# SERVER -########### - -HTTP_REDIRECT=false -HTTP_PORT=8002 -HTTPS_PORT=8003 - - -############ -# APPROOV -############ - -APPROOV_TOKEN_BINDING_HEADER_NAME=Authorization - -# Feel free to play with different secrets. For development only you can create them with: -# $ openssl rand -base64 64 | tr -d '\n'; echo -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 diff --git a/servers/shapes-api/README.md b/servers/shapes-api/README.md deleted file mode 100644 index 1256ea1..0000000 --- a/servers/shapes-api/README.md +++ /dev/null @@ -1,831 +0,0 @@ -# APPROOV JAVA SPRING INTEGRATION EXAMPLE - -To see how a Java Spring server runs with an Approov integration please follow the -[Approov Shapes API Server](./docs/approov-shapes-api-server.md) walk-through. - -The implementation of the Approov token check is on [this folder](./src/main/java/com/criticalblue/approov/jwt/authentication), that uses the Java Spring Framework Security package to implement the authentication flow for checking the Approov token. - -Now let's continue reading this README for a **quick start** introduction in how -to integrate Approov on a project built with the Java Spring Framework. - -You may want to first follow [this demo walk-through](./docs/approov-shapes-api-server.md) before you try the Approov integration on your own app, but it's not mandatory you do it, but doing so will give you a better understanding how everything fits together in the simple Shapes app. - - -## 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 have 3 parts, that -> are separated by dots and represented as a string in the format of -> `header.payload.signature`. Read more about JWT tokens [here](https://jwt.io/introduction/). - -### 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 value, 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 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 demo. - -Docker is only required for developers wanting to use the Java docker stack provided -by the [stack](./stack) bash script, that is a wrapper around docker commands. - -Postman is the tool we recommend to be used when simulating the queries against -the API, but feel free to use any other tool of your preference. - - -## 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](./docs/approov-shapes-api-server.md#development-environment) -walk-through. - -For example, to get a shell inside the docker stack: - -```bash -$ ./stack shell -``` - -Now, you can do whatever you need inside this shell, like: - -```bash -$ java --version -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) - -$ gradle --version - ------------------------------------------------------------- -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 -``` - -The use of the docker stack is not mandatory thus feel free to use your local environment to play with this Approov integration. - -### The Postman Collection - -As you go through your Approov Integration you may want to test it and if you are using Postman then you can import this [Postman collection](https://raw.githubusercontent.com/approov/postman-collections/master/quickstarts/shapes-api/shapes-api.postman_collection.json) to see how it's done for the Approov Shapes API Server [example](./docs/approov-shapes-api-server.md), and use it as an inspiration or starting point for your own collection. - -The Approov tokens used in the headers of this Postman collection where generated with this [Python script](./bin/generate-token), that used the dummy secret set on the `.env.example` file to sign all the Approov tokens. - -If you are using the Aproov secret retrieved with the [Approov CLI]((https://approov.io/docs/latest/approov-cli-tool-reference/)) tool then you need to use it to generate some valid and invalid tokens. Some examples of using it can be found in the Approov [docs](https://approov.io/docs/latest/approov-usage-documentation/#generating-example-tokens). - - -## DEPENDENCIES - -Probably the only dependencies from the [build.gradle](./build.gradle) that you -do not have in your own project are this ones: - -```gradle -implementation 'io.jsonwebtoken:jjwt-api:0.10.5' -runtime 'io.jsonwebtoken:jjwt-impl:0.10.5', - 'io.jsonwebtoken:jjwt-jackson:0.10.5' - -implementation 'io.github.cdimascio:java-dotenv:5.0.1' -``` - -If they are not yet in your project 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 3 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 Approov token binding. - -To integrate Approov in your own project you may want to use the package -[com.criticalblue.approov.jwt.authentication](./src/main/java/com/criticalblue/approov/jwt/authentication), that contains all the code that -is project agnostic. To use this package you need to configure it from the class -extending the `WebSecurityConfigurerAdapter`, that in this demo is named as -[WebSecurityConfig](./src/main/java/com/criticalblue/approov/jwt/WebSecurityConfig.java). - - -### Understanding the WebSecurityConfig - -The [WebSecurityConfig](./src/main/java/com/criticalblue/approov/jwt/WebSecurityConfig.java) -is where we will setup 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 is required to always check if the signature and -expiration time of the Approov token is valid, and optionally to check if the -Approov 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 approovConfig, boolean checkTokenBinding)` -and the `ApproovAuthenticationEntryPoint()`to the Spring framework security -context, plus the endpoint name and http verbs, were the authentication should -be triggered. - -The `approovConfig` contains several information necessary to check the -Approov token, like 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 -[here](./src/main/java/com/criticalblue/approov/jwt/authentication/ApproovConfig.java). - -Each time we add and endpoint to be protected by an Approov token we need to -tell if the Approov token binding is to be checked or not, and this is done with -the boolean flag `checkTokenBinding`. - -In order to be able to have endpoints that perform only the required checks in -the Approov token, while at the same time having others endpoints where both the -required and optional checks must take place, we need to configure the Spring -framework security context with static subclasses of the main `WebSecurityConfig` -class, and this sub classes also need to implement the abstract -`WebSecurityConfigurerAdapter` class. This 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, that in this demo is to -allow any request to the root endpoint `/` 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](./.env.example) as your -starting point. - -The `.env` file must contain this five variables: - -```env -APPROOV_TOKEN_BINDING_HEADER_NAME=Authorization - -# Feel free to play with different secrets. For development only you can create them with: -# $ openssl rand -base64 64 | tr -d '\n'; echo -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 Code - -Add the package [com.criticalblue.approov.jwt.authentication](./src/main/java/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: - -```java -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() - .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() - } -} -``` - -Now let's protect the endpoint for `/v2/shapes` and `/v2/forms` with an Approov token. - -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 -Approov token binding. - -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 Aprroov token, and finally a subclass for endpoints that do not require -authentication. - -So let's prepare the `WebSecurityConfig` with only a subclass that maintains the -access to all endpoints without any authentication. - -Lets' add the subclass `ApiWebSecurityConfig`: - -```java -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.setAllowedOrigins(Arrays.asList("http://localhost:8002")); - 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`, thus we -need to allow it in the CORS configuration. - -If our Approov integration also uses the Approov token binding check, then we -also need to allow the header from where we want to retrieve the value we bind -to the Approov token payload in the mobile app, that in this demo is the -`Authorization` header. - -So we add to the CORS configuration this 2 new lines: - -```java -configuration.addAllowedHeader("Authorization"); -configuration.addAllowedHeader("Approov-Token"); -``` - -That will give us this new CORS configuration: - -```java -@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`: - -```java -@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`: - -```java -@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 that was 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 `AproovPayloadWebSecurityConfig`: - -```java -@Configuration -@Order(2) -public static class AproovPayloadWebSecurityConfig 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 noticed that the configuration order is the same -as of the subclass `ApiWebSecurityConfig` in the previous step, thus we need to -change it again, this time from `2` to `3`: - -```java -@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/forms` to be reached without any authentication. - - -#### Putting All-Together - -After we implemented the Approov protection for the `/v2/shapes` and `/v2/forms` -endpoints the class `WebSecurityConfig` should look like: - -```java -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 AproovPayloadWebSecurityConfig 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 this difference: - -```java ---- untitled (Previous) -+++ /home/sublime/workspace/java/spring/src/main/java/com/criticalblue/approov/jwt/WebSecurityConfig.java -@@ -1,6 +1,7 @@ - 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; -@@ -25,6 +26,8 @@ - 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; -@@ -35,27 +38,86 @@ - web.ignoring().antMatchers("/error"); - } - -- @Override -- protected void configure(HttpSecurity http) throws Exception { -+ @Configuration -+ @Order(1) -+ public static class ApproovWebSecurityConfig extends WebSecurityConfigurerAdapter { - -- http.cors(); -+ @Override -+ protected void configure(HttpSecurity http) throws Exception { - -- http -- .httpBasic().disable() -- .formLogin().disable() -- .logout().disable() -- .csrf().disable() -- .authenticationProvider(new ApproovAuthenticationProvider(approovConfig)) -- .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); -+ http.cors(); - -- http -- .authorizeRequests() -- .antMatchers(HttpMethod.GET, "/").permitAll() -- .antMatchers(HttpMethod.GET, "/v2/hello").permitAll() -- .antMatchers(HttpMethod.GET, "/v2/shapes").permitAll() -- .antMatchers(HttpMethod.GET, "/v2/forms").permitAll(); -+ http -+ .httpBasic().disable() -+ .formLogin().disable() -+ .logout().disable() -+ .csrf().disable() -+ .authenticationProvider(new ApproovAuthenticationProvider(approovConfig)) -+ .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); - -- // the above endpoints declaration can be resumed to: -- // .antMatchers(HttpMethod.GET, "/**").permitAll() -+ 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 AproovPayloadWebSecurityConfig 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(); -+ } -+ } -+} -``` - - -As we can see the Approov integration in a current server is simple, easy and is -done with just a few lines of code. - -If you have not done it already, now is time to follow the -[Approov Shapes API Server](./docs/approov-shapes-api-server.md) walk-through -to see and have a feel for how all this works. - - -## PRODUCTION - -In order to protect the communication between your mobile app and the API server -is important to only communicate hover a secure communication channel, aka 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](https://approov.io/docs/mitm-detection.html). - -We do not use certificate pinning in this Approov integration example -because we want to be able to demonstrate, via Postman how, the API works. - -However in production will be mandatory to implement [certificate pinning](https://approov.io/docs/mitm-detection.html#id1). diff --git a/servers/shapes-api/bin/generate-token b/servers/shapes-api/bin/generate-token deleted file mode 100755 index 446e348..0000000 --- a/servers/shapes-api/bin/generate-token +++ /dev/null @@ -1,92 +0,0 @@ -#!/usr/bin/env python3 - -""" -GENERATE APPROOV TOKEN CLI - -To be used only to generate Approov tokens for testing purposes during development. - -Usage: - generate-token - generate-token [--expire EXPIRE] [--claim CLAIM] [--claim-example] [--secret SECRET] - -Options: - --expire EXPIRE The Approov token expire time in minutes [default: 5]. - --claim CLAIM The base64 encode sha256 hash of the custom payload claim for the Approov token. - --claim-example Same as --claim but using an hard-coded claim example. - --secret SECRET The base64 encoded secret to sign the Approov token for test purposes. - -h --help Show this screen. - -v --version Show version. - -""" - -# Standard Libraries -from os import getenv -from sys import exit -from time import time -from hashlib import sha256 -from base64 import b64decode, b64encode - -# Third-Party Libraries -from jwt import encode -from docopt import docopt - -# to base64 encode the custom payload claim hash: http://tomeko.net/online_tools/hex_to_base64.php -REQUEST_CLAIM_RAW_VALUE_EXAMPLE = 'claim-value-to-be-sha256-hashed-and-base64-encoded' - -def _generateSha256HashBase64Encoded(value): - value_hash = sha256(value.encode('utf-8')).digest() - return b64encode(value_hash).decode('utf-8') - -def generateToken(approov_base64_secret, token_expire_in_minutes, request_claim_raw_value): - """Generates a token with a 5 minutes lifetime. Optionally we can set also a custom payload claim.""" - - approov_base64_secret = approov_base64_secret.strip() - - if not approov_base64_secret: - raise ValueError('Approov base64 encoded secret is missing.') - - if not token_expire_in_minutes: - token_expire_in_minutes = 5 - - payload = { - 'exp': time() + (60 * token_expire_in_minutes), # required - the timestamp for when the token expires. - } - - if request_claim_raw_value: - payload['pay'] = _generateSha256HashBase64Encoded(request_claim_raw_value) - - return encode(payload, b64decode(approov_base64_secret), algorithm='HS256').decode() - -def main(): - - arguments = docopt(__doc__, version='GENERATE APPROOV TOKEN CLI - 1.0') - - request_claim_raw_value = None - token_expire_in_minutes = int(arguments['--expire']) - approov_base64_secret = getenv("APPROOV_BASE64_SECRET") - - if arguments['--claim']: - request_claim_raw_value = arguments['--claim'] - - if not request_claim_raw_value and arguments['--claim-example'] is True: - request_claim_raw_value = REQUEST_CLAIM_RAW_VALUE_EXAMPLE - - if arguments['--secret']: - approov_base64_secret = arguments['--secret'] - - if not approov_base64_secret: - raise ValueError('--secret was provided as an empty string in the CLI or in the .env file.') - - token = generateToken(approov_base64_secret, token_expire_in_minutes, request_claim_raw_value) - - print('Token:\n', token) - - return token - -if __name__ == '__main__': - try: - main() - exit(0) - except Exception as error: - print(error) - exit(1) diff --git a/servers/shapes-api/build.gradle b/servers/shapes-api/build.gradle deleted file mode 100644 index 7c81148..0000000 --- a/servers/shapes-api/build.gradle +++ /dev/null @@ -1,35 +0,0 @@ -plugins { - id 'org.springframework.boot' version '2.1.3.RELEASE' - id 'java' -} - -apply plugin: 'io.spring.dependency-management' - -group = 'com.criticalblue' -version = '0.0.1-SNAPSHOT' -sourceCompatibility = '1.8' - -repositories { - mavenCentral() -} - -dependencies { - implementation 'org.springframework.boot:spring-boot-starter-integration' - implementation 'org.springframework.boot:spring-boot-starter-security' - implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.springframework.security:spring-security-core' - implementation 'org.springframework.security:spring-security-web' - implementation 'org.springframework.security:spring-security-config' - - implementation 'io.jsonwebtoken:jjwt-api:0.10.5' - runtime 'io.jsonwebtoken:jjwt-impl:0.10.5', - 'io.jsonwebtoken:jjwt-jackson:0.10.5' - - compileOnly 'org.jetbrains:annotations:17.0.0' - - compileOnly 'javax.servlet:servlet-api:3.1.0' - - runtimeOnly 'org.springframework.boot:spring-boot-devtools' - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'org.springframework.security:spring-security-test' -} diff --git a/servers/shapes-api/docker/Dockerfile b/servers/shapes-api/docker/Dockerfile deleted file mode 100644 index 5b79eaf..0000000 --- a/servers/shapes-api/docker/Dockerfile +++ /dev/null @@ -1,96 +0,0 @@ -FROM openjdk:11.0.3 - -ARG CONTAINER_USER="java" -ARG CONTAINER_UID="1000" -ARG ZSH_THEME="robbyrussell" -ARG GRADLE_VERSION=5.2.1 - - -# Will not prompt for questions -ENV DEBIAN_FRONTEND=noninteractive \ - CONTAINER_USER="${CONTAINER_USER}" \ - CONTAINER_UID="${CONTAINER_UID}" \ - ROOT_CA_DIR=/root-ca/ \ - ROOT_CA_KEY="self-signed-root-ca.key" \ - ROOT_CA_PEM="self-signed-root-ca.pem" \ - ROOT_CA_NAME="ApproovStackRootCA" \ - PROXY_CA_FILENAME="FirewallProxyCA.crt" \ - PROXY_CA_PEM="certificates/FirewallProxyCA.crt" \ - PROXY_CA_NAME="FirewallProxy" \ - NO_AT_BRIDGE=1 \ - DISPLAY=":0" \ - GRADLE_HOME=/opt/gradle/gradle-"${GRADLE_VERSION}" \ - PATH=/opt/gradle/gradle-"${GRADLE_VERSION}"/bin:${PATH} - -COPY ./setup ${ROOT_CA_DIR} - -RUN apt update && \ - apt -y upgrade && \ - - apt -y install \ - python3 \ - python3-pip \ - locales \ - tzdata \ - ca-certificates \ - inotify-tools \ - libnss3-tools \ - zip \ - zsh \ - curl \ - git \ - maven && \ - - printf "\n\n----------> FORCING INSTALLATION OF MISSING DEPENDENCIES <------------\n\n" && \ - apt -y -f install && \ - - printf "\n\n----------> FIXING INOTIFY WATCHES <------------\n\n" && \ - #https://github.com/guard/listen/wiki/Increasing-the-amount-of-inotify-watchers - printf "fs.inotify.max_user_watches=524288\n" >> /etc/sysctl.conf && \ - - printf "\n\n----------> ADDING LOCALE <------------\n\n" && \ - echo "en_GB.UTF-8 UTF-8" > /etc/locale.gen && \ - locale-gen en_GB.UTF-8 && \ - dpkg-reconfigure locales && \ - - printf "\n\n----------> ADDING A USER <------------\n\n" && \ - useradd -m -u ${CONTAINER_UID} -s /usr/bin/zsh ${CONTAINER_USER} && \ - - printf "\n\n----------> INSTALLING CUSTOM CERTIFICATES <------------\n\n" && \ - cd ${ROOT_CA_DIR} && \ - ./setup-root-certificate.sh "${ROOT_CA_KEY}" "${ROOT_CA_PEM}" "${ROOT_CA_NAME}" && \ - ./add-proxy-certificate.sh "${PROXY_CA_PEM}" && \ - - printf "\n\n----------> INSTALLING GRADLE <------------\n\n" && \ - curl -o gradle.zip -fsSL https://services.gradle.org/distributions/gradle-"${GRADLE_VERSION}"-bin.zip && \ - unzip -d /opt/gradle gradle.zip && \ - rm -f gradle.zip && \ - gradle --version && \ - - printf "\n\n----------> INSTALLING OH MY ZSH <------------\n\n" && \ - bash -c "$(curl -fsSL https://raw.githubusercontent.com/robbyrussell/oh-my-zsh/master/tools/install.sh)" && \ - chsh -s /usr/bin/zsh && \ - cp -R /root/.oh-my-zsh /home/"${CONTAINER_USER}" && \ - cp /root/.zsh* /home/"${CONTAINER_USER}" && \ - sed -i "s/\/root/\/home\/${CONTAINER_USER}/g" /home/"${CONTAINER_USER}"/.zshrc && \ - chown -R "${CONTAINER_USER}":"${CONTAINER_USER}" /home/"${CONTAINER_USER}" && \ - - printf "\n\n----------> CLEANUP <------------\n\n" && \ - rm -rvf /var/lib/apt/lists/* - -ENV LANG=en_GB.UTF-8 \ - LANGUAGE=en_GB:en \ - LC_ALL=en_GB.UTF-8 - -USER ${CONTAINER_USER} - -RUN pip3 install \ - pyjwt \ - docopt - -# pip install will put the executables under ~/.local/bin -ENV PATH=/home/"${CONTAINER_USER}"/.local/bin:$PATH - -WORKDIR /home/${CONTAINER_USER}/workspace - -CMD ["zsh"] diff --git a/servers/shapes-api/docker/setup/adb-setup-certificate.sh b/servers/shapes-api/docker/setup/adb-setup-certificate.sh deleted file mode 100755 index 2b87a9b..0000000 --- a/servers/shapes-api/docker/setup/adb-setup-certificate.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash - -# https://stackoverflow.com/a/48814971/6454622 - -set -eu - -CA_PEM=${1?Missing certificate file name} - -cert_name=$(openssl x509 -inform PEM -subject_hash_old -in ${CA_PEM} | head -1) -cat ${CA_PEM} > $cert_name -openssl x509 -inform PEM -text -in ${CA_PEM} -out nul >> $cert_name - -adb shell mount -o rw,remount,rw /system -adb push $cert_name /system/etc/security/cacerts/ -adb shell mount -o ro,remount,ro /system diff --git a/servers/shapes-api/docker/setup/add-certificate-to-browser.sh b/servers/shapes-api/docker/setup/add-certificate-to-browser.sh deleted file mode 100755 index cce7176..0000000 --- a/servers/shapes-api/docker/setup/add-certificate-to-browser.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/bin/bash - -set -eu - -### -# https://thomas-leister.de/en/how-to-import-ca-root-certificate/ -### - - -### Script installs root.cert.pem to certificate trust store of applications using NSS -### (e.g. Firefox, Thunderbird, Chromium) -### Mozilla uses cert8, Chromium and Chrome use cert9 - -### -### Requirement: apt install libnss3-tools -### - -CA_PEM="${1?Missing file path for the PEM certificate}" -CA_NAME="${2?Missing Certificate Name}" -BROWSER_CONFIG_DIR="${3:-/home/node}" - -printf "\n>>> ADDING CERTIFICATE TO BROWSERS TRUSTED STORE <<<\n" - -if [ -f "${CA_PEM}" ] - then - printf "\n--> CERTIFICATE FILE: ${CA_PEM}\n" - printf "\n--> CERTIFICATE NAME: ${CA_NAME}\n" - printf "\n--> BROWSER CONFIG DIR: ${BROWSER_CONFIG_DIR}\n" - - ### - ### For cert8 (legacy - DBM) - ### - for certDB in $(find ${BROWSER_CONFIG_DIR} -name "cert8.db") - do - certdir=$(dirname ${certDB}); - certutil -A -n "${CA_NAME}" -t "TCu,Cu,Tu" -i ${CA_PEM} -d dbm:${certdir} - done - - ### - ### For cert9 (SQL) - ### - for certDB in $(find ${BROWSER_CONFIG_DIR} -name "cert9.db") - do - certdir=$(dirname ${certDB}); - certutil -A -n "${CA_NAME}" -t "TCu,Cu,Tu" -i ${CA_PEM} -d sql:${certdir} - done - else - printf "\n>>> CERTIFICATE FILE NOT FOUND FOR: ${CA_PEM}\n" -fi diff --git a/servers/shapes-api/docker/setup/add-certificate-to-node-server.sh b/servers/shapes-api/docker/setup/add-certificate-to-node-server.sh deleted file mode 100755 index 6d84496..0000000 --- a/servers/shapes-api/docker/setup/add-certificate-to-node-server.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash - -set -eu - -CA_PEM_FILE="${1?Missing name for certificate file}" -CA_EXTENSION="${CA_PEM_FILE##*.}" - -if [ "${CA_EXTENSION}" != "pem" ] - then - printf "\nFATAL ERROR: Certificate must use .pem extension\n\n" - exit 1 -fi - -if [ -f "${CA_PEM_FILE}" ] - then - printf "\n>>> ADDING A CERTIFICATE TO NODE SERVER <<<\n" - - # Add certificate to node, so that we can use npm install - printf "cafile=${CA_PEM_FILE}" >> /root/.npmrc - printf "cafile=${CA_PEM_FILE}" >> /home/${CONTAINER_USER}/.npmrc; - - printf "\n >>> CERTICATE ADDED SUCCESEFULY<<<\n" - - else - printf "\n >>> NO CERTIFICATE TO ADD <<<\n" -fi - diff --git a/servers/shapes-api/docker/setup/add-proxy-certificate.sh b/servers/shapes-api/docker/setup/add-proxy-certificate.sh deleted file mode 100755 index f102483..0000000 --- a/servers/shapes-api/docker/setup/add-proxy-certificate.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash - -set -eu - -PROXY_CA_PEM="${1?Missing name for Proxy CRT file}" - -if [ -f "${PROXY_CA_PEM}" ] - then - printf "\n>>> ADDING A PROXY CERTIFICATE TO THE TRUSTED STORE <<<\n" - - # add certificate tpo the trust store - cp -v ${PROXY_CA_PEM} /usr/local/share/ca-certificates - update-ca-certificates - - # verifies the certificate - openssl x509 -in ${PROXY_CA_PEM} -text -noout > "${PROXY_CA_PEM}.txt" - - else - printf "\n >>> FATAL ERROR: Certificate not found in path ${PROXY_CA_PEM} <<<\n" -fi diff --git a/servers/shapes-api/docker/setup/certificates/.gitignore b/servers/shapes-api/docker/setup/certificates/.gitignore deleted file mode 100644 index d6b7ef3..0000000 --- a/servers/shapes-api/docker/setup/certificates/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore diff --git a/servers/shapes-api/docker/setup/create-domain-certificate.sh b/servers/shapes-api/docker/setup/create-domain-certificate.sh deleted file mode 100755 index cd42c42..0000000 --- a/servers/shapes-api/docker/setup/create-domain-certificate.sh +++ /dev/null @@ -1,59 +0,0 @@ -#!/bin/bash - -set -eu - -### -# inspired https://fabianlee.org/2018/02/17/ubuntu-creating-a-trusted-ca-and-san-certificate-using-openssl-on-ubuntu/ -### - - -DOMAIN="${1:-example.com}" -ROOT_CA_KEY="${2?Missing Name for root certificate KEY file}" -ROOT_CA_PEM="${3?Missing Name for root certificate PEM file}" - -DOMAIN_CA_KEY="${DOMAIN}.key" -DOMAIN_CA_CSR="${DOMAIN}.csr" -DOMAIN_CA_CRT="${DOMAIN}.crt" -DOMAIN_CA_TXT="${DOMAIN}.txt" -CONFIG_FILE="${DOMAIN}.cnf" - - -printf "\n>>> MERGINGING CONFIGURATION FROM ${DOMAIN_CA_TXT} INTO ${CONFIG_FILE} <<<\n" -cat openssl.cnf ${DOMAIN_CA_TXT} > ${CONFIG_FILE} - - -printf "\n>>> GENERATING KEY FOR DOMAIN CERTIFICATE: ${DOMAIN_CA_KEY} <<<\n" - -# generate the private/public RSA key pair for the domain -openssl genrsa -out ${DOMAIN_CA_KEY} 4096 - -printf "\n>>> GENERATING CSR FOR DOMAIN CERTIFICATE: ${DOMAIN_CA_CSR} <<<\n" - -# create the server certificate signing request: -openssl req \ - -subj "/CN=${DOMAIN}" \ - -extensions v3_req \ - -sha256 \ - -new \ - -key ${DOMAIN_CA_KEY} \ - -out ${DOMAIN_CA_CSR} - -printf "\n>>> GENERATING CRT FOR DOMAIN CERTIFICATE: ${DOMAIN_CA_CRT} <<<\n" - -# generate the server certificate using the: server signing request, the CA signing key, and CA cert. -openssl x509 \ - -req \ - -extensions v3_req \ - -days 3650 \ - -sha256 \ - -in ${DOMAIN_CA_CSR} \ - -CA ${ROOT_CA_PEM} \ - -CAkey ${ROOT_CA_KEY} \ - -CAcreateserial \ - -out ${DOMAIN_CA_CRT} \ - -extfile ${CONFIG_FILE} - -# verifies the certificate -openssl x509 -in ${DOMAIN_CA_CRT} -text -noout > ${DOMAIN}.txt - -printf "\n >>> CERTIFICATE CREATED FOR DOMAIN: ${DOMAIN} <<<\n" diff --git a/servers/shapes-api/docker/setup/localhost.txt b/servers/shapes-api/docker/setup/localhost.txt deleted file mode 100644 index 9ffb34d..0000000 --- a/servers/shapes-api/docker/setup/localhost.txt +++ /dev/null @@ -1,15 +0,0 @@ -[ v3_req ] - -# Extensions to add to a certificate request - -basicConstraints = CA:FALSE -keyUsage = nonRepudiation, digitalSignature, keyEncipherment - -#extendedKeyUsage=serverAuth -subjectAltName = @alt_names - - -[ alt_names ] - -DNS.1 = localhost -DNS.2 = *.localhost diff --git a/servers/shapes-api/docker/setup/openssl.cnf b/servers/shapes-api/docker/setup/openssl.cnf deleted file mode 100644 index a25e990..0000000 --- a/servers/shapes-api/docker/setup/openssl.cnf +++ /dev/null @@ -1,353 +0,0 @@ -# -# OpenSSL example configuration file. -# This is mostly being used for generation of certificate requests. -# - -# This definition stops the following lines choking if HOME isn't -# defined. -HOME = . -RANDFILE = $ENV::HOME/.rnd - -# Extra OBJECT IDENTIFIER info: -#oid_file = $ENV::HOME/.oid -oid_section = new_oids - -# To use this configuration file with the "-extfile" option of the -# "openssl x509" utility, name here the section containing the -# X.509v3 extensions to use: -# extensions = -# (Alternatively, use a configuration file that has only -# X.509v3 extensions in its main [= default] section.) - -[ new_oids ] - -# We can add new OIDs in here for use by 'ca', 'req' and 'ts'. -# Add a simple OID like this: -# testoid1=1.2.3.4 -# Or use config file substitution like this: -# testoid2=${testoid1}.5.6 - -# Policies used by the TSA examples. -tsa_policy1 = 1.2.3.4.1 -tsa_policy2 = 1.2.3.4.5.6 -tsa_policy3 = 1.2.3.4.5.7 - -#################################################################### -[ ca ] -default_ca = CA_default # The default ca section - -#################################################################### -[ CA_default ] - -dir = ./demoCA # Where everything is kept -certs = $dir/certs # Where the issued certs are kept -crl_dir = $dir/crl # Where the issued crl are kept -database = $dir/index.txt # database index file. -#unique_subject = no # Set to 'no' to allow creation of - # several certs with same subject. -new_certs_dir = $dir/newcerts # default place for new certs. - -certificate = $dir/cacert.pem # The CA certificate -serial = $dir/serial # The current serial number -crlnumber = $dir/crlnumber # the current crl number - # must be commented out to leave a V1 CRL -crl = $dir/crl.pem # The current CRL -private_key = $dir/private/cakey.pem# The private key -RANDFILE = $dir/private/.rand # private random number file - -x509_extensions = usr_cert # The extensions to add to the cert - -# Comment out the following two lines for the "traditional" -# (and highly broken) format. -name_opt = ca_default # Subject Name options -cert_opt = ca_default # Certificate field options - -# Extension copying option: use with caution. -# copy_extensions = copy - -# Extensions to add to a CRL. Note: Netscape communicator chokes on V2 CRLs -# so this is commented out by default to leave a V1 CRL. -# crlnumber must also be commented out to leave a V1 CRL. -# crl_extensions = crl_ext - -default_days = 365 # how long to certify for -default_crl_days= 30 # how long before next CRL -default_md = default # use public key default MD -preserve = no # keep passed DN ordering - -# A few difference way of specifying how similar the request should look -# For type CA, the listed attributes must be the same, and the optional -# and supplied fields are just that :-) -policy = policy_match - -# For the CA policy -[ policy_match ] -countryName = match -stateOrProvinceName = match -organizationName = match -organizationalUnitName = optional -commonName = supplied -emailAddress = optional - -# For the 'anything' policy -# At this point in time, you must list all acceptable 'object' -# types. -[ policy_anything ] -countryName = optional -stateOrProvinceName = optional -localityName = optional -organizationName = optional -organizationalUnitName = optional -commonName = supplied -emailAddress = optional - -#################################################################### -[ req ] -default_bits = 2048 -default_keyfile = privkey.pem -distinguished_name = req_distinguished_name -attributes = req_attributes -x509_extensions = v3_ca # The extensions to add to the self signed cert - -# Passwords for private keys if not present they will be prompted for -# input_password = secret -# output_password = secret - -# This sets a mask for permitted string types. There are several options. -# default: PrintableString, T61String, BMPString. -# pkix : PrintableString, BMPString (PKIX recommendation before 2004) -# utf8only: only UTF8Strings (PKIX recommendation after 2004). -# nombstr : PrintableString, T61String (no BMPStrings or UTF8Strings). -# MASK:XXXX a literal mask value. -# WARNING: ancient versions of Netscape crash on BMPStrings or UTF8Strings. -string_mask = utf8only - -req_extensions = v3_req # The extensions to add to a certificate request - -[ req_distinguished_name ] -countryName = Country Name (2 letter code) -countryName_default = AU -countryName_min = 2 -countryName_max = 2 - -stateOrProvinceName = State or Province Name (full name) -stateOrProvinceName_default = Some-State - -localityName = Locality Name (eg, city) - -0.organizationName = Organization Name (eg, company) -0.organizationName_default = Internet Widgits Pty Ltd - -# we can do this but it is not needed normally :-) -#1.organizationName = Second Organization Name (eg, company) -#1.organizationName_default = World Wide Web Pty Ltd - -organizationalUnitName = Organizational Unit Name (eg, section) -#organizationalUnitName_default = - -commonName = Common Name (e.g. server FQDN or YOUR name) -commonName_max = 64 - -emailAddress = Email Address -emailAddress_max = 64 - -# SET-ex3 = SET extension number 3 - -[ req_attributes ] -challengePassword = A challenge password -challengePassword_min = 4 -challengePassword_max = 20 - -unstructuredName = An optional company name - -[ usr_cert ] - -# These extensions are added when 'ca' signs a request. - -# This goes against PKIX guidelines but some CAs do it and some software -# requires this to avoid interpreting an end user certificate as a CA. - -basicConstraints=CA:FALSE - -# Here are some examples of the usage of nsCertType. If it is omitted -# the certificate can be used for anything *except* object signing. - -# This is OK for an SSL server. -# nsCertType = server - -# For an object signing certificate this would be used. -# nsCertType = objsign - -# For normal client use this is typical -# nsCertType = client, email - -# and for everything including object signing: -# nsCertType = client, email, objsign - -# This is typical in keyUsage for a client certificate. -# keyUsage = nonRepudiation, digitalSignature, keyEncipherment - -# This will be displayed in Netscape's comment listbox. -nsComment = "OpenSSL Generated Certificate" - -# PKIX recommendations harmless if included in all certificates. -subjectKeyIdentifier=hash -authorityKeyIdentifier=keyid,issuer - -# This stuff is for subjectAltName and issuerAltname. -# Import the email address. -# subjectAltName=email:copy -# An alternative to produce certificates that aren't -# deprecated according to PKIX. -# subjectAltName=email:move - -# Copy subject details -# issuerAltName=issuer:copy - -#nsCaRevocationUrl = http://www.domain.dom/ca-crl.pem -#nsBaseUrl -#nsRevocationUrl -#nsRenewalUrl -#nsCaPolicyUrl -#nsSslServerName - -# This is required for TSA certificates. -# extendedKeyUsage = critical,timeStamping - -[ v3_req ] - -# Extensions to add to a certificate request - -basicConstraints = CA:FALSE -keyUsage = nonRepudiation, digitalSignature, keyEncipherment - -#extendedKeyUsage=serverAuth -#subjectAltName = @alt_names - - -[ v3_ca ] - - -# Extensions for a typical CA - - -# PKIX recommendation. - -subjectKeyIdentifier=hash - -authorityKeyIdentifier=keyid:always,issuer - -#basicConstraints = critical,CA:true -basicConstraints = critical, CA:TRUE, pathlen:3 - - -# Key usage: this is typical for a CA certificate. However since it will -# prevent it being used as an test self-signed certificate it is best -# left out by default. -# keyUsage = cRLSign, keyCertSign -keyUsage = critical, cRLSign, keyCertSign - -# Some might want this also -nsCertType = sslCA, emailCA - -# Include email address in subject alt name: another PKIX recommendation -# subjectAltName=email:copy -# Copy issuer details -# issuerAltName=issuer:copy - -# DER hex encoding of an extension: beware experts only! -# obj=DER:02:03 -# Where 'obj' is a standard or added object -# You can even override a supported extension: -# basicConstraints= critical, DER:30:03:01:01:FF - -[ crl_ext ] - -# CRL extensions. -# Only issuerAltName and authorityKeyIdentifier make any sense in a CRL. - -# issuerAltName=issuer:copy -authorityKeyIdentifier=keyid:always - -[ proxy_cert_ext ] -# These extensions should be added when creating a proxy certificate - -# This goes against PKIX guidelines but some CAs do it and some software -# requires this to avoid interpreting an end user certificate as a CA. - -basicConstraints=CA:FALSE - -# Here are some examples of the usage of nsCertType. If it is omitted -# the certificate can be used for anything *except* object signing. - -# This is OK for an SSL server. -# nsCertType = server - -# For an object signing certificate this would be used. -# nsCertType = objsign - -# For normal client use this is typical -# nsCertType = client, email - -# and for everything including object signing: -# nsCertType = client, email, objsign - -# This is typical in keyUsage for a client certificate. -# keyUsage = nonRepudiation, digitalSignature, keyEncipherment - -# This will be displayed in Netscape's comment listbox. -nsComment = "OpenSSL Generated Certificate" - -# PKIX recommendations harmless if included in all certificates. -subjectKeyIdentifier=hash -authorityKeyIdentifier=keyid,issuer - -# This stuff is for subjectAltName and issuerAltname. -# Import the email address. -# subjectAltName=email:copy -# An alternative to produce certificates that aren't -# deprecated according to PKIX. -# subjectAltName=email:move - -# Copy subject details -# issuerAltName=issuer:copy - -#nsCaRevocationUrl = http://www.domain.dom/ca-crl.pem -#nsBaseUrl -#nsRevocationUrl -#nsRenewalUrl -#nsCaPolicyUrl -#nsSslServerName - -# This really needs to be in place for it to be a proxy certificate. -proxyCertInfo=critical,language:id-ppl-anyLanguage,pathlen:3,policy:foo - -#################################################################### -[ tsa ] - -default_tsa = tsa_config1 # the default TSA section - -[ tsa_config1 ] - -# These are used by the TSA reply generation only. -dir = ./demoCA # TSA root directory -serial = $dir/tsaserial # The current serial number (mandatory) -crypto_device = builtin # OpenSSL engine to use for signing -signer_cert = $dir/tsacert.pem # The TSA signing certificate - # (optional) -certs = $dir/cacert.pem # Certificate chain to include in reply - # (optional) -signer_key = $dir/private/tsakey.pem # The TSA private key (optional) -signer_digest = sha256 # Signing digest to use. (Optional) -default_policy = tsa_policy1 # Policy if request did not specify it - # (optional) -other_policies = tsa_policy2, tsa_policy3 # acceptable policies (optional) -digests = sha1, sha256, sha384, sha512 # Acceptable message digests (mandatory) -accuracy = secs:1, millisecs:500, microsecs:100 # (optional) -clock_precision_digits = 0 # number of digits after dot. (optional) -ordering = yes # Is ordering defined for timestamps? - # (optional, default: no) -tsa_name = yes # Must the TSA name be included in the reply? - # (optional, default: no) -ess_cert_id_chain = no # Must the ESS cert id chain be included? - # (optional, default: no) diff --git a/servers/shapes-api/docker/setup/setup-root-certificate.sh b/servers/shapes-api/docker/setup/setup-root-certificate.sh deleted file mode 100755 index db1a30c..0000000 --- a/servers/shapes-api/docker/setup/setup-root-certificate.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/bin/bash - -set -eu - -### -# inspired https://fabianlee.org/2018/02/17/ubuntu-creating-a-trusted-ca-and-san-certificate-using-openssl-on-ubuntu/ -### - - -ROOT_CA_KEY="${1?Missing Name for root certificate KEY file}" -ROOT_CA_PEM="${2?Missing Name for root certificate PEM file}" -ROOT_CA_NAME="${3?Missing Certificate Name}" -CONFIG_FILE="${4:-openssl.cnf}" - -if [ ! -f ROOT_CA_PEM ] - then - printf "\n>>> CREATING A ROOT CERTIFICATE <<<\n" - - openssl req \ - -new \ - -newkey rsa:4096 \ - -days 3650 \ - -nodes \ - -x509 \ - -extensions v3_ca \ - -subj "/C=US/ST=CA/L=SF/O=${ROOT_CA_NAME}/CN=${ROOT_CA_NAME}" \ - -keyout ${ROOT_CA_KEY} \ - -out ${ROOT_CA_PEM} \ - -config ${CONFIG_FILE} - - printf "\n>>> ADDING ROOT CERTIFICATE TO THE TRUSTED STORE <<<\n" - - # add certificate to the trust store - cp ${ROOT_CA_PEM} /usr/local/share/ca-certificates/self-signed-root-ca.crt - update-ca-certificates - - # verifies the certificate - openssl x509 -in ${ROOT_CA_PEM} -text -noout > "${ROOT_CA_NAME}.txt" - - printf "\n >>> ROOT CERTICATE CREATED SUCCESEFULY<<<\n" - - else - printf "\n >>> ROOT CERTICATE ALREADY EXISTS <<<\n" -fi diff --git a/servers/shapes-api/docker/usage-help.txt b/servers/shapes-api/docker/usage-help.txt deleted file mode 100644 index 5ef091c..0000000 --- a/servers/shapes-api/docker/usage-help.txt +++ /dev/null @@ -1,50 +0,0 @@ -DOCKER STACK CLI WRAPPER - -This bash script is a wrapper around docker for easier use of the docker stack -in this project. - -Signature: - ./stack [options] - - -Usage: - ./stack - ./stack [-d, --detach] [-h, --help] [--http] [--https] [-u, --user] - - -Options: - -d, --detach Runs the docker container detached from the terminal. - $ ./stack --detach up - - -h, --help Shows this help. - $ ./stack --help - - --http The HTTP port map host:container. - Defaults to use port map 5000:5000. - $ ./stack --http 8000:5000 up - - --https The HTTPS port map host:container. - Defaults to use port map 5443:5443. - $ ./stack --https 8443:5443 up - - -u, --user Run the docker container under the given user name or uid. - $ ./stack --user root shell - -Commands/Args: - build Builds the docker image for this stack. - $ ./stack build - - down Stops and removes the docker container. - $ ./stack down - - up Starts the docker container with the Java server running. - $ ./stack up - $ ./stack --detach up - $ ./stack --detach --https 8443:5443 up - $ ./stack --http 8000:5000 --https 8443:5443 up - - shell Starts a shell in the docker container: - $ ./stack shell - $ ./stack shell bash - $ ./stack --http 4000:5000 shell - $ ./stack --user root shell diff --git a/servers/shapes-api/docs/approov-shapes-api-server.md b/servers/shapes-api/docs/approov-shapes-api-server.md deleted file mode 100644 index d1ae0c1..0000000 --- a/servers/shapes-api/docs/approov-shapes-api-server.md +++ /dev/null @@ -1,537 +0,0 @@ -# APPROOV SHAPES API SERVER - -The Approov Shapes API Server contains endpoints with and without the Approov -protection. The protected endpoints differ in the sense that they can use or not -the optional token binding feature for the Approov token. - -We will demonstrate how to call each API endpoint with screen-shots from Postman -and from the shell terminal. 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 need to request a demo -[here](https://info.approov.io/demo). - -When presenting the screen-shots we will present them as 2 distinct views. The -Postman view will tell how we performed the request and what response we got -back and the shell view show us the log entries that lets us see the result of -checking the Approov token and how the requested was handled. - - -## REQUIREMENTS - -The same as we defined [here](README.md#requirements) when explain how to -integrate Approov. - - -## INSTALL - -### Approov Shapes Api Server - -Let's start by cloning the demo: - -```bash -git clone https://github.com/approov/quickstart-java-spring_shapes-api.git -cd quickstart-java-spring_shapes-api -``` - -### Development Environment - -In order to have an agnostic development environment through this tutorial we -recommend the use of Docker, that can be installed by following [the official -instructions](https://docs.docker.com/install/) for your platform. - -A bash script `./stack` is provided in the root of the demo to make easy to use -the docker stack to run this demo. - -Show the usage help with: - -```bash -./stack --help -``` - -The output: - -```bash -DOCKER STACK CLI WRAPPER - -This bash script is a wrapper around docker for easier use of the docker stack -in this project. - -Signature: - ./stack [options] - - -Usage: - ./stack - ./stack [-d, --detach] [-h, --help] [--http] [--https] [-u, --user] - - -Options: - -d, --detach Runs the docker container detached from the terminal. - $ ./stack --detach up - - -h, --help Shows this help. - $ ./stack --help - - --http The HTTP port map host:container. - Defaults to use port map 5000:5000. - $ ./stack --http 8000:5000 up - - --https The HTTPS port map host:container. - Defaults to use port map 5443:5443. - $ ./stack --https 8443:5443 up - - -u, --user Run the docker container under the given user name or uid. - $ ./stack --user root shell - -Commands/Args: - build Builds the docker image for this stack. - $ ./stack build - - down Stops and removes the docker container. - $ ./stack down - - up Starts the docker container with the Java server running. - $ ./stack up - $ ./stack --detach up - $ ./stack --detach --https 8443:5443 up - $ ./stack --http 8000:5000 --https 8443:5443 up - - shell Starts a shell in the docker container: - $ ./stack shell - $ ./stack shell bash - $ ./stack --http 4000:5000 shell - $ ./stack --user root shell -``` - -#### Building the docker image: - -```bash -./stack build -``` -> The resulting docker image will contain the Approov Shapes Demo Server in Java. - - -## SETUP - -### Environment File - -Lets' copy the `.env.example` to `.env` with the command: - -```bash -cp .env.example .env -``` - -No modifications are necessary to the newly created `.env` in order to run the -demo with the provided Postman collection. - -### Getting a shell terminal inside the docker container: - -```bash -./stack shell -``` -> All subsequent shell commands must be executed from this shell terminal. - -### Building the Project - -Lets' try to build the project: - -```bash -./gradlew build -``` - -The output: - -``` -# omitted output ... - -BUILD SUCCESSFUL in 33s -5 actionable tasks: 3 executed, 2 up-to-date -``` - -The build went successful, thus we are ready to start playing around with the -server. - -## RUNNING THE APPROOV SHAPES DEMO SERVER - -We will run this demo first with Approov enabled and a second time with Approov -disabled. When Approov is enabled any API endpoint protected by an Approov token -will have the request denied with a `400` or `401` response. When Approov is -disabled the check still takes place but no requests are denied, only the reason -for the failure is logged. - -### The logs - -When a request is issued from Postman you can see the logs being printed to your -shell terminal where you can see all log entries about requests protected by -Approov, and compare the logged messages with the results returned to Postman -for failures or success in the validation of the Approov token. - -An example for an accepted request: - -```bash -2019-04-26 11:42:30.503 INFO 17062 --- [nio-5000-exec-2] c.c.a.j.a.ApproovAuthentication : Request approved with a valid Approov token. -2019-04-26 11:42:30.503 INFO 17062 --- [nio-5000-exec-2] c.c.a.j.a.ApproovAuthentication : Request approved with a valid token binding in the Approov token. -2019-04-26 11:42:30.524 INFO 17062 --- [nio-5000-exec-2] c.c.a.j.a.ApproovAuthentication : Serving request for endpoint '/v2/forms', that is protect by an Approov Token. -``` - -Example for a rejected request: - -```bash -2019-04-26 11:43:43.083 ERROR 17062 --- [nio-5000-exec-5] c.c.a.j.a.ApproovAuthenticationException : JWT expired at 2019-04-12T15:35:49Z. Current time: 2019-04-26T10:43:43Z, a difference of 1192074083 milliseconds. Allowed clock skew: 0 milliseconds. -> See: com.criticalblue.approov.jwt.authentication.ApproovAuthentication.checkWith(ApproovAuthentication.java:98) - -``` - -### Starting Postman - -Open Postman and import [this collection](https://raw.githubusercontent.com/approov/postman-collections/master/quickstarts/shapes-api/shapes-api.postman_collection.json) -that contains all the API endpoints prepared with all scenarios we want to -demonstrate. - -### Starting the Java Server - -We do not use HTTPS and certificate pinning in this demo, because we are running in localhost, that would require us to provide self-signed certificates to start the Java server with HTTPS enabled, that is doable, but we also use Postman to show how the API works, and Postman does not support self-signed certificates. - -This server has `HTTPS` enabled with a self-signed certificate at -`src/main/java/resources/keystore/ApproovTLS.p12`, thus feel free to use it in -another tool that supports self-signed certicates, and then just hit the same -API endpoints hover `HTTPS`, or change the setting `HTTP_REDIRECT` to `true` in -the `.env` file. - -To start the server we want to issue the command: - -```bash -source .env && ./gradlew bootRun -``` - -> **NOTE**: -> -> If you decide to run the Java server from your IDE, then you need to set all -> the environments variables in the `.env` file in your IDE. - -After the Java server is up and running it will be available at http://localhost:8002. - -### Endpoint Not Protected by Approov - -This endpoint does not benefit from Approov protection and the goal here is to -show that both Approov protected and unprotected endpoints can coexist in the -same API server. - -#### /v2/hello - -**Postman View:** - -![postman hello endpoint](./assets/img/postman-hello.png) -> As we can see we have not set any headers. - -**Shell view:** - -![shell terminal hello endpoint](./assets/img/shell-hello.png) -> As expected the logs don't have entries with Approov errors. - - -**Request Overview:** - -Looking into the Postman view, we can see that the request was sent without the -`Approov-Token` header and we got a `200` response, and looking to the shell -view we can see a log entry telling that this is a endpoint not protected by an -Approov token. - -### Endpoints Protected by an Approov Token - -This endpoint requires a `Approov-Token` header and depending on the boolean -value for the environment variable `APPROOV_ABORT_REQUEST_ON_INVALID_TOKEN` we will -have 2 distinct behaviours. When being set to `true` we refuse to fulfill the -request and when set to `false` we will let the request pass through. For both -behaviours we log the result of checking the Approov token. - -The default behaviour is to have `APPROOV_ABORT_REQUEST_ON_INVALID_TOKEN` set to -`true`, but you may feel more comfortable to have it setted to `false` during -the initial deployment, until you are confident that you are only refusing bad -requests to your API server. - -#### /v2/shapes - missing the Approov token header - -Make sure that the `.env` file contains `APPROOV_ABORT_REQUEST_ON_INVALID_TOKEN` set to `true`. - -Cancel current server session with `ctrl+c` and start it again with: - -```bash -source .env && ./gradlew bootRun -``` - -**Postman view:** - -![Postman - shapes endpoint without an Approov token](./assets/img/postman-shapes-missing-approov-token.png) -> As we can see we have not set any headers. - -**Shell view:** - -![Shell - shapes endpoint without an Approov token](./assets/img/shell-shapes-missing-approov-token.png) -> No log entries for Approov exceptions in this request? - -**Request Overview:** - -Looking to the Postman view we can see that we forgot to add the `Approov-Token` -header, thus a `400` response is returned. - -In the shell view we can also see that we have a `400` response, and that the associated exception doesn't belong to the -Approov implementation, instead its from the Java Spring framework security authentication. - -**Let's see the same request with Approov disabled** - -Make sure that the `.env` file contains `APPROOV_ABORT_REQUEST_ON_INVALID_TOKEN` set to `false`. - -Cancel current server session with `ctrl+c` and start it again with: - -```bash -source .env && ./gradlew bootRun -``` - -**Postman view:** - -![Postman - shapes endpoint without an Approov token and approov disabled](./assets/img/postman-shapes-missing-approov-token-and-approov-disabled.png) -> Did you notice that now we have a response with a shape? - -**Shell view:** - -![Shell - shapes endpoint without an Approov token and approov disabled](./assets/img/shell-shapes-missing-approov-token-and-approov-disabled.png) -> Now we have some logs entries from the Approov authentication. - -**Request Overview:** - -We continue to not provide the `Approov-Token` header but this time we have a -`200` response with the value for the shape, because once Approov is disabled the -request is not denied. - -Looking into the shell view we can see a log entry informing that the Approov token is missing in the request, but now -we can see another log entry for the `/v2/shapes` endpoint, that says the request was fulfilled. - - -#### /v2/shapes - Malformed Approov token header - -Make sure that the `.env` file contains `APPROOV_ABORT_REQUEST_ON_INVALID_TOKEN` set to `true`. - -Cancel current server session with `ctrl+c` and start it again with: - -```bash -source .env && ./gradlew bootRun -``` - -**Postman view:** - -![Postman - shapes endpoint with an invalid Approov token](./assets/img/postman-shapes-malformed-approov-token.png) -> Did you notice the `Approov-Token` with an invalid JWT token? - -**Shell view:** - -![Shell - shapes endpoint with an invalid Approov token](./assets/img/shell-shapes-malformed-approov-token.png) -> Can you spot what is the reason for the `401` response? - -**Request Overview:** - -In Postman we issue the request with a malformed `Approov-Token` header, that is -a normal string, not a JWT token, thus we get back a `401` response. - -Looking to shell view we can see that the logs is also telling us that the -request was denied with a `401` and that the reason is an invalid JWT token, -that doesn't contain exactly 2 periods `.` characters. - - -**Let's see the same request with Approov disabled** - -Make sure that the `.env` file contains `APPROOV_ABORT_REQUEST_ON_INVALID_TOKEN` set to `false`. - -Cancel current server session with `ctrl+c` and start it again with: - -```bash -source .env && ./gradlew bootRun -``` - -**Postman view:** - -![Postman - shapes endpoint with an invalid Approov token and approov disabled](./assets/img/postman-shapes-malformed-approov-token-and-approov-disabled.png) - - -**Shell view:** - -![Shell - shapes endpoint with an invalid Approov token and approov disabled](./assets/img/shell-shapes-malformed-approov-token-and-approov-disabled.png) - - -**Request Overview:** - -In Postman, instead of sending a valid JWT token, we continue to send the -`Approov-Token` header as a normal string, but this time we got a `200` response -back because Approov is disabled, thus not blocking the request. - -In the shell view we continue to see the same reason for the Approov token -validation failure. - - -#### /v2/shapes - Valid Approov token header - -Make sure that the `.env` file contains `APPROOV_ABORT_REQUEST_ON_INVALID_TOKEN` set to `true`. - -Cancel current server session with `ctrl+c` and start it again with: - -```bash -source .env && ./gradlew bootRun -``` - -> **NOTE**: -> -> For your convenience the Postman collection includes a token that only expires -> in a 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 token correctly signed and not expired token:** - -![Postman - shapes endpoint with a valid Approov token](./assets/img/postman-shapes-valid-approov-token.png) - -**Postman view with token correctly signed but this time is expired:** - -![Postman - shapes endpoint with a expired Approov token](./assets/img/postman-shapes-expired-approov-token.png) - - -**Shell view:** - -![Shell - shapes endpoint with a valid and with a expired Approov token](./assets/img/shell-shapes-valid-and-expired-token.png) - - -**Request Overview:** - -In Postman we performed 2 requests with correctly signed Approov tokens, where the first one was successful while the second request failed with a `401` response. This was because the token in the second request as already expired as we can see by the log messages in the shell view. A token expires when the timestamp contained in the payload claim `exp` is in the past. - - -**Let's see the same request with Approov disabled** - -Make sure that the `.env` file contains `APPROOV_ABORT_REQUEST_ON_INVALID_TOKEN` set to `false`. - -Cancel current server session with `ctrl+c` and start it again with: - -```bash -source .env && ./gradlew bootRun -``` -**Postman view with token valid for 1 minute:** - -![Postman - shapes endpoint with a valid Approov token and Approov disabled](./assets/img/postman-shapes-valid-approov-token-and-approov-disabled.png) - -**Postman view with same token but this time is expired:** - -![Postman - shapes endpoint with a expired Approov token and Approov disabled](./assets/img/postman-shapes-expired-approov-token-and-approov-disabled.png) - -**Shell view:** - -![Shell - shapes endpoint with a valid, with an expired Approov token and Approov disabled](./assets/img/shell-shapes-approov-disabled-with-valid-and-expired-token.png) -> Can you spot where is the difference between this shell view and the previous one? - -**Request Overview:** - -We repeated the same two requests, but this time we got both of them with `200` responses. - -If we look into the shell view we can see that the first request have -a valid token and in the second request the token is not valid because is -expired, but once Approov is disabled the request is accepted. - -### Endpoints Protected with the Approov Token Binding - -The token binding is optional in any Approov token and you can read more about them [here](./../README.md#approov-validation-process). - -The requests where the Approov token binding is checked will be rejected on failure, but -only if the environment variable `APPROOV_ABORT_REQUEST_ON_INVALID_TOKEN_BINDING` -is set to `true`. To bear in mind that before this check is done the request -have already been through the same flow we have described for the `/v2/shapes` endpoint. - - -#### /v2/forms - Invalid Approov Token Binding - -Make sure that the `.env` file contains `APPROOV_ABORT_REQUEST_ON_INVALID_TOKEN_BINDING` set to `true`. - -Cancel current server session with `ctrl+c` and start it again with: - -```bash -source .env && ./gradlew bootRun -``` - -**Postman view:** - -![Postman - forms endpoint with an invalid Approov token binding](./assets/img/postman-forms-invalid-approov-token-binding.png) - -**Shell view:** - -![Shell - forms endpoint with an invalid Approov token binding](./assets/img/shell-forms-invalid-approov-token-binding.png) - -**Request Overview:** - -In Postman we added an Approov token with a token binding not matching the -`Authorization` token, thus the API server rejects the request with a `401` response. - -While we can see in the shell view that the request is accepted for the Approov -token itself, afterwards we see the request being rejected, and this is due to -an invalid token binding in the Approov token, thus returning a `401` response. - -> **IMPORTANT**: -> -> When decoding the Approov token we only check if the signature and expiration -> time are valid, nothing else within the token is checked. -> -> The token binding check works on the decoded Approov token to validate if the -> value from the key `pay` matches the one for the token binding header, that in -> our case is the `Authorization` header. - - -**Let's see the same request with Approov disabled** - -Make sure that the `.env` file contains `APPROOV_ABORT_REQUEST_ON_INVALID_TOKEN_BINDING` set to `false`. - -Cancel current server session with `ctrl+c` and start it again with: - -```bash -source .env && ./gradlew bootRun -``` - -**Postman view:** - -![Postman - forms endpoint with an invalid Approov token binding](./assets/img/postman-forms-invalid-approov-token-binding-with-approov-disabled.png) - -**Shell view:** - -![Shell - forms endpoint with an invalid Approov token binding](./assets/img/shell-forms-invalid-approov-token-binding-with-approov-disabled.png) - -**Request Overview:** - -We still have the invalid token binding in the Approov token, but once we have -disabled Approov we now have a `200` response. - -In the shell view we can confirm that the log entry still reflects that the -token binding is invalid, but this time a `401` response is not logged, and -this is because Approov is now disabled. - - -#### /v2/forms - Valid Approov Token Binding - -Make sure that the `.env` file contains `APPROOV_ABORT_REQUEST_ON_INVALID_TOKEN_BINDING` set to `true`. - -Cancel current server session with `ctrl+c` and start it again with: - -```bash -source .env && ./gradlew bootRun -``` - -**Postman view:** - -![Postman - forms endpoint with valid Approov Token Binding](./assets/img/postman-forms-valid-approov-token-binding.png) - -**Shell view:** - -![Shell - forms endpoint with valid Approov Token Binding](./assets/img/shell-forms-valid-approov-token-binding.png) - -**Request Overview:** - -In the Postman view the `Approov-Token` contains a valid token binding, the -`Authorization` token value, thus when we perform the request, the API server -doesn't reject it, and a `200` response is sent back. - -The shell view confirms us that the token binding is valid and we can also see -the log entry confirming the `200` response. diff --git a/servers/shapes-api/docs/assets/img/postman-forms-invalid-approov-token-binding-with-approov-disabled.png b/servers/shapes-api/docs/assets/img/postman-forms-invalid-approov-token-binding-with-approov-disabled.png deleted file mode 100644 index c2edac3..0000000 Binary files a/servers/shapes-api/docs/assets/img/postman-forms-invalid-approov-token-binding-with-approov-disabled.png and /dev/null differ diff --git a/servers/shapes-api/docs/assets/img/postman-forms-invalid-approov-token-binding.png b/servers/shapes-api/docs/assets/img/postman-forms-invalid-approov-token-binding.png deleted file mode 100644 index a74d5e7..0000000 Binary files a/servers/shapes-api/docs/assets/img/postman-forms-invalid-approov-token-binding.png and /dev/null differ diff --git a/servers/shapes-api/docs/assets/img/postman-forms-valid-approov-token-binding.png b/servers/shapes-api/docs/assets/img/postman-forms-valid-approov-token-binding.png deleted file mode 100644 index 9f3c8be..0000000 Binary files a/servers/shapes-api/docs/assets/img/postman-forms-valid-approov-token-binding.png and /dev/null differ diff --git a/servers/shapes-api/docs/assets/img/postman-hello.png b/servers/shapes-api/docs/assets/img/postman-hello.png deleted file mode 100644 index e5c0b7c..0000000 Binary files a/servers/shapes-api/docs/assets/img/postman-hello.png and /dev/null differ diff --git a/servers/shapes-api/docs/assets/img/postman-shapes-expired-approov-token-and-approov-disabled.png b/servers/shapes-api/docs/assets/img/postman-shapes-expired-approov-token-and-approov-disabled.png deleted file mode 100644 index d93f909..0000000 Binary files a/servers/shapes-api/docs/assets/img/postman-shapes-expired-approov-token-and-approov-disabled.png and /dev/null differ diff --git a/servers/shapes-api/docs/assets/img/postman-shapes-expired-approov-token.png b/servers/shapes-api/docs/assets/img/postman-shapes-expired-approov-token.png deleted file mode 100644 index 75af44f..0000000 Binary files a/servers/shapes-api/docs/assets/img/postman-shapes-expired-approov-token.png and /dev/null differ diff --git a/servers/shapes-api/docs/assets/img/postman-shapes-malformed-approov-token-and-approov-disabled.png b/servers/shapes-api/docs/assets/img/postman-shapes-malformed-approov-token-and-approov-disabled.png deleted file mode 100644 index b6de92a..0000000 Binary files a/servers/shapes-api/docs/assets/img/postman-shapes-malformed-approov-token-and-approov-disabled.png and /dev/null differ diff --git a/servers/shapes-api/docs/assets/img/postman-shapes-malformed-approov-token.png b/servers/shapes-api/docs/assets/img/postman-shapes-malformed-approov-token.png deleted file mode 100644 index 40c33dc..0000000 Binary files a/servers/shapes-api/docs/assets/img/postman-shapes-malformed-approov-token.png and /dev/null differ diff --git a/servers/shapes-api/docs/assets/img/postman-shapes-missing-approov-token-and-approov-disabled.png b/servers/shapes-api/docs/assets/img/postman-shapes-missing-approov-token-and-approov-disabled.png deleted file mode 100644 index 8c22751..0000000 Binary files a/servers/shapes-api/docs/assets/img/postman-shapes-missing-approov-token-and-approov-disabled.png and /dev/null differ diff --git a/servers/shapes-api/docs/assets/img/postman-shapes-missing-approov-token.png b/servers/shapes-api/docs/assets/img/postman-shapes-missing-approov-token.png deleted file mode 100644 index ddf6ebb..0000000 Binary files a/servers/shapes-api/docs/assets/img/postman-shapes-missing-approov-token.png and /dev/null differ diff --git a/servers/shapes-api/docs/assets/img/postman-shapes-valid-approov-token-and-approov-disabled.png b/servers/shapes-api/docs/assets/img/postman-shapes-valid-approov-token-and-approov-disabled.png deleted file mode 100644 index e03a8a8..0000000 Binary files a/servers/shapes-api/docs/assets/img/postman-shapes-valid-approov-token-and-approov-disabled.png and /dev/null differ diff --git a/servers/shapes-api/docs/assets/img/postman-shapes-valid-approov-token.png b/servers/shapes-api/docs/assets/img/postman-shapes-valid-approov-token.png deleted file mode 100644 index 2033c96..0000000 Binary files a/servers/shapes-api/docs/assets/img/postman-shapes-valid-approov-token.png and /dev/null differ diff --git a/servers/shapes-api/docs/assets/img/shell-forms-invalid-approov-token-binding-with-approov-disabled.png b/servers/shapes-api/docs/assets/img/shell-forms-invalid-approov-token-binding-with-approov-disabled.png deleted file mode 100644 index 3a092e3..0000000 Binary files a/servers/shapes-api/docs/assets/img/shell-forms-invalid-approov-token-binding-with-approov-disabled.png and /dev/null differ diff --git a/servers/shapes-api/docs/assets/img/shell-forms-invalid-approov-token-binding.png b/servers/shapes-api/docs/assets/img/shell-forms-invalid-approov-token-binding.png deleted file mode 100644 index 9a9da5d..0000000 Binary files a/servers/shapes-api/docs/assets/img/shell-forms-invalid-approov-token-binding.png and /dev/null differ diff --git a/servers/shapes-api/docs/assets/img/shell-forms-valid-approov-token-binding.png b/servers/shapes-api/docs/assets/img/shell-forms-valid-approov-token-binding.png deleted file mode 100644 index 1761c7c..0000000 Binary files a/servers/shapes-api/docs/assets/img/shell-forms-valid-approov-token-binding.png and /dev/null differ diff --git a/servers/shapes-api/docs/assets/img/shell-hello.png b/servers/shapes-api/docs/assets/img/shell-hello.png deleted file mode 100644 index 1835234..0000000 Binary files a/servers/shapes-api/docs/assets/img/shell-hello.png and /dev/null differ diff --git a/servers/shapes-api/docs/assets/img/shell-shapes-approov-disabled-with-valid-and-expired-token.png b/servers/shapes-api/docs/assets/img/shell-shapes-approov-disabled-with-valid-and-expired-token.png deleted file mode 100644 index 29d759f..0000000 Binary files a/servers/shapes-api/docs/assets/img/shell-shapes-approov-disabled-with-valid-and-expired-token.png and /dev/null differ diff --git a/servers/shapes-api/docs/assets/img/shell-shapes-malformed-approov-token-and-approov-disabled.png b/servers/shapes-api/docs/assets/img/shell-shapes-malformed-approov-token-and-approov-disabled.png deleted file mode 100644 index 0d90377..0000000 Binary files a/servers/shapes-api/docs/assets/img/shell-shapes-malformed-approov-token-and-approov-disabled.png and /dev/null differ diff --git a/servers/shapes-api/docs/assets/img/shell-shapes-malformed-approov-token.png b/servers/shapes-api/docs/assets/img/shell-shapes-malformed-approov-token.png deleted file mode 100644 index be65d85..0000000 Binary files a/servers/shapes-api/docs/assets/img/shell-shapes-malformed-approov-token.png and /dev/null differ diff --git a/servers/shapes-api/docs/assets/img/shell-shapes-missing-approov-token-and-approov-disabled.png b/servers/shapes-api/docs/assets/img/shell-shapes-missing-approov-token-and-approov-disabled.png deleted file mode 100644 index 1d75c92..0000000 Binary files a/servers/shapes-api/docs/assets/img/shell-shapes-missing-approov-token-and-approov-disabled.png and /dev/null differ diff --git a/servers/shapes-api/docs/assets/img/shell-shapes-missing-approov-token.png b/servers/shapes-api/docs/assets/img/shell-shapes-missing-approov-token.png deleted file mode 100644 index 6274893..0000000 Binary files a/servers/shapes-api/docs/assets/img/shell-shapes-missing-approov-token.png and /dev/null differ diff --git a/servers/shapes-api/docs/assets/img/shell-shapes-valid-and-expired-token.png b/servers/shapes-api/docs/assets/img/shell-shapes-valid-and-expired-token.png deleted file mode 100644 index 586acea..0000000 Binary files a/servers/shapes-api/docs/assets/img/shell-shapes-valid-and-expired-token.png and /dev/null differ diff --git a/servers/shapes-api/gradle/wrapper/gradle-wrapper.jar b/servers/shapes-api/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 87b738c..0000000 Binary files a/servers/shapes-api/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/servers/shapes-api/gradle/wrapper/gradle-wrapper.properties b/servers/shapes-api/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index ab56c48..0000000 --- a/servers/shapes-api/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -#Tue Apr 02 13:15:36 BST 2019 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.2.1-all.zip diff --git a/servers/shapes-api/gradlew b/servers/shapes-api/gradlew deleted file mode 100755 index af6708f..0000000 --- a/servers/shapes-api/gradlew +++ /dev/null @@ -1,172 +0,0 @@ -#!/usr/bin/env sh - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m"' - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn () { - echo "$*" -} - -die () { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=$((i+1)) - done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=$(save "$@") - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" - -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi - -exec "$JAVACMD" "$@" diff --git a/servers/shapes-api/gradlew.bat b/servers/shapes-api/gradlew.bat deleted file mode 100644 index 0f8d593..0000000 --- a/servers/shapes-api/gradlew.bat +++ /dev/null @@ -1,84 +0,0 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/servers/shapes-api/settings.gradle b/servers/shapes-api/settings.gradle deleted file mode 100644 index 67a9126..0000000 --- a/servers/shapes-api/settings.gradle +++ /dev/null @@ -1,6 +0,0 @@ -pluginManagement { - repositories { - gradlePluginPortal() - } -} -rootProject.name = 'approov-jwt' diff --git a/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/ApiController.java b/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/ApiController.java deleted file mode 100644 index 58e28e9..0000000 --- a/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/ApiController.java +++ /dev/null @@ -1,127 +0,0 @@ -package com.criticalblue.approov.jwt; - -import com.criticalblue.approov.jwt.authentication.ApproovAuthentication; -import com.criticalblue.approov.jwt.dto.Forms; -import com.criticalblue.approov.jwt.dto.Hello; -import com.criticalblue.approov.jwt.dto.Shapes; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; -import javax.servlet.http.HttpServletRequest; - -@RestController -public class ApiController { - - private static Logger logger = LoggerFactory.getLogger(ApproovAuthentication.class); - private RandomShape randomShape = new RandomShape(); - private RandomForm randomForm = new RandomForm(); - - @GetMapping("/") - public String homePage() { - - logger.info("Serving request for endpoint '/', that is not protect by an Approov Token."); - - // This endpoint is not protected by an Approov token, thus the Approov Authentication does not take place, and - // everything works as usual. - - return "\n" + - "\n" + - " \n" + - "

Approov Mobile App Authentication

\n" + - "

\n" + - " To learn more about how Approov protects your APIs from malicious bots and tampered or fake apps, see https://approov.io/docs.\n" + - "

\n" + - " \n" + - ""; - } - - @GetMapping("/v1/hello") - public Hello helloV1() { - - logger.info("Serving request for endpoint '/v1/hello', that is not protect by an Approov Token."); - - return new Hello(); - } - - @GetMapping("/v1/shapes") - public Shapes shapesV1() { - - logger.info("Serving request for endpoint '/v1/shapes', that is not protect by an Approov Token."); - - String shape = randomShape.create(); - return new Shapes(shape); - } - - @GetMapping("/v1/forms") - public Forms formsV1(HttpServletRequest request) { - - logger.info("Serving request for endpoint '/v1/forms', that is not protect by an Approov Token."); - - String form = randomForm.create(); - return new Forms(form); - } - - @GetMapping("/v2/hello") - public Hello helloV2() { - - logger.info("Serving request for endpoint '/v2/hello', that is not protect by an Approov Token."); - - // This endpoint is not protected by an Approov token, thus the Approov Authentication does not take place, and - // everything works as usual. - - return new Hello(); - } - - @GetMapping("/v2/shapes") - public Shapes shapesV2() { - - logger.info("Serving request for endpoint '/v2/shapes', that is protect by an Approov Token."); - - // This endpoint is protected by an Approov token that MUST be signed with the secret shared between Arppoov and - // the API server, and an exception will be raised if the Approov token is not signed with it, or its - // expiration of 5 minutes have been exceeded. - // - // The shared secret is the one declared in the .env file in the var `APPROOV_BASE64_SECRET`, and you retrieve - // it with the Approov CLI tool. For this demo purpose we will test the API with a tool like Postman, thus - // we can generate some Approov tokens with the help of the Approov CLI tool. - // - // The raised exception will be: com.criticalblue.approov.jwt.authentication.ApproovAuthenticationException. - // - // Throwing the ApproovAuthenticationException on an invalid Approov token is controlled by the environment - // variable `APPROOV_ABORT_REQUEST_ON_INVALID_TOKEN`, that can be found in the .env file. Setting its value to - // `false` should only be used when you want to limit the response returned to the client, instead of totally - // blocking it, but once Approov uses a positive attestation model(no false positives), we discourage this - // approach, unless in an initial phase where you just want to assert that everything works as you intend to. - - String shape = randomShape.create(); - return new Shapes(shape); - } - - @GetMapping("/v2/forms") - public Forms formsV2(HttpServletRequest request) { - - logger.info("Serving request for endpoint '/v2/forms', that is protect by an Approov Token."); - - // This endpoint is protected by an Approov token, where in addition to what is checked in `/shapes` endpoint, a - // check is also performed to see if contains a key named `pay` in the Approov token payload section. When the - // value in this key does not match the request claim value, an exception will be raised, but you cannot catch - // it here, once its thrown before reaching this method. - // - // The exception will be: com.criticalblue.approov.jwt.authentication.ApproovTokenBindingAuthenticationException. - // - // The value in the `pay` key is a base64 encoded hash(SHA256) of the Approov header claim value, like an - // Authorization token. So the Approov header claim value is retrieved from an header that is configurable from - // the .env file, by changing the default value `Authorization` in the var `APPROOV_TOKEN_BINDING_HEADER_NAME`. - // - // Throwing the ApproovTokenBindingAuthenticationException on an invalid custom payload claim is controlled by the - // environment variable `APPROOV_ABORT_REQUEST_ON_INVALID_TOKEN_BINDING`, that can be found int the .env - // file. Setting its value to `false` should only be used when you want to limit the response returned to the - // client, instead of totally blocking it, but once Approov uses a positive attestation model(no false - // positives), we discourage this approach, unless in an initial phase where you just want to assert that - // everything works as you intend to. - - String form = randomForm.create(); - return new Forms(form); - } -} diff --git a/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/Application.java b/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/Application.java deleted file mode 100644 index c547b68..0000000 --- a/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/Application.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.criticalblue.approov.jwt; - -import org.apache.catalina.Context; -import org.apache.catalina.connector.Connector; -import org.apache.tomcat.util.descriptor.web.SecurityCollection; -import org.apache.tomcat.util.descriptor.web.SecurityConstraint; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; -import org.springframework.boot.web.servlet.server.ServletWebServerFactory; -import org.springframework.context.annotation.Bean; - -@SpringBootApplication -public class Application { - - private static Logger logger = LoggerFactory.getLogger(Application.class); - - @Value("${http.port}") - private int httpPort; - - @Value("${https.port}") - private int httpsPort; - - @Value("${http.redirect}") - private boolean isToRedirectHttp; - - public static void main(String[] args) { - SpringApplication.run(Application.class, args); - } - - @Bean - public ServletWebServerFactory servletContainer() { - - TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory() { - - @Override - protected void postProcessContext(Context context) { - if (isToRedirectHttp) { - logger.info("Creating security constrain to redirect http to https."); - SecurityConstraint securityConstraint = new SecurityConstraint(); - securityConstraint.setUserConstraint("CONFIDENTIAL"); - SecurityCollection collection = new SecurityCollection(); - collection.addPattern("/*"); - securityConstraint.addCollection(collection); - context.addConstraint(securityConstraint); - } - } - }; - - tomcat.addAdditionalTomcatConnectors(createConnector()); - return tomcat; - } - - private Connector createConnector() { - Connector connector = new Connector(TomcatServletWebServerFactory.DEFAULT_PROTOCOL); - - connector.setScheme("http"); - connector.setPort(httpPort); - connector.setSecure(false); - - if (isToRedirectHttp) { - logger.info("Redirecting http to port: {}", httpsPort); - connector.setRedirectPort(httpsPort); - } - - return connector; - } -} diff --git a/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/CustomServletErrorAttributes.java b/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/CustomServletErrorAttributes.java deleted file mode 100644 index ef0b726..0000000 --- a/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/CustomServletErrorAttributes.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.criticalblue.approov.jwt; - -import org.springframework.boot.web.servlet.error.DefaultErrorAttributes; -import org.springframework.stereotype.Component; -import org.springframework.web.context.request.WebRequest; -import java.util.Map; - -@Component -public class CustomServletErrorAttributes extends DefaultErrorAttributes { - - @Override - public Map getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) { - - Map errorAttributes = super.getErrorAttributes(webRequest, includeStackTrace); - - // Remove from response in order to make the response comply with the Shapes API specification - errorAttributes.remove("timestamp"); - errorAttributes.remove("message"); - errorAttributes.remove("path"); - errorAttributes.remove("error"); - errorAttributes.remove("trace"); - errorAttributes.remove("status"); - - return errorAttributes; - } -} diff --git a/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/RandomForm.java b/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/RandomForm.java deleted file mode 100644 index b3c624f..0000000 --- a/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/RandomForm.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.criticalblue.approov.jwt; - -import java.util.ArrayList; -import java.util.List; -import java.util.Random; - -public class RandomForm { - - private final Random random = new Random(); - - private static List list = new ArrayList(); - - static { - list.add("Sphere"); - list.add("Cone"); - list.add("Cube"); - list.add("Box"); - } - - public String create() { - - int index = random.nextInt(list.size()); - return list.get(index); - } -} diff --git a/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/RandomShape.java b/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/RandomShape.java deleted file mode 100644 index 40b06f9..0000000 --- a/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/RandomShape.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.criticalblue.approov.jwt; - -import java.util.ArrayList; -import java.util.List; -import java.util.Random; - -public class RandomShape { - private final Random random = new Random(); - - private static List list = new ArrayList(); - - static { - list.add("Circle"); - list.add("Triangle"); - list.add("Square"); - list.add("Rectangle"); - } - - public String create() { - - int index = random.nextInt(list.size()); - return list.get(index); - } -} diff --git a/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/WebSecurityConfig.java b/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/WebSecurityConfig.java deleted file mode 100644 index 8560927..0000000 --- a/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/WebSecurityConfig.java +++ /dev/null @@ -1,122 +0,0 @@ -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(); - } - } -} diff --git a/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovAuthentication.java b/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovAuthentication.java deleted file mode 100644 index a274c95..0000000 --- a/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovAuthentication.java +++ /dev/null @@ -1,161 +0,0 @@ -package com.criticalblue.approov.jwt.authentication; - -import java.util.Collection; -import java.util.Collections; - -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.JwtException; -import io.jsonwebtoken.Jwts; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.springframework.http.HttpStatus; -import org.springframework.security.core.GrantedAuthority; - - -/** - * Validates the Approov Token is signed with the shared secret between Approov and the API server, that have not - * expired, and optionally also validates the token binding in the Approov token matches the token binding header. - * - * @see ApproovAuthenticationProvider - * @see ApproovSecurityContextRepository - */ -public class ApproovAuthentication implements ApproovJwtAuthentication { - - private static Logger logger = LoggerFactory.getLogger(ApproovAuthentication.class); - - private final ApproovTokenBindingAuthentication approovPayload = new ApproovTokenBindingAuthentication(); - - private final ApproovConfig approovConfig; - - private Claims approovTokenPayloadClaims; - - private final String tokenBindingHeader; - - private String approovToken; - - private final boolean checkTokenBinding; - - private boolean isAuthenticated = false; - - private boolean validTokenBinding; - - /** - * Constructs the Approov Authentication instance that will validate hte Approov token and the token binding. - * - * @param approovConfig Extracted from the .env file in the root of the package. - * @param approovToken Extracted from the header `Approov-Token`. - * @param checkTokenBinding When to check or not the token binding in the Approov token. - * @param tokenBindingHeader Extracted by default from the request header `Authorization`. - */ - ApproovAuthentication(ApproovConfig approovConfig, String approovToken, boolean checkTokenBinding, String tokenBindingHeader) { - this.approovConfig = approovConfig; - this.approovToken = approovToken; - this.checkTokenBinding = checkTokenBinding; - this.tokenBindingHeader = tokenBindingHeader; - } - - @Override - public void checkWith(byte[] approovSecret) throws ApproovAuthenticationException { - - if (approovSecret == null) { - throw new ApproovAuthenticationException("The Approov secret is null.", HttpStatus.INTERNAL_SERVER_ERROR.value()); - } - - if (approovToken == null) { - - logger.info("Request missing the Approov token."); - - if (approovConfig.isToAbortRequestOnInvalidToken()) { - throw new ApproovAuthenticationException("The Approov token is null.", HttpStatus.FORBIDDEN.value()); - } - - return; - } - - approovToken = approovToken.trim(); - - if (approovToken.equals("")) { - - if (approovConfig.isToAbortRequestOnInvalidToken()) { - throw new ApproovAuthenticationException("The Approov token is empty.", HttpStatus.BAD_REQUEST.value()); - } - - return; - } - - try { - - approovTokenPayloadClaims = Jwts.parser() - .setSigningKey(approovSecret) - .parseClaimsJws(approovToken) - .getBody(); - - logger.info("Request approved with a valid Approov token."); - - } catch (JwtException e) { - if (approovConfig.isToAbortRequestOnInvalidToken()) { - logger.info("INFO: " + e.getMessage()); - throw new ApproovAuthenticationException(e.getMessage(), HttpStatus.UNAUTHORIZED.value()); - } - - logger.warn("Request approved, but with an invalid Approov token: {}", e.getMessage()); - - return; - } - - if (checkTokenBinding) { - validTokenBinding = approovPayload.checkClaimMatchesFor(tokenBindingHeader, approovTokenPayloadClaims, approovConfig); - } - - isAuthenticated = true; - } - - @Override - public Claims getApproovTokenPayloadClaims() { - return approovTokenPayloadClaims; - } - - @Override - public boolean isValidTokenBinding() { - return validTokenBinding; - } - - @Override - public Collection getAuthorities() { - return Collections.emptyList(); - } - - @Override - public Object getCredentials() { - return approovToken; - } - - @Override - public Object getDetails() { - return approovTokenPayloadClaims; - } - - @Override - public Object getPrincipal() { - return null; - } - - @Override - public boolean isAuthenticated() { - return isAuthenticated; - } - - @Override - public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { - if (isAuthenticated) { - throw new ApproovAuthenticationException("A new Approov Authentication instance needs to be created to set this.isAuthenticated.", HttpStatus.INTERNAL_SERVER_ERROR.value()); - } - } - - @Override - public String getName() { - return null; - } -} diff --git a/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovAuthenticationEntryPoint.java b/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovAuthenticationEntryPoint.java deleted file mode 100644 index 494c792..0000000 --- a/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovAuthenticationEntryPoint.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.criticalblue.approov.jwt.authentication; - -import java.io.IOException; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.springframework.http.HttpStatus; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.web.AuthenticationEntryPoint; - - -/** - * When a failure occurs during the Approov token authentication process, an exception is thrown and Spring redirects - * to an authentication entry point, that have been configured in the Sring security to be this one. - * - * @see com.criticalblue.approov.jwt.WebSecurityConfig - * @see ApproovAuthentication - * @see ApproovTokenBindingAuthentication - */ -public class ApproovAuthenticationEntryPoint implements AuthenticationEntryPoint { - - private final static Logger logger = LoggerFactory.getLogger(ApproovAuthenticationEntryPoint.class); - - @Override - public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { - int httpStatusCode = HttpStatus.BAD_REQUEST.value(); - - if (authException instanceof ApproovException) { - httpStatusCode = ((ApproovException) authException).getHttpStatusCode(); - } - - final String httpStatusMessage = String.valueOf(HttpStatus.valueOf(httpStatusCode)); - final String exceptionType = String.valueOf(authException.getClass()); - final String exceptionMessage = authException.getMessage(); - - logger.info(httpStatusMessage + " | " + exceptionType + " | " + exceptionMessage); - response.sendError(httpStatusCode); - } -} diff --git a/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovAuthenticationException.java b/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovAuthenticationException.java deleted file mode 100644 index 6e695df..0000000 --- a/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovAuthenticationException.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.criticalblue.approov.jwt.authentication; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.springframework.http.HttpStatus; -import org.springframework.security.core.AuthenticationException; -import org.springframework.web.bind.annotation.ResponseStatus; - -/** - * Custom exception for failures when verifying the Approov token signature and expiration time. - * - * This exception is only thrown when `APPROOV_ABORT_REQUEST_ON_INVALID_TOKEN` is set to `true` in the .env file at the - * root of the project. - * - * @see ApproovAuthentication - */ -class ApproovAuthenticationException extends AuthenticationException implements ApproovException { - - private final static Logger logger = LoggerFactory.getLogger(ApproovAuthenticationException.class); - - private final int httpStatusCode; - - public ApproovAuthenticationException(String msg, int httpStatusCode) { - - super(msg); - - this.httpStatusCode = httpStatusCode; - - logger.error( msg + " -> See: " + getStackTrace()[0].toString()); - } - - public int getHttpStatusCode() { - return this.httpStatusCode; - } -} diff --git a/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovAuthenticationProvider.java b/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovAuthenticationProvider.java deleted file mode 100644 index 1ed431a..0000000 --- a/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovAuthenticationProvider.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.criticalblue.approov.jwt.authentication; - -import org.apache.tomcat.util.codec.binary.Base64; - -import org.jetbrains.annotations.NotNull; - -import org.springframework.security.authentication.AuthenticationProvider; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; - -/** - * Used to configure the Spring framework security with the trigger for the Approov Authentication. - * - * @see com.criticalblue.approov.jwt.WebSecurityConfig - * @see ApproovAuthentication - */ -public class ApproovAuthenticationProvider implements AuthenticationProvider { - - private final byte[] approovSecret; - - /** - * Constructs the Approov Authentication provider with an instance of the Approov config. - * - * @param approovConfig Extracted from the .env file in the root of the package. - */ - public ApproovAuthenticationProvider(ApproovConfig approovConfig) { - this.approovSecret = Base64.decodeBase64(approovConfig.getApproovBase64Secret()); - } - - @Override - public boolean supports(Class authentication) { - return ApproovJwtAuthentication.class.isAssignableFrom(authentication); - } - - @Override - public Authentication authenticate(@NotNull Authentication authentication) throws AuthenticationException { - - if (!supports(authentication.getClass())) { - return null; - } - - ApproovJwtAuthentication approovTokenAuthentication = (ApproovJwtAuthentication) authentication; - - approovTokenAuthentication.checkWith(approovSecret); - - return approovTokenAuthentication; - } -} diff --git a/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovConfig.java b/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovConfig.java deleted file mode 100644 index 4c19146..0000000 --- a/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovConfig.java +++ /dev/null @@ -1,87 +0,0 @@ -package com.criticalblue.approov.jwt.authentication; - -import org.springframework.http.HttpStatus; - -/** - * The Approov configuration that is built from the .env file in the root of the package. - */ -final public class ApproovConfig { - - private static ApproovConfig ourInstance = new ApproovConfig(); - - private String approovHeaderName = "Approov-Token"; - - private String approovBase64Secret; - - private final String approovTokenBindingHeaderName; - - private boolean toAbortRequestOnInvalidToken; - - private boolean toAbortRequestOnInvalidTokenBinding; - - /** - * Constructs the Approov Config singleton with values retrieved from the .env file in the root of the project. - */ - private ApproovConfig() { - this.approovBase64Secret = retrieveApproovBase64Secret(); - this.approovTokenBindingHeaderName = retrieveStringValueFromEnv("APPROOV_TOKEN_BINDING_HEADER_NAME", "Authorization"); - this.toAbortRequestOnInvalidToken = retrieveBooleanValueFromEnv("APPROOV_ABORT_REQUEST_ON_INVALID_TOKEN", true); - this.toAbortRequestOnInvalidTokenBinding = retrieveBooleanValueFromEnv("APPROOV_ABORT_REQUEST_ON_INVALID_TOKEN_BINDING", true); - } - - public static ApproovConfig getInstance() { - return ourInstance; - } - - String getApproovHeaderName() { - return approovHeaderName; - } - - String getApproovTokenBindingHeaderName() { - return approovTokenBindingHeaderName; - } - - String getApproovBase64Secret() { - return approovBase64Secret; - } - - boolean isToAbortRequestOnInvalidToken() { - return toAbortRequestOnInvalidToken; - } - - boolean isToAbortRequestOnInvalidTokenBinding() { - return toAbortRequestOnInvalidTokenBinding; - } - - private String retrieveApproovBase64Secret() { - approovBase64Secret = System.getenv("APPROOV_BASE64_SECRET"); - - if (approovBase64Secret == null) { - throw new ApproovAuthenticationException("Cannot retrieve APPROOV_BASE64_SECRET from the environment.", HttpStatus.INTERNAL_SERVER_ERROR.value()); - } - - return approovBase64Secret; - } - - private String retrieveStringValueFromEnv(String key, String defaultValue) { - - String value = System.getenv(key); - - if (value == null) { - return defaultValue; - } - - return value.trim(); - } - - private boolean retrieveBooleanValueFromEnv(String key, boolean defaultValue) { - - String value = System.getenv(key); - - if (value == null) { - return defaultValue; - } - - return value.trim().equalsIgnoreCase("true"); - } -} diff --git a/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovException.java b/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovException.java deleted file mode 100644 index 60b7576..0000000 --- a/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovException.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.criticalblue.approov.jwt.authentication; - -/** - * The Interface to be used in the Approov exceptions. - */ -public interface ApproovException { - - public int getHttpStatusCode(); -} diff --git a/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovJwtAuthentication.java b/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovJwtAuthentication.java deleted file mode 100644 index 5d5c68f..0000000 --- a/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovJwtAuthentication.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.criticalblue.approov.jwt.authentication; - -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.JwtException; - -import org.springframework.security.core.Authentication; - -/** - * The Interface to be used in the Approov authentication. - * - * @see ApproovAuthentication - */ -public interface ApproovJwtAuthentication extends Authentication { - - boolean isValidTokenBinding(); - - Claims getApproovTokenPayloadClaims(); - - void checkWith(byte[] secret) throws JwtException, ApproovAuthenticationException; -} diff --git a/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovSecurityContextRepository.java b/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovSecurityContextRepository.java deleted file mode 100644 index 19c9f1a..0000000 --- a/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovSecurityContextRepository.java +++ /dev/null @@ -1,91 +0,0 @@ -package com.criticalblue.approov.jwt.authentication; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContext; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.web.context.HttpRequestResponseHolder; -import org.springframework.security.web.context.SecurityContextRepository; - -/** - * Used to setup the Approov Authentication Context when configuring the Spring framework security. - * - * @see com.criticalblue.approov.jwt.WebSecurityConfig - */ -public class ApproovSecurityContextRepository implements SecurityContextRepository { - - private final boolean checkTokenBinding; - - private String approovToken = null; - - final private ApproovConfig approovConfig; - - /** - * Constructs with an instance of the Approov configuration, and with a boolean flag to indicate if is to check the - * token binding in the Approov token. - * - * @param approovConfig Extracted from the .env file in the root of the project. - * @param checkTokenBinding When to check or not the token binding in the Approov token. - */ - public ApproovSecurityContextRepository(ApproovConfig approovConfig, boolean checkTokenBinding) { - this.approovConfig = approovConfig; - this.checkTokenBinding = checkTokenBinding; - } - - @Override - public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) { - - String tokenBindingHeader = null; - - HttpServletRequest request = requestResponseHolder.getRequest(); - - SecurityContext context = SecurityContextHolder.createEmptyContext(); - - approovToken = request.getHeader(approovConfig.getApproovHeaderName()); - - if (approovToken == null && approovConfig.isToAbortRequestOnInvalidToken()) { - - // returning an empty security context in an endpoint protected by Approov, will cause Spring to later throw - // this exception: - // org.springframework.security.access.AccessDeniedException: Access is denied - return context; - } - - if (checkTokenBinding) { - tokenBindingHeader = getTokenBindingHeader(request); - } - - Authentication approovAuthentication = new ApproovAuthentication(approovConfig, approovToken, checkTokenBinding, tokenBindingHeader); - context.setAuthentication(approovAuthentication); - - return context; - } - - private String getTokenBindingHeader(HttpServletRequest request) { - - final String headerName = approovConfig.getApproovTokenBindingHeaderName(); - - if (headerName == null) { - return null; - } - - final String tokenBindingHeader = request.getHeader(headerName); - - if (tokenBindingHeader == null) { - return null; - } - - return tokenBindingHeader.trim(); - } - - @Override - public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) { - } - - @Override - public boolean containsContext(HttpServletRequest request) { - return approovToken != null; - } -} diff --git a/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovTokenBindingAuthentication.java b/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovTokenBindingAuthentication.java deleted file mode 100644 index 2971d38..0000000 --- a/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovTokenBindingAuthentication.java +++ /dev/null @@ -1,118 +0,0 @@ -package com.criticalblue.approov.jwt.authentication; - -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; - -import io.jsonwebtoken.Claims; - -import org.apache.tomcat.util.codec.binary.Base64; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.http.HttpStatus; -import org.springframework.security.authentication.AuthenticationServiceException; - -public class ApproovTokenBindingAuthentication { - - private static Logger logger = LoggerFactory.getLogger(ApproovAuthentication.class); - - /** - * Checks the value in the key `pay` of an Approov token matches the token binding header, that by default is - * the value for the `Authorization` header. - * - * @param tokenBindingHeader Extracted from an header, that by default is the Authorization header. - * @param approovTokenPayloadClaims Extracted from the already verified Approov token. - * @param approovConfig Extracted from the .env file in the root of the package. - * @return - */ - boolean checkClaimMatchesFor(String tokenBindingHeader, Claims approovTokenPayloadClaims, ApproovConfig approovConfig) { - - if (tokenBindingHeader == null && approovConfig.isToAbortRequestOnInvalidTokenBinding()) { - throw new ApproovTokenBindingAuthenticationException("The token binding header value is null.", HttpStatus.BAD_REQUEST.value()); - } - - final String approovTokenBindingClaim = getApproovTokenBindingClaim(approovTokenPayloadClaims, approovConfig); - - if (approovTokenBindingClaim == null) { - - logger.info("Request approved, but not able to check the token binding in the Approov token."); - - // When is `null`, it means we have not met yet a condition to fail the check when the claim is missing. - // @see getApproovTokenBindingClaim() for the conditions that will throw an exception. - return true; - } - - boolean isValidTokenBinding = getHashBase64Encoded(tokenBindingHeader).equals(approovTokenBindingClaim); - - if (isValidTokenBinding) { - logger.info("Request approved with a valid token binding in the Approov token."); - return isValidTokenBinding; - } - - // When the token binding header does not match the value in key `pay` of the Approov token, the request is - // aborted, but only if it is enabled in the Approov configuration. - if (approovConfig.isToAbortRequestOnInvalidTokenBinding()) { - throw new ApproovTokenBindingAuthenticationException("The token binding header does not match the key `pay` in the Approov token.", HttpStatus.UNAUTHORIZED.value()); - } - - logger.info("Request not approved, because the token binding header does not match the key `pay` in the Approov token."); - - return false; - } - - private String getApproovTokenBindingClaim(Claims approovTokenPayloadClaims, ApproovConfig approovConfig) { - - if (approovTokenPayloadClaims == null) { - - if (approovConfig.isToAbortRequestOnInvalidTokenBinding()) { - throw new ApproovTokenBindingAuthenticationException("Approov token payload is null.", HttpStatus.INTERNAL_SERVER_ERROR.value()); - } - - logger.warn("Approov token payload is null."); - - return null; - } - - if ( ! approovTokenPayloadClaims.containsKey("pay") ) { - - if (approovConfig.isToAbortRequestOnInvalidTokenBinding()) { - throw new ApproovTokenBindingAuthenticationException("The key `pay`, for the token binding, is missing in the Approov token payload.", HttpStatus.BAD_REQUEST.value()); - } - - logger.warn("The key `pay`, for the token binding, is missing in the Approov token payload."); - - // The Approov the token binding is optional, so we cannot throw an exception... - return null; - } - - final String approovTokenBindingClaim = approovTokenPayloadClaims.get("pay").toString(); - - if (approovTokenBindingClaim == null || approovTokenBindingClaim.trim().equals("")) { - - if (approovConfig.isToAbortRequestOnInvalidTokenBinding()) { - throw new ApproovTokenBindingAuthenticationException("The token binding in the Approov token is null or empty.", HttpStatus.BAD_REQUEST.value()); - } - - logger.warn("The token binding in the Approov token is null or empty."); - - return null; - } - - return approovTokenBindingClaim; - } - - private String getHashBase64Encoded(String value) { - - final MessageDigest digest; - - try { - digest = MessageDigest.getInstance("SHA-256"); - } catch (NoSuchAlgorithmException e) { - throw new AuthenticationServiceException(e.getMessage()); - } - - byte[] hash = digest.digest(value.getBytes(StandardCharsets.UTF_8)); - return Base64.encodeBase64String(hash); - } -} diff --git a/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovTokenBindingAuthenticationException.java b/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovTokenBindingAuthenticationException.java deleted file mode 100644 index 62dd601..0000000 --- a/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/authentication/ApproovTokenBindingAuthenticationException.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.criticalblue.approov.jwt.authentication; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.springframework.security.core.AuthenticationException; - -/** - * Custom exception for failures in the validation of the token binding in an Approov token. - * - * This exception is only thrown when `APPROOV_ABORT_REQUEST_ON_INVALID_TOKEN_BINDING` is set to `true` in the - * .env file at the root of the project. - * - * @see ApproovTokenBindingAuthentication - */ -class ApproovTokenBindingAuthenticationException extends AuthenticationException implements ApproovException { - - private final static Logger logger = LoggerFactory.getLogger(ApproovTokenBindingAuthenticationException.class); - private final int httpStatusCode; - - ApproovTokenBindingAuthenticationException(String msg, int httpStatusCode) { - - super(msg); - - this.httpStatusCode = httpStatusCode; - - logger.error( msg + " -> See: " + getStackTrace()[0].toString()); - } - - public int getHttpStatusCode() { - return httpStatusCode; - } -} diff --git a/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/dto/Forms.java b/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/dto/Forms.java deleted file mode 100644 index 5740756..0000000 --- a/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/dto/Forms.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.criticalblue.approov.jwt.dto; - -public class Forms { - - private final String form; - - public Forms(String form) { - this.form = form; - } - - public String getForm() { - return form; - } -} diff --git a/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/dto/Hello.java b/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/dto/Hello.java deleted file mode 100644 index 2480ca2..0000000 --- a/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/dto/Hello.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.criticalblue.approov.jwt.dto; - -public class Hello { - private final String text = ""; - - public String getText() { - return "Hello, World!"; - } -} diff --git a/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/dto/Shapes.java b/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/dto/Shapes.java deleted file mode 100644 index 9892347..0000000 --- a/servers/shapes-api/src/main/java/com/criticalblue/approov/jwt/dto/Shapes.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.criticalblue.approov.jwt.dto; - -public class Shapes { - - private final String shape; - - public Shapes(String shape) { - this.shape = shape; - } - - public String getShape() { - return shape; - } -} diff --git a/servers/shapes-api/src/main/resources/application.properties b/servers/shapes-api/src/main/resources/application.properties deleted file mode 100644 index cda3b69..0000000 --- a/servers/shapes-api/src/main/resources/application.properties +++ /dev/null @@ -1,41 +0,0 @@ -######################### -# SPRING CONFIGURATION -######################### - -spring.mvc.throw-exception-if-no-handler-found: true -spring.resources.add-mappings: false - - -######################### -# LOGGER CONFIGURATION -######################### - -logging.level.root: ERROR -logging.level.org.hibernate: ERROR -logging.level.org.springframework.web: ERROR -logging.level.org.springframework.security: ERROR -logging.level.com.criticalblue.approov: INFO - - -####################### -# HTTP CONFIGURATION -####################### - -# This vars need to be set in the .env file or in the environment -http.port: ${HTTP_PORT} -http.redirect: ${HTTP_REDIRECT} - - -####################### -# HTTPS CONFIGURATION -####################### - -# Needs to be set in the .env file or in the environment -server.port: ${HTTPS_PORT} - -# Self signed certificate was generated with: -# keytool -genkeypair -alias ApproovTLS -keyalg RSA -keysize 2048 -storetype PKCS12 -keystore ApproovTLS.p12 -validity 100000 -server.ssl.key-store-type: PKCS12 -server.ssl.key-store: classpath:keystore/ApproovTLS.p12 -server.ssl.key-store-password: supersecret -server.ssl.key-alias: ApproovTLS diff --git a/servers/shapes-api/src/main/resources/keystore/ApproovTLS.p12 b/servers/shapes-api/src/main/resources/keystore/ApproovTLS.p12 deleted file mode 100644 index 72a2163..0000000 Binary files a/servers/shapes-api/src/main/resources/keystore/ApproovTLS.p12 and /dev/null differ diff --git a/servers/shapes-api/src/test/java/com/criticalblue/approov/jwt/ApplicationTests.java b/servers/shapes-api/src/test/java/com/criticalblue/approov/jwt/ApplicationTests.java deleted file mode 100644 index 5606894..0000000 --- a/servers/shapes-api/src/test/java/com/criticalblue/approov/jwt/ApplicationTests.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.criticalblue.approov.jwt; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.junit4.SpringRunner; - -@RunWith(SpringRunner.class) -@SpringBootTest -public class ApplicationTests { - - @Test - public void contextLoads() { - } - -} diff --git a/servers/shapes-api/stack b/servers/shapes-api/stack deleted file mode 100755 index 072aa08..0000000 --- a/servers/shapes-api/stack +++ /dev/null @@ -1,224 +0,0 @@ -#!/bin/sh - -set -eu - -Show_Help() -{ - ############################################################################ - # EXECUTION - ############################################################################ - - echo && cat ./docker/usage-help.txt && echo -} - -Build_Docker_Image() -{ - ############################################################################ - # INPUT - ############################################################################ - - local docker_image="${1? Missing docker image name!!!}" - - - ############################################################################ - # EXECUTION - ############################################################################ - - sudo docker build \ - --pull \ - --tag "${docker_image}" \ - ./docker -} - -Docker_Container_Is_Running() -{ - ############################################################################ - # INPUT - ############################################################################ - - local container_name="${1? Missing container name!!!}" - - - ############################################################################ - # EXECUTION - ############################################################################ - - sudo docker container ls -a | grep -qw "${container_name}" - - - return $? -} - -Attach_To_App_Container() -{ - ############################################################################ - # INPUT - ############################################################################ - - local container_name="${1? Missing container name!!!}" - - local container_user="${2? Missing container user!!!}" - - local background_mode="${3? Missing backround mode to run the container!!!}" - - local run_command="${4? Missing command to run in the container!!!}" - - shift 4 - - - ############################################################################ - # EXECUTION - ############################################################################ - - sudo docker exec \ - --user ${container_user} \ - ${background_mode} \ - ${container_name} \ - ${run_command} ${@} -} - - -Start_Or_Attach_To_App_Container() -{ - ############################################################################ - # INPUT - ############################################################################ - - local container_name="${1? Missing container name !!!}" - - local container_user="${2? Missing container user !!!}" - - local http_port_map="${3? Missing the http port host:container !!!}" - - local https_port_map="${4? Missing the https port map host:container !!!}" - - local background_mode="${5? Missing backround mode to run the container !!!}" - - local docker_image="${6? Missing docker image name !!!}" - - local run_command="${7? Missing command to run in the docker container !!!}" - - shift 7 - - - ############################################################################ - # EXECUTION - ############################################################################ - - mkdir -p "${PWD}"/.local/.gradle - - if Docker_Container_Is_Running "${container_name}" ; then - Attach_To_App_Container \ - "${container_name}" \ - "${container_user}" \ - "${background_mode}" \ - "${run_command}" - "${@}" - - return - fi - - sudo docker run \ - -it \ - --rm \ - --user "${container_user}" \ - --env-file .env \ - --name "${container_name}" \ - --publish "127.0.0.1:${http_port_map}" \ - --publish "127.0.0.1:${https_port_map}" \ - --volume "$PWD:/home/java/workspace" \ - --volume "${PWD}/.local/.gradle:/home/java/.gradle" \ - ${docker_image} ${run_command} -} - -Main() -{ - ############################################################################ - # CONSTANTS - ############################################################################ - - local DOCKER_IMAGE="approov/java-spring-demo:11" - - local CONTAINER_NAME="java-spring-demo" - - - ############################################################################ - # INPUT / EXECUTION - ############################################################################ - - local container_user="$(id -u)" - - local http_port_map=8002:8002 - local https_port_map=8003:8003 - - local background_mode="-it" - - for input in "${@}"; do - case "${input}" in - - -d | --detached ) - background_mode="--detach" - shift 1 - ;; - - -h | --help ) - Show_Help - exit 0 - ;; - - --http ) - http_port_map="${2? Missing HTTP port map host:container}" - shift 2 - ;; - - --https ) - https_port_map="${2? Missing HTTPS port host:container}" - shift 2 - ;; - - -u | --user) - container_user="${2? Missing user name or uid to use inside the container}" - shift 2 - ;; - - build) - Build_Docker_Image "${DOCKER_IMAGE}" - exit 0 - ;; - - down ) - sudo docker container stop "java-spring-demo" - exit 0 - ;; - - shell ) - Start_Or_Attach_To_App_Container \ - "${CONTAINER_NAME}" \ - "${container_user}" \ - "${http_port_map}" \ - "${https_port_map}" \ - "${background_mode}" \ - "${DOCKER_IMAGE}" \ - "${2:-zsh}" - - exit 0 - ;; - - up ) - Start_Or_Attach_To_App_Container \ - "${CONTAINER_NAME}" \ - "${container_user}" \ - "${http_port_map}" \ - "${https_port_map}" \ - "${background_mode}" \ - "${DOCKER_IMAGE}" \ - "./gradlew bootRun" - - exit 0 - ;; - esac - done - - Show_Help -} - -Main "${@}" diff --git a/servers/hello/src/approov-protected-server/token-binding-check/settings.gradle b/settings.gradle similarity index 98% rename from servers/hello/src/approov-protected-server/token-binding-check/settings.gradle rename to settings.gradle index 67a9126..bc6fa23 100644 --- a/servers/hello/src/approov-protected-server/token-binding-check/settings.gradle +++ b/settings.gradle @@ -3,4 +3,5 @@ pluginManagement { gradlePluginPortal() } } + rootProject.name = 'approov-jwt' diff --git a/src/main/java/io/approov/ApproovApplication.java b/src/main/java/io/approov/ApproovApplication.java new file mode 100644 index 0000000..5cd2783 --- /dev/null +++ b/src/main/java/io/approov/ApproovApplication.java @@ -0,0 +1,354 @@ +package io.approov; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collections; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.authentication.HttpStatusEntryPoint; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.filter.OncePerRequestFilter; + +@SpringBootApplication +public class ApproovApplication { + + private static final Logger LOGGER = LoggerFactory.getLogger(ApproovApplication.class); + private static final String APPROOV_HEADER = "Approov-Token"; + private static final String AUTH_HEADER = "Authorization"; + private static final String DIGEST_HEADER = "Content-Digest"; + private static final AtomicBoolean APPROOV_ENABLED = new AtomicBoolean(true); + private static final AtomicBoolean TOKEN_BINDING_ENABLED = new AtomicBoolean(true); + private static final byte[] APPROOV_SECRET = loadApproovSecret(); + + private static boolean hasText(String value) { + return value != null && !value.trim().isEmpty(); + } + + public static void main(String[] args) { + SpringApplication.run(ApproovApplication.class, args); + } + + static boolean isApproovEnabled() { + return APPROOV_ENABLED.get(); + } + + static boolean isTokenBindingEnabled() { + return TOKEN_BINDING_ENABLED.get(); + } + + static void enableApproov() { + APPROOV_ENABLED.set(true); + TOKEN_BINDING_ENABLED.set(true); + } + + static void disableApproov() { + APPROOV_ENABLED.set(false); + TOKEN_BINDING_ENABLED.set(false); + } + + static byte[] approovSecret() { + return APPROOV_SECRET; + } + + private static byte[] loadApproovSecret() { + String secret = System.getenv("APPROOV_BASE64URL_SECRET"); + if (!hasText(secret)) { + LOGGER.error("APPROOV_BASE64URL_SECRET environment variable is not set"); + throw new IllegalStateException("APPROOV_BASE64URL_SECRET environment variable is not set"); + } + return Base64.getUrlDecoder().decode(secret.trim()); + } + + @RestController + @RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE) + static class ApproovController { + + @GetMapping("/") + public Map home() { + return infoPayload("Approov demo API is running on port 8080."); + } + + @GetMapping("/approov-state") + public ResponseEntity> approovState() { + Map body = statePayload(); + return ResponseEntity.ok(body); + } + + @PostMapping("/approov/enable") + public Map enableApproovEndpoint() { + enableApproov(); + return statePayload(); + } + + @PostMapping("/approov/disable") + public Map disableApproovEndpoint() { + disableApproov(); + return statePayload(); + } + + @PostMapping("/token-binding/enable") + public Map enableTokenBindingEndpoint() { + TOKEN_BINDING_ENABLED.set(true); + return statePayload(); + } + + @PostMapping("/token-binding/disable") + public Map disableTokenBindingEndpoint() { + TOKEN_BINDING_ENABLED.set(false); + return statePayload(); + } + + @GetMapping("/unprotected") + public Map unprotected() { + return infoPayload("Unprotected endpoint '/unprotected'; no Approov checks performed."); + } + + @GetMapping("/token-check") + public Map tokenCheck() { + return infoPayload("Protected endpoint '/token-check'; Approov token verified."); + } + + @GetMapping("/token-binding") + public Map tokenBinding( + @RequestHeader(value = AUTH_HEADER, required = false) String authorization) { + Map response = infoPayload( + "Protected endpoint '/token-binding'; Approov token binding enforced."); + response.put("authorizationHeaderPresent", hasText(authorization)); + return response; + } + + @GetMapping("/token-double-binding") + public Map tokenDoubleBinding( + @RequestHeader(value = AUTH_HEADER, required = false) String authorization, + @RequestHeader(value = DIGEST_HEADER, required = false) String contentDigest) { + Map response = infoPayload( + "Protected endpoint '/token-double-binding'; dual token binding enforced."); + response.put("authorizationHeaderPresent", hasText(authorization)); + response.put("contentDigestHeaderPresent", hasText(contentDigest)); + return response; + } + + private Map statePayload() { + Map body = new LinkedHashMap<>(); + body.put("approovEnabled", isApproovEnabled()); + body.put("tokenBindingEnabled", isTokenBindingEnabled()); + return body; + } + + private Map infoPayload(String details) { + Map body = statePayload(); + body.put("details", details); + return body; + } + } + + @Configuration + @EnableWebSecurity + static class SecurityConfig { + + private final AuthenticationEntryPoint authEntryPoint = new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED); + + @Bean + public UserDetailsService userDetailsService() { + return new InMemoryUserDetailsManager(); + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http.csrf(csrf -> csrf.disable()) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> auth + .requestMatchers( + "/", + "/unprotected", + "/approov-state", + "/approov/enable", + "/approov/disable", + "/token-binding/enable", + "/token-binding/disable") + .permitAll() + .anyRequest() + .authenticated()) + .exceptionHandling(exceptions -> exceptions.authenticationEntryPoint(authEntryPoint)) + .addFilterBefore( + new ApproovTokenVerifier(authEntryPoint), + UsernamePasswordAuthenticationFilter.class); + return http.build(); + } + } + + /** + * Stateless filter that validates the Approov token (and bindings when enabled) + * before protected endpoints. + */ + static class ApproovTokenVerifier extends OncePerRequestFilter { + + private static final Set APPROOV_PROTECTED_PATHS = Collections.unmodifiableSet( + new java.util.HashSet<>(Arrays.asList( + "/token-check", "/token-binding", "/token-double-binding"))); + + private final AuthenticationEntryPoint entryPoint; + + ApproovTokenVerifier(AuthenticationEntryPoint entryPoint) { + this.entryPoint = entryPoint; + } + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + String path = request.getRequestURI(); + return path == null || !APPROOV_PROTECTED_PATHS.contains(path); + } + + @Override + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + + if (!isApproovEnabled()) { + SecurityContextHolder.getContext().setAuthentication(disabledAuthentication()); + filterChain.doFilter(request, response); + return; + } + + String rawToken = request.getHeader(APPROOV_HEADER); + if (!hasText(rawToken)) { + unauthorized(request, response); + return; + } + + try { + Claims claims = verifyApproovToken(rawToken.trim()); + String path = request.getRequestURI(); + + if (needsBindingCheck(path) && isTokenBindingEnabled()) { + String bindingValue = extractBindingValue(path, request); + if (!hasText(bindingValue) || !isBindingValid(bindingValue, claims)) { + unauthorized(request, response); + return; + } + } + + Authentication authentication = new UsernamePasswordAuthenticationToken( + "approov-token", null, Collections.emptyList()); + SecurityContextHolder.getContext().setAuthentication(authentication); + filterChain.doFilter(request, response); + } catch (JwtException | IllegalArgumentException e) { + LOGGER.error("Approov token verification failed: {}", e.getMessage()); + unauthorized(request, response); + } + } + + private void unauthorized( + HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { + entryPoint.commence( + request, + response, + new BadCredentialsException("Approov authentication failed.")); + } + + private Claims verifyApproovToken(String token) { + Jws claims = Jwts.parser() + .verifyWith(Keys.hmacShaKeyFor(approovSecret())) + .build() + .parseSignedClaims(token); + validateExpiration(claims.getPayload()); + return claims.getPayload(); + } + + private boolean needsBindingCheck(String path) { + return "/token-binding".equals(path) || "/token-double-binding".equals(path); + } + + private String extractBindingValue(String path, HttpServletRequest request) { + if ("/token-binding".equals(path)) { + return trimOrNull(request.getHeader(AUTH_HEADER)); + } + String authorization = trimOrNull(request.getHeader(AUTH_HEADER)); + String digest = trimOrNull(request.getHeader(DIGEST_HEADER)); + if (!hasText(authorization) || !hasText(digest)) { + return null; + } + return authorization + digest; + } + + private boolean isBindingValid(String bindingValue, Claims claims) { + String expected = claims.get("pay", String.class); + if (!hasText(expected)) { + return false; + } + String computed = hashBase64Url(bindingValue); + return expected.trim().equals(computed); + } + + private String hashBase64Url(String value) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(value.getBytes(StandardCharsets.UTF_8)); + return Base64.getEncoder().encodeToString(hash); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("SHA-256 not available", e); + } + } + + private Authentication disabledAuthentication() { + return new UsernamePasswordAuthenticationToken( + "approov-disabled", null, Collections.emptyList()); + } + + private void validateExpiration(Claims claims) { + Date expiration = claims.getExpiration(); + if (expiration == null) { + throw new JwtException("Approov token missing expiration."); + } + if (expiration.before(new Date())) { + throw new JwtException("Approov token expired."); + } + } + + private String trimOrNull(String value) { + return value == null ? null : value.trim(); + } + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..eb25faa --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,10 @@ +# Reduce Spring/Security chatter at startup +logging.level.root=INFO +logging.level.org.springframework=WARN +logging.level.org.springframework.boot=WARN +logging.level.org.springframework.security=WARN + +# Silence DevTools extras +spring.devtools.add-properties=false +spring.devtools.livereload.enabled=false +spring.devtools.restart.enabled=false diff --git a/test.sh b/test.sh new file mode 100644 index 0000000..eb04b4b --- /dev/null +++ b/test.sh @@ -0,0 +1,382 @@ +#!/bin/bash +set -o errexit +set -o nounset +set -o pipefail + +####################################### +# Approov demo API test harness. +# +# Description: +# Calls unprotected and protected endpoints of the Approov demo API, +# validates HTTP status codes and logs complete HTTP exchanges +# (request + response) to a timestamped log file. +# +# Dependencies: +# - bash +# - curl +# - approov CLI available on PATH and configured +# +# Environment: +# BASE_URL: +# Base URL of the API under test. Default: http://localhost:8080 +# TOKDIR: +# Directory where temporary token files are stored. Default: .config +# LOGDIR=${TOKDIR}/logs, LOGFILE=${LOGDIR}/.log +####################################### + +# Constants +readonly BASE_URL="${BASE_URL:-http://localhost:8080}" +readonly TOKDIR="${TOKDIR:-.config}" +readonly LOGDIR="${TOKDIR}/logs" +readonly LOGFILE="${LOGDIR}/$(date '+%Y-%m-%d_%H-%M-%S').log" + +# Globals +# is_approov_disabled: +# Boolean flag indicating if Approov checks appear disabled +# based on /approov-state endpoint. +is_approov_disabled=false +success_code=200 +failure_code=401 + +# state_http_code: +# HTTP status code from /approov-state endpoint. +state_http_code='' + +####################################### +# Print error message to STDERR with timestamp. +# Globals: +# None +# Arguments: +# All arguments are printed as the error message. +# Outputs: +# Writes formatted error message to STDERR. +# Returns: +# 0 +####################################### +err() { + echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')]: $*" >&2 +} + +####################################### +# Ensure a required command exists on PATH. +# Globals: +# None +# Arguments: +# command name to check. +# Outputs: +# Error message to STDERR if command is missing. +# Returns: +# Exits the script with code 1 if the command is missing. +####################################### +requirement_check() { + local cmd="$1" + + if ! command -v "${cmd}" >/dev/null 2>&1; then + err "Missing required command: ${cmd}" + exit 1 + fi +} + +####################################### +# Generate an Approov token into an output file. +# Globals: +# None +# Arguments: +# output file path. +# arguments passed to "approov token". +# Outputs: +# Captures stdout+stderr from "approov token", takes the last non-empty +# line as the token, and writes only that line to the output file. +# Returns: +# 0 on success. +# 1 on failure (CLI error or no token produced). +####################################### +gen_token() { + local outfile="$1" + shift + + set +o errexit + local cli_output + cli_output="$(approov token "$@" 2>&1)" + local rc=$? + set -o errexit + + if ((rc != 0)); then + err "Approov CLI failed: approov token $*" + printf '%s\n' "${cli_output}" >&2 + return 1 + fi + + # Prints notices before the token, grab the last non-empty line. + local token + token="$(printf '%s\n' "${cli_output}" | awk 'NF{last=$0} END{print last}')" + if [[ -z "${token}" ]]; then + err "Approov CLI produced no token output" + return 1 + fi + + printf '%s\n' "${token}" >"${outfile}" +} + +####################################### +# Print test result and append full HTTP exchange to a log file. +# Globals: +# LOGFILE +# is_approov_disabled +# Arguments: +# $1 - test name. +# $2 - expected HTTP status code. +# $3 - actual HTTP status code. +# $4 - full HTTP response (headers + body). +# Outputs: +# Human-readable result to STDOUT. +# Detailed log entry appended to LOGFILE. +# Returns: +# 0 +####################################### +print_test_result() { + local name="$1" + local expected="$2" + local status="$3" + local resp="$4" + + local result="Failed" + if [[ "${status}" == "${expected}" ]]; then + result="Passed" + fi + + echo "${name}: ${result} (status: ${status}, expected: ${expected})" + + { + echo "Test: ${name}" + echo "Expected status: ${expected}" + echo "Actual status: ${status}" + if [[ "${is_approov_disabled}" == "false" ]]; then + echo "Approov State: enabled, token checks performed." + else + echo "Approov State: disabled, no checks performed." + fi + echo + echo "HTTP exchange:" + echo "${resp}" + echo + } >>"${LOGFILE}" 2>&1 +} + +####################################### +# Execute a curl call for a test and evaluate the result. +# Globals: +# None +# Notes: +# Uses print_test_result, which logs to LOGFILE and reads +# is_approov_disabled. +# Arguments: +# test name. +# expected HTTP status code. +# arguments passed to curl. +# Outputs: +# Short result to STDOUT, full HTTP exchange appended to LOGFILE. +# Returns: +# 0 on success, curl's exit code on failure. +####################################### +run_test() { + # shift after each grab so $1 advances (name -> expected -> rest) + local name="$1"; shift + local expected="$1"; shift + + local resp + local status + local curl_rc + + # -i: include headers, -s: silent + set +o errexit + resp="$(curl -i -s "$@")" + curl_rc=$? + set -o errexit + + if ((curl_rc != 0)); then + err "curl failed for ${name} (rc=${curl_rc})" + return "${curl_rc}" + fi + + status="$( + printf '%s\n' "${resp}" | + grep -m1 '^HTTP/' | + awk '{print $2}' + )" + + print_test_result "${name}" "${expected}" "${status}" "${resp}" +} + +main() { + requirement_check "approov" + requirement_check "curl" + + mkdir -p "${TOKDIR}" "${LOGDIR}" + + echo "Listing Approov API configuration:" + approov api -list + echo + + echo "Approov state check:" + local state_response + state_response="$(curl -i -s "${BASE_URL}/approov-state")" + state_http_code="$( + printf '%s\n' "${state_response}" | + grep -m1 '^HTTP/' | + awk '{print $2}' + )" + + if [[ "${state_http_code}" != "200" || -z "${state_http_code}" ]]; then + err "Failed to get Approov state from ${BASE_URL}/approov-state (status=${state_http_code:-unknown})" + exit 1 + fi + + if grep -q '"approovEnabled":true' <<<"${state_response}"; then + echo " Approov service: ENABLED" + is_approov_disabled=false + else + echo " Approov service: DISABLED" + is_approov_disabled=true + failure_code=200 + fi + echo + + # 0) Unprotected endpoint. + run_test \ + "Unprotected request - no approov protection" \ + "${success_code}" \ + "${BASE_URL}/unprotected" + + # 1) Token check. + gen_token \ + "${TOKDIR}/approov_token_valid" \ + -genExample \ + example.com + + # 1.1) Valid Token. + run_test \ + "Token check - valid token" \ + "${success_code}" \ + -H "approov-token: $(<"${TOKDIR}/approov_token_valid")" \ + "${BASE_URL}/token-check" + + # 1.2) Invalid Token. + gen_token \ + "${TOKDIR}/approov_token_invalid" \ + -genExample \ + example.com \ + -type invalid || true + + run_test \ + "Token check - invalid token" \ + "${failure_code}" \ + -H "approov-token: $(<"${TOKDIR}/approov_token_invalid")" \ + "${BASE_URL}/token-check" + + # 2) Token Binding ["Authorization"]. + local AUTH_VAL="ExampleAuthToken==" + export HASH_INPUT="${AUTH_VAL}" + + gen_token \ + "${TOKDIR}/approov_token_bind_auth_valid" \ + -setDataHashInToken "${HASH_INPUT}" \ + -genExample \ + example.com + + # 2.1) Valid Token. + run_test \ + "Single Binding - valid token and header" \ + "${success_code}" \ + -H "Authorization: ${AUTH_VAL}" \ + -H "approov-token: $(<"${TOKDIR}/approov_token_bind_auth_valid")" \ + "${BASE_URL}/token-binding" + + # 2.2) Missing Header. + run_test \ + "Single Binding - missing Authorization header" \ + "${failure_code}" \ + -H "approov-token: $(<"${TOKDIR}/approov_token_bind_auth_valid")" \ + "${BASE_URL}/token-binding" + + # 2.3) Incorrect Header. + run_test \ + "Single Binding - incorrect Authorization header" \ + "${failure_code}" \ + -H "Authorization: BadAuthToken==" \ + -H "approov-token: $(<"${TOKDIR}/approov_token_bind_auth_valid")" \ + "${BASE_URL}/token-binding" + + # 2.4) Invalid Token. + gen_token \ + "${TOKDIR}/approov_token_bind_auth_invalid" \ + -setDataHashInToken "${HASH_INPUT}" \ + -genExample \ + example.com \ + -type invalid || true + + run_test \ + "Single Binding - invalid token" \ + "${failure_code}" \ + -H "Authorization: ${AUTH_VAL}" \ + -H "approov-token: $(<"${TOKDIR}/approov_token_bind_auth_invalid")" \ + "${BASE_URL}/token-binding" + + # 3) Token Binding ["Authorization", "Content-Digest"]. + local AUTH_VAL2="ExampleAuthToken==" + local CD_VAL="ContentDigest==" + export HASH_INPUT="${AUTH_VAL2}${CD_VAL}" + + gen_token \ + "${TOKDIR}/approov_token_bind_auth_cd_valid" \ + -setDataHashInToken "${HASH_INPUT}" \ + -genExample \ + example.com + + # 3.1) Valid. + run_test \ + "Double Binding - valid token and headers" \ + "${success_code}" \ + -H "Authorization: ${AUTH_VAL2}" \ + -H "Content-Digest: ${CD_VAL}" \ + -H "approov-token: $(<"${TOKDIR}/approov_token_bind_auth_cd_valid")" \ + "${BASE_URL}/token-double-binding" + + # 3.2) Missing headers. + run_test \ + "Double Binding - missing binding headers" \ + "${failure_code}" \ + -H "approov-token: $(<"${TOKDIR}/approov_token_bind_auth_cd_valid")" \ + "${BASE_URL}/token-double-binding" + + # 3.3) Incorrect headers. + run_test \ + "Double Binding - incorrect binding headers" \ + "${failure_code}" \ + -H "Authorization: BadAuthToken==" \ + -H "Content-Digest: BadContentDigest==" \ + -H "approov-token: $(<"${TOKDIR}/approov_token_bind_auth_cd_valid")" \ + "${BASE_URL}/token-double-binding" + + # 3.4) Invalid token. + gen_token \ + "${TOKDIR}/approov_token_bind_auth_cd_invalid" \ + -setDataHashInToken "${HASH_INPUT}" \ + -genExample \ + example.com \ + -type invalid || true + + run_test \ + "Double Binding - invalid token" \ + "${failure_code}" \ + -H "Authorization: ${AUTH_VAL2}" \ + -H "Content-Digest: ${CD_VAL}" \ + -H "approov-token: $(<"${TOKDIR}/approov_token_bind_auth_cd_invalid")" \ + "${BASE_URL}/token-double-binding" + + echo + echo "Full request and response details are saved in:" + echo " ${LOGFILE}" +} + +main "$@"