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:

This tutorial is the fifth part of a series on building a server-side Zendesk 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.

  1. In an empty terminal session, install the PyJWT package:

    pip3 install PyJWT
  2. 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.

  1. If running, stop the ZCLI web server by pressing Ctrl+C.

  2. 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.

  3. 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}:{password}

    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/mliC8BuDgagg2wUImH6Ve+CAFetSzdGujOCjKdKCuFdwzXKzlt/EfFSlq8BLFD88XEjFljc+y1xzxHS13E4AK+CVKtZzInJswb5uQuJokJ0KbQGMudps7grabTklvzbQmoymnTXWTmAAi1IzySsplm9JGkseja6C6oOt3CtM2wvF6h+nI4h5Zla6Nr+qhoqQlxvML9YKr93sWne8UJzasooccc/Q/c9emj9IaRSlGp1UFcEqIjrIZsIRwMmpYlqrf03o0MiDOhfkLnCpbj91ZojutN/C17A2/QoPKT6Txoyfl8kXu2p8MsT9dJvhv6qHXNHMBp75Sio6mmEAbblQIDAQAB-----END PUBLIC KEY-----
  4. 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}:{password}

    In the response, find the object containing your Zendesk app's app_id and name. The object's id 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.

  1. In the app_local folder, add a top-level "signedUrls": true property to manifest.json. Example:

    {  ...  "signedUrls": true,  "location": { ... },  ...}
  2. 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:

  1. Changes the request method for the initial page from GET to POST.
  2. 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:

  1. Handle POST requests for the initial page of the web app.
  2. Get the token from the form data in the POST request.

To retrieve the token

  1. In app.py, modify the "/sidebar" route to accept only POST requests (highlighted) :

    @route('/sidebar', method='POST')def send_iframe_html():    ...
  2. Retrieve the token from the request's form data:

    @route('/sidebar', method='POST')def send_iframe_html():    token = request.forms.get('token')    ...
  3. 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

  1. 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/mliC8BuDgagg2wUImH6Ve+CAFetSzdGujOCjKdKCuFdwzXKzlt/EfFSlq8BLFD88XEjFljc+y1xzxHS13E4AK+CVKtZzInJswb5uQuJokJ0KbQGMudps7grabTklvzbQmoymnTXWTmAAi1IzySsplm9JGkseja6C6oOt3CtM2wvF6h+nI4h5Zla6Nr+qhoqQlxvML9YKr93sWne8UJzasooccc/Q/c9emj9IaRSlGp1UFcEqIjrIZsIRwMmpYlqrf03o0MiDOhfkLnCpbj91ZojutN/C17A2/QoPKT6Txoyfl8kXu2p8MsT9dJvhv6qHXNHMBp75Sio6mmEAbblQIDAQAB-----END PUBLIC KEY-----"
  2. In app.py, add the PyJWT library with the following statement at the top of the file:

    import jwtimport osfrom os.path import join, dirname...
  3. 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 the jwt.decode() method call to validate the token. The call uses the token, key, and audience arguments.

    The except block handles errors for the jwt.decode() call. If an InvalidTokenError 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:

  1. Restart the Bottle development server.

  2. 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.

  3. 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}

  4. Click the Apps icon.

    The app displays the start page.

  5. 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.