Skip to content

Commit 5067deb

Browse files
Merge branch 'dev_approov-quickstart' into 'master'
Approov Quickstarts See merge request criticalblue/playground/quickstart-python-fastapi-token-check!1
2 parents 4222a59 + bcd1282 commit 5067deb

15 files changed

+995
-1
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.env

README.md

Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,124 @@
22

33
[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.
44

5-
This repo implements the Approov server-side request verification code in Python FastAPI framework, which performs the verification check before allowing valid traffic to be processed by the API endpoint.
5+
This repo implements the Approov server-side request verification code with the Python FastAPI framework, which performs the verification check before allowing valid traffic to be processed by the API endpoint.
6+
7+
8+
## TOC - Table of Contents
9+
10+
* [Why?](#why)
11+
* [How it Works?](#how-it-works)
12+
* [Quickstarts](#approov-integration-quickstarts)
13+
* [Examples](#approov-integration-examples)
14+
* [Useful Links](#useful-links)
15+
16+
17+
## Why?
18+
19+
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:
20+
21+
* Ensures that accesses to your API come from official versions of your apps; it blocks accesses from republished, modified, or tampered versions
22+
* Protects the sensitive data behind your API; it prevents direct API abuse from bots or scripts scraping data and other malicious activity
23+
* 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
24+
* Removes the need for an API key in the mobile app
25+
* 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.
26+
27+
[TOC](#toc---table-of-contents)
28+
29+
30+
## How it works?
31+
32+
This is a brief overview of how the Approov cloud service and the Python FastAPI 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.
33+
34+
### Approov Cloud Service
35+
36+
The Approov cloud service attests that a device is running a legitimate and tamper-free version of your mobile app.
37+
38+
* If the integrity check passes then a valid token is returned to the mobile app
39+
* If the integrity check fails then a legitimate looking token will be returned
40+
41+
In either case, the app, unaware of the token's validity, adds it to every request it makes to the Approov protected API(s).
42+
43+
### Python Backend Server
44+
45+
The Python FastAPI 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 Python backend server.
46+
47+
The request is handled such that:
48+
49+
* If the Approov Token is valid, the request is allowed to be processed by the API endpoint
50+
* If the Approov Token is invalid, an HTTP 401 Unauthorized response is returned
51+
52+
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.
53+
54+
>#### System Clock
55+
>
56+
>In order to correctly check for the expiration times of the Approov tokens is very important that the Python backend server is synchronizing automatically the system clock over the network with an authoritative time source. In Linux this is usual done with a NTP server.
57+
58+
[TOC](#toc---table-of-contents)
59+
60+
61+
## Approov Integration Quickstarts
62+
63+
The quickstart code for the Approov Python 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)).
64+
* [Approov token check quickstart](/docs/APPROOV_TOKEN_QUICKSTART.md)
65+
* [Approov token check with token binding quickstart](/docs/APPROOV_TOKEN_BINDING_QUICKSTART.md)
66+
67+
Both the quickstarts are built from the unprotected example server defined [here](/src/unprotected-server/hello-server-unprotected.py), thus you can use Git to see the code differences between them.
68+
69+
Code difference between the Approov token check quickstart and the original unprotected server:
70+
71+
```
72+
git diff --no-index src/unprotected-server/hello-server-unprotected.py src/approov-protected-server/token-check/hello-server-protected.py
73+
```
74+
75+
You can do the same for the Approov token binding quickstart:
76+
77+
```
78+
git diff --no-index src/unprotected-server/hello-server-unprotected.py src/approov-protected-server/token-binding-check/hello-server-protected.py
79+
```
80+
81+
Or you can compare the code difference between the two quickstarts:
82+
83+
```
84+
git diff --no-index src/approov-protected-server/token-check/hello-server-protected.py src/approov-protected-server/token-binding-check/hello-server-protected.py
85+
```
86+
87+
[TOC](#toc---table-of-contents)
88+
89+
90+
## Approov Integration Examples
91+
92+
The code examples for the Approov quickstarts are extracted from this simple [Approov integration examples](/src/approov-protected-server), that you can run from your computer to play around with the Approov integration and gain a better understanding of how simple and easy it is to integrate Approov in a Python API server.
93+
94+
### Testing with Postman
95+
96+
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 Python server for testing. The collection contains requests with valid and invalid Approov tokens, and with and without token binding.
97+
98+
### Testing with Curl
99+
100+
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).
101+
102+
### The Dummy Secret
103+
104+
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==`.
105+
106+
[TOC](#toc---table-of-contents)
107+
108+
109+
## Useful Links
110+
111+
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:
112+
113+
* [Approov Free Trial](https://approov.io/signup)(no credit card needed)
114+
* [Approov QuickStarts](https://approov.io/docs/latest/approov-integration-examples/)
115+
* [Approov Live Demo](https://approov.io/product/demo)
116+
* [Approov Docs](https://approov.io/docs)
117+
* [Approov Blog](https://blog.approov.io)
118+
* [Approov Resources](https://approov.io/resource/)
119+
* [Approov Customer Stories](https://approov.io/customer)
120+
* [Approov Support](https://approov.zendesk.com/hc/en-gb/requests/new)
121+
* [About Us](https://approov.io/company)
122+
* [Contact Us](https://approov.io/contact)
123+
124+
125+
[TOC](#toc---table-of-contents)
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
# Approov Token Binding Quickstart
2+
3+
This quickstart is for developers familiar with Python 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 Python FastAPI server.
4+
5+
## TOC - Table of Contents
6+
7+
* [Why?](#why)
8+
* [How it Works?](#how-it-works)
9+
* [Requirements](#requirements)
10+
* [Approov Setup](#approov-setup)
11+
* [Approov Token Check](#approov-token-binding-check)
12+
* [Try the Approov Integration Example](#try-the-approov-integration-example)
13+
14+
15+
## Why?
16+
17+
To lock down your API server to your mobile app. Please read the brief summary in the [README](/README.md#why) at the root of this repo or visit our [website](https://approov.io/product.html) for more details.
18+
19+
[TOC](#toc---table-of-contents)
20+
21+
22+
## How it works?
23+
24+
For more background, see the overview in the [README](/README.md#how-it-works) at the root of this repository.
25+
26+
The main functionality for the Approov token binding check is in the file [src/approov-protected-server/token-binding-check/hello-server-protected.py](/src/approov-protected-server/token-binding-check/hello-server-protected.py). Take a look at the `verifyApproovToken()` and `verifyApproovTokenBinding()` functions to see the simple code for the checks.
27+
28+
[TOC](#toc---table-of-contents)
29+
30+
31+
## Requirements
32+
33+
To complete this quickstart you will need both the Python, FastAPI, and the Approov CLI tool installed.
34+
35+
* [Python 3](https://wiki.python.org/moin/BeginnersGuide/Download)
36+
* [FastAPI](https://fastapi.tiangolo.com/tutorial/#install-fastapi)
37+
* [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/)
38+
39+
[TOC](#toc---table-of-contents)
40+
41+
42+
## Approov Setup
43+
44+
To use Approov with the Python FastAPI server we need a small amount of configuration. First, Approov needs to know the API domain that will be protected. Second, the Python FastAPI server needs the Approov Base64 encoded secret that will be used to verify the tokens generated by the Approov cloud service.
45+
46+
### Configure API Domain
47+
48+
Approov needs to know the domain name of the API for which it will issue tokens.
49+
50+
Add it with:
51+
52+
```text
53+
approov api -add your.api.domain.com
54+
```
55+
56+
Adding the API domain also configures the [dynamic certificate pinning](https://approov.io/docs/latest/approov-usage-documentation/#approov-dynamic-pinning) setup, out of the box.
57+
58+
> **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.
59+
60+
### Approov Secret
61+
62+
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 Python FastAPI server environment to check the signatures of the [Approov Tokens](https://www.approov.io/docs/latest/approov-usage-documentation/#approov-tokens) that it processes.
63+
64+
Retrieve the Approov secret with:
65+
66+
```text
67+
approov secret -get base64
68+
```
69+
70+
> **NOTE:** The `approov secret` command requires an [administration role](https://approov.io/docs/latest/approov-usage-documentation/#account-access-roles) to execute successfully.
71+
72+
#### Set the Approov Secret
73+
74+
Open the `.env` file and add the Approov secret to the var:
75+
76+
```text
77+
APPROOV_BASE64_SECRET=approov_base64_secret_here
78+
```
79+
80+
[TOC](#toc---table-of-contents)
81+
82+
83+
## Approov Token Check
84+
85+
To check the Approov token we will use the [jpadilla/pyjwt/](https://github.com/jpadilla/pyjwt/) package, but you are free to use another one of your preference.
86+
87+
Add this code to your project, just before your first API endpoint:
88+
89+
```python
90+
from fastapi import FastAPI, Request
91+
from fastapi.responses import JSONResponse
92+
93+
# @link https://github.com/jpadilla/pyjwt/
94+
import jwt
95+
import base64
96+
import hashlib
97+
98+
# @link https://github.com/theskumar/python-dotenv
99+
from dotenv import load_dotenv, find_dotenv
100+
load_dotenv(find_dotenv(), override=True)
101+
from os import getenv
102+
103+
# Token secret value obtained with the Approov CLI tool:
104+
# - approov secret -get
105+
approov_base64_secret = getenv('APPROOV_BASE64_SECRET')
106+
107+
if approov_base64_secret == None:
108+
raise ValueError("Missing the value for environment variable: APPROOV_BASE64_SECRET")
109+
110+
APPROOV_SECRET = base64.b64decode(approov_base64_secret)
111+
112+
app = FastAPI()
113+
114+
################################################################################
115+
# ONLY ADD YOUR MIDDLEWARE BEFORE THIS LINE.
116+
# - FastAPI seems to execute middleware in the reverse we declare it in the
117+
# code.
118+
# - Approov middleware SHOULD be the first to be executed in the request life
119+
# cycle.
120+
################################################################################
121+
122+
# @link https://approov.io/docs/latest/approov-usage-documentation/#token-binding
123+
# @IMPORTANT FastAPI seems to execute middleware in the reverse order they
124+
# appear in the code, therefore this one must come right before the
125+
# verifyApproovToken() middleware.
126+
@app.middleware("http")
127+
async def verifyApproovTokenBinding(request: Request, call_next):
128+
# Note that the `pay` claim will, under normal circumstances, be present,
129+
# but if the Approov failover system is enabled, then no claim will be
130+
# present, and in this case you want to return true, otherwise you will not
131+
# be able to benefit from the redundancy afforded by the failover system.
132+
if not 'pay' in request.state.approov_token_claims:
133+
# You may want to add some logging here.
134+
return JSONResponse({}, status_code = 401)
135+
136+
# We use the Authorization token, but feel free to use another header in
137+
# the request. Beqar in mind that it needs to be the same header used in the
138+
# mobile app to qbind the request with the Approov token.
139+
token_binding_header = request.headers.get("Authorization")
140+
141+
if not token_binding_header:
142+
# You may want to add some logging here.
143+
return JSONResponse({}, status_code = 401)
144+
145+
# We need to hash and base64 encode the token binding header, because that's
146+
# how it was included in the Approov token on the mobile app.
147+
token_binding_header_hash = hashlib.sha256(token_binding_header.encode('utf-8')).digest()
148+
token_binding_header_encoded = base64.b64encode(token_binding_header_hash).decode('utf-8')
149+
150+
if request.state.approov_token_claims['pay'] == token_binding_header_encoded:
151+
return await call_next(request)
152+
153+
return JSONResponse({}, status_code = 401)
154+
155+
# @link https://approov.io/docs/latest/approov-usage-documentation/#backend-integration
156+
# @IMPORTANT FastAPI seems to execute middleware in the reverse order they
157+
# appear in the code, therefore this one must come as the LAST of the
158+
# middleware's.
159+
@app.middleware("http")
160+
async def verifyApproovToken(request: Request, call_next):
161+
approov_token = request.headers.get("Approov-Token")
162+
163+
# If we didn't find a token, then reject the request.
164+
if approov_token == "":
165+
# You may want to add some logging here.
166+
# return None
167+
return JSONResponse({}, status_code = 401)
168+
169+
try:
170+
# Decode the Approov token explicitly with the HS256 algorithm to avoid
171+
# the algorithm None attack.
172+
request.state.approov_token_claims = jwt.decode(approov_token, APPROOV_SECRET, algorithms=['HS256'])
173+
return await call_next(request)
174+
except jwt.ExpiredSignatureError as e:
175+
# You may want to add some logging here.
176+
return JSONResponse({}, status_code = 401)
177+
except jwt.InvalidTokenError as e:
178+
# You may want to add some logging here.
179+
return JSONResponse({}, status_code = 401)
180+
181+
# @app.get("/")
182+
# async def root():
183+
# return {"message": "Hello World"}
184+
```
185+
186+
> **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`.
187+
188+
Using the middleware approach will ensure that all endpoints in your API will be protected by Approov.
189+
190+
A full working example for a simple Hello World server can be found at [src/approov-protected-server/token-binding-check](/src/approov-protected-server/token-binding-check).
191+
192+
[TOC](#toc---table-of-contents)
193+
194+
195+
## Test your Approov Integration
196+
197+
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.
198+
199+
#### With Valid Approov Tokens
200+
201+
Generate a valid token example from the Approov Cloud service:
202+
203+
```
204+
approov token -setDataHashInToken 'Bearer authorizationtoken' -genExample your.api.domain.com
205+
```
206+
207+
Then make the request with the generated token:
208+
209+
```text
210+
curl -i --request GET 'https://your.api.domain.com/v1/shapes' \
211+
--header 'Authorization: Bearer authorizationtoken' \
212+
--header 'Approov-Token: APPROOV_TOKEN_EXAMPLE_HERE'
213+
```
214+
215+
The request should be accepted. For example:
216+
217+
```text
218+
HTTP/1.1 200 OK
219+
220+
...
221+
222+
{"message": "Hello, World!"}
223+
```
224+
225+
#### With Invalid Approov Tokens
226+
227+
##### No Authorization Token
228+
229+
Let's just remove the Authorization header from the request:
230+
231+
```text
232+
curl -i --request GET 'https://your.api.domain.com/v1/shapes' \
233+
--header 'Approov-Token: APPROOV_TOKEN_EXAMPLE_HERE'
234+
```
235+
236+
The above request should fail with an Unauthorized error. For example:
237+
238+
```text
239+
HTTP/1.1 401 Unauthorized
240+
241+
...
242+
243+
{}
244+
```
245+
246+
##### Same Approov Token with a Different Authorization Token
247+
248+
Make the request with the same generated token, but with another random authorization token:
249+
250+
```
251+
curl -i --request GET 'https://your.api.domain.com/v1/shapes' \
252+
--header 'Authorization: Bearer anotherauthorizationtoken' \
253+
--header 'Approov-Token: APPROOV_TOKEN_EXAMPLE_HERE'
254+
```
255+
256+
The above request should also fail with an Unauthorized error. For example:
257+
258+
```text
259+
HTTP/1.1 401 Unauthorized
260+
261+
...
262+
263+
{}
264+
```

0 commit comments

Comments
 (0)