Building a server-side app - Part 5: Securing the app
One security feature to consider for a server-side app is verifying that an HTTP request for the initial page originates from a legitimate Zendesk product instance.
If you want, Zendesk can include a ZAF JSON Web Token (JWT) in the request for the initial page. After receiving the request, your server-side app can check the signed token to validate that the request originated from a legitimate Zendesk product instance. This helps prevent downgrade attacks.
The signed token also contains a number of attributes (known as claims) that your server-side app can use to look up externally stored values associated with your Zendesk Support account.
The tutorial describes how to modify the tutorial app to validate an HTTP request for the initial page of the web app. It'll walk you through the following tasks:
- Installing required libraries
- Getting your Zendesk app's public key and installation id
- Enabling the ZAF JWT in Zendesk
- Retrieving the ZAF JWT in your server-side app
- Validating the ZAF JWT
This tutorial is the fifth part of a series on building a server-side Zendesk app:
- Part 1: Core concepts
- Part 2: Displaying server-side content in a Zendesk app
- Part 3: Accessing external APIs
- Part 4: Accessing framework APIs
- Part 5: Securing the app - YOU ARE HERE
- Part 6: Deploying the app
For more information about ZAF JWTs, see Authenticating Zendesk in your server-side app.
Installing required libraries
You need a JWT client library to decode the token sent by Zendesk. PyJWT is a popular choice for Python.
Zendesk signs the ZAF JWT with an RSA digital signature known as RS256. PyJWT depends on a second Python library called cryptography to decode tokens signed with RSA signature algorithms, so you'll need to install it too.
-
In an empty terminal session, install the PyJWT package:
pip3 install PyJWT
-
Install the cryptography package:
pip3 install cryptography
Getting your Zendesk app's public key and installation id
To decode the ZAF JWT, the PyJWT library needs your Zendesk app's public key and installation id. To get the public key and installation id, you first need to install the Zendesk app.
-
If running, stop the ZCLI web server by pressing Ctrl+C.
-
In the app_local folder, run:
zcli apps:create
The command packages, uploads, and installs the Zendesk app to your Zendesk instance. When finished, it returns an id for the app.
-
To get the app's public key, make a Get App Public Key request:
curl https://{subdomain}.zendesk.com/api/v2/apps/{app_id}/public_key.pem \
-u {email_address}/token:{api_token}
Replace
{app_id}
with the id you received in step 2.The request returns the app's public key in the PEM format. Save the key securely. You'll use it later in the tutorial.
-----BEGIN PUBLIC KEY-----
MIIBIjAMBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm/mliC8BuDgagg2wUImH
6Ve+CAFetSzdGujOCjKdKCuFdwzXKzlt/EfFSlq8BLFD88XEjFljc+y1xzxHS13E
4AK+CVKtZzInJswb5uQuJokJ0KbQGMudps7grabTklvzbQmoymnTXWTmAAi1IzyS
splm9JGkseja6C6oOt3CtM2wvF6h+nI4h5Zla6Nr+qhoqQlxvML9YKr93sWne8UJ
zasooccc/Q/c9emj9IaRSlGp1UFcEqIjrIZsIRwMmpYlqrf03o0MiDOhfkLnCpbj
91ZojutN/C17A2/QoPKT6Txoyfl8kXu2p8MsT9dJvhv6qHXNHMBp75Sio6mmEAbb
lQIDAQAB
-----END PUBLIC KEY-----
-
To get the app's installation id, make a List App Installations request:
curl https://{subdomain}.zendesk.com/api/v2/apps/installations.json \
-u {email_address}/token:{api_token}
In the response, find the object containing your Zendesk app's
app_id
andname
. The object'sid
is the app's installation id. Example:{
"installations": [
...
{
"id": 1234567890123,
"app_id": 123456,
"product": "support",
"settings": {
"name": "Server-side App Tutorial",
"title": "Server-side App Tutorial"
},
....
}
]
}
Enabling the ZAF JWT in Zendesk
Enable the ZAF JWT by updating the manifest.json file for the installed Zendesk app.
-
In the app_local folder, add a top-level
"signedUrls": true
property to manifest.json. Example:{
...
"signedUrls": true,
"location": { ... },
...
}
-
Save the manifest.json file, but don't update your installed Zendesk app yet.
You'll update the Zendesk app later in the tutorial.
Retrieving the ZAF JWT in your server-side app
Once the ZAF JWT is enabled, Zendesk does the following:
- Changes the request method for the initial page from GET to POST.
- Includes a signed JWT token in a field named
token
in the POST request's form data.
You must update your server-side app to perform the following tasks:
- Handle POST requests for the initial page of the web app.
- Get the token from the form data in the POST request.
To retrieve the token
-
In app.py, modify the "/sidebar" route to accept only POST requests (highlighted) :
@route('/sidebar', method='POST')
def send_iframe_html():
...
-
Retrieve the token from the request's form data:
@route('/sidebar', method='POST')
def send_iframe_html():
token = request.forms.get('token')
...
-
If a token is not found, reject the page request:
@route('/sidebar', method='POST')
def send_iframe_html():
token = request.forms.get('token')
if not token:
return 'Missing token. Sorry, no can do.'
...
Validating the ZAF JWT
Once you retrieve the JWT token from the POST request, the next step is to validate it.
The PyJWT decoder needs two things to validate the token:
- Your public key
- Proof that your Zendesk app is the intended audience for the token
If either is incorrect, the decoder returns an error that the token is invalid.
The token that Zendesk issues is meant for your Zendesk app only. To prove that your app is the intended audience, you must supply the following value to the decoder:
https://{your_subdomain}.zendesk.com/api/v2/apps/installations/{app_installation_id}.json
The decoder will try to match it against the audience specified in the token's values. The values must match or the decoder will return an invalid token error.
To validate the token
-
In the app_remote folder, add the following variables to the .env file:
...
ZENDESK_APP_AUD="https://{subdomain}.zendesk.com/api/v2/apps/installations/{app_installation_id}.json"
ZENDESK_APP_PUBLIC_KEY="{zendesk_app_key}"
Replace
{subdomain}
with your Zendesk account's subdomain. Replace{app_installation_id}
with the Zendesk app installation id you saved in Getting your Zendesk app's public key and installation id.Replace
{zendesk_app_key}
with the public key you saved in Getting your app's Zendesk public key and installation id. Include the opening and closing comments as well as any line breaks. Ensure the value is enclosed in double quotes ("). Example:...
ZENDESK_APP_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----
MIIBIjAMBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm/mliC8BuDgagg2wUImH
6Ve+CAFetSzdGujOCjKdKCuFdwzXKzlt/EfFSlq8BLFD88XEjFljc+y1xzxHS13E
4AK+CVKtZzInJswb5uQuJokJ0KbQGMudps7grabTklvzbQmoymnTXWTmAAi1IzyS
splm9JGkseja6C6oOt3CtM2wvF6h+nI4h5Zla6Nr+qhoqQlxvML9YKr93sWne8UJ
zasooccc/Q/c9emj9IaRSlGp1UFcEqIjrIZsIRwMmpYlqrf03o0MiDOhfkLnCpbj
91ZojutN/C17A2/QoPKT6Txoyfl8kXu2p8MsT9dJvhv6qHXNHMBp75Sio6mmEAbb
lQIDAQAB
-----END PUBLIC KEY-----"
-
In app.py, add the PyJWT library with the following statement at the top of the file:
import jwt
import os
from os.path import join, dirname
...
-
In the "/sidebar" route, replace the following highlighted lines:
@route('/sidebar', method='POST')
def send_iframe_html():
token = request.forms.get('token')
if not token:
return 'Missing token. Sorry, no can do.'
qs = request.query_string
response.set_cookie('my_app_params', qs)
return template('start')
...
with the following
try-except-else
blocks:@route('/sidebar', method='POST')
def send_iframe_html():
token = request.forms.get('token')
if not token:
return 'Missing token. Sorry, no can do.'
try:
key = os.environ.get('ZENDESK_APP_PUBLIC_KEY')
audience = os.environ.get('ZENDESK_APP_AUD')
payload = jwt.decode(token, key, algorithms=['RS256'], audience=audience)
except jwt.InvalidTokenError:
return '401 Invalid token. Calling the cops.'
else:
qs = request.query_string
response.set_cookie('my_app_params', qs)
return template('start', qs=qs)
...
The
try
block makes thejwt.decode()
method call to validate the token. The call uses thetoken
,key
, andaudience
arguments.The
except
block handles errors for thejwt.decode()
call. If anInvalidTokenError
exception is raised during the decoding attempt, then the web app returns an error message and aborts the request. If no exception is raised, then the token is valid and the app proceeds with the normal response to the page request.The POST request still includes url parameters with the required origin and app_guide values. See Accessing framework APIs in Part 1 of this tutorial series.
Note that the payload variable now consists of a dictionary with all the token's values, which are also known as claims. You can access the claims as follows:
issuer = payload["iss"] # example, issuer = 'example.zendesk.com'
For a complete list, see JWT claims.
Testing the app
To test the updated web app in Zendesk Support:
-
Restart the Bottle development server.
-
In an empty terminal session, run the following command in the app_local folder:
zcli apps:update
The command updates the manifest.json file for the installed app.
-
In your browser, sign in to Zendesk Support and go to the Agent Workspace. From the workspace, open a new or existing ticket.
The URL should look like this:
https://{subdomain}.zendesk.com/agent/tickets/{ticket_id}
-
Click the Apps icon.
The app displays the start page.
-
To test the JWT error handling, introduce a typo in the
ZENDESK_APP_AUD
variable in .env. Then restart the Bottle server and reload the Agent Workspace page. The app displays an error message.Remember to fix the typo when you're finished testing.
You've updated your web app so that it verifies requests from Zendesk. In the next tutorial, you'll deploy your web app to Glitch for hosting. See Part 6: Deploying the app.