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 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 app. It'll walk you through the following tasks:

To review the tutorial app code so far, see Code complete at the end of Part 4 of this tutorial.

This tutorial is the sixth part of a series on building a server-side Zendesk app:

For more information on this feature, see Authenticating Zendesk in your server-side app in the developer docs.

Disclaimer: Zendesk provides this article for instructional purposes only. Zendesk does not support or guarantee the code. Zendesk also can't provide support for third-party technologies such as Python and PyJWT. Please post any issue in the comments section or search for a solution online.

Install 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 JWT token with a 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. Install PyJWT:

    $ pip3 install PyJWT
  2. Install cryptography:

    $ pip3 install cryptography
  3. Add PyJWT and cryptography to your Bottle app's requirements.txt file (in bold):

    bottle==0.12.13
    requests==2.13.0
    PyJWT==1.4.2
    cryptography==1.8.1
    

Get your app's public key

The PyJWT library needs your app's public key to decode the JWT token sent by Zendesk. To get the key, you need the id of your app in Zendesk. You can get the app id with the List All Apps endpoint in the Zendesk REST API.

Important: If you haven't installed the app in your Zendesk product instance yet, you'll need to do so to create the id. See Install the framework app. After uploading the app, don't enable it yet. Return here to continue.

To get a public key

  1. Get your app id by running the following curl command in your command-line interface, making sure to replace the placeholders with your values:

    curl https://{subdomain}.zendesk.com/api/v2/apps.json \-u {email_address}:{password}

    Tip: To find the app id in the verbose response, search for the name you specified in the manifest.json file. Example: "Server-side App Tutorial".

    Once you have the id, you can plug it into the Get App Public Key endpoint to get a public key.

  2. Get the app's public key by running the following curl command, replacing the placeholders with your values:

    curl https://{subdomain}.zendesk.com/api/v2/apps/{id}/public_key.pem \-u {email_address}:{password}

    The API returns the key in the PEM format.

  3. Save the key to a text file named public-key.txt in the same folder as your app.py file. Make sure to include the opening and closing comments to preserve the PEM format. Example:

    -----BEGIN PUBLIC KEY-----MIIBIjAMBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm/mliC8BuDgagg2wUImH6Ve+CAFetSzdGujOCjKdKCuFdwzXKzlt/EfFSlq8BLFD88XEjFljc+y1xzxHS13E4AK+CVKtZzInJswb5uQuJokJ0KbQGMudps7grabTklvzbQmoymnTXWTmAAi1IzySsplm9JGkseja6C6oOt3CtM2wvF6h+nI4h5Zla6Nr+qhoqQlxvML9YKr93sWne8UJzasooccc/Q/c9emj9IaRSlGp1UFcEqIjrIZsIRwMmpYlqrf03o0MiDOhfkLnCpbj91ZojutN/C17A2/QoPKT6Txoyfl8kXu2p8MsT9dJvhv6qHXNHMBp75Sio6mmEAbblQIDAQAB-----END PUBLIC KEY-----

Enable the JWT token in Zendesk

You enable the JWT token in Zendesk by updating the framework app's manifest.json file installed in Zendesk.

  1. Add "signedUrls": true to your manifest.json file. Example:

    {  "name": "Server-side App Tutorial",  "signedUrls": true,  "location": { ... },  ...}
  2. Save the file, but don't upload it to Zendesk yet.

    You'll update the client-side and server-side parts of the app at the same time later in this tutorial.

Retrieve the token in your server-side app

Once the JWT token 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.

Therefore, you must update your server-side app to perform the following tasks:

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

To review the code of the server-side app up to this point, see Code complete at the end of Part 4 of this tutorial.

To retrieve the token

  1. In app.py, modify the '/sidebar' route to accept only POST requests (in bold) :

    @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.'
        ...
    

Validate the token

Once you retrieve the JWT token from the POST request, the next step is to validate it.

The PyJWT decoder needs 2 things to validate the token:

  • Your public key
  • Proof that your 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 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/{ap_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 app.py, add the the PyJWT library with the following statement at the top of the file:

    import jwt
  2. In the '/sidebar' route, assign the public key from the file you created earlier to the key variable (in bold):

    @route('/sidebar', method='POST')
    def send_iframe_html():
        token = request.forms.get('token')
        if not token:
            return 'Missing token. Sorry, no can do.'
        with open('public-key.txt', mode='r', encoding='utf-8') as f:
            key = f.read()
        ...
    
  3. Assign the correct string to the audience variable, replacing the placeholder values with your values:

    @route('/sidebar', method='POST')
    def send_iframe_html():
        token = request.forms.get('token')
        if not token:
            return 'Missing token. Sorry, no can do.'
        with open('public-key.txt', mode='r', encoding='utf-8') as f:
            key = f.read()
        audience = 'https://{your_subdomain}.zendesk.com/api/v2/apps/installations/{app_id}.json'
        ...
    
  4. Use the token, key, and audience variables in the jwt.decode() method to validate the token:

    @route('/sidebar', method='POST')
    def send_iframe_html():
        token = request.forms.get('token')
        if not token:
            return 'Missing token. Sorry, no can do.'
        with open('public-key.txt', mode='r', encoding='utf-8') as f:
            key = f.read()
        audience = 'https://{your_subdomain}.zendesk.com/api/v2/apps/installations/{app_id}.json'
        try:
            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)
    

    If an InvalidTokenError exception is raised during the decoding attempt, then the 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.

    By the way, the POST request still includes url parameters with the required origin and app_guid values. See Accessing framework APIs in Part 1 of this tutorial.

    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 = 'omniwear.zendesk.com'

    For a complete list, see JWT claims in the developer docs.

Redeploy the app

Redeploy the Zendesk part and server-side part of the app at the same time to ensure the two stay in sync.

  1. Update the local framework app on Zendesk with your modified manifest.json file. For instructions, see Updating an installed app.
  2. Update the remote Bottle app on Heroku. For instructions, see Push updates to Heroku.