In the previous tutorial in the series, you modified the Zendesk app to use access tokens when making requests to Asana. In this tutorial, you'll build a lightweight server-side application to manage the OAuth authorization flow and pass the token to the Zendesk app.

The completed application should be able to perform the following tasks:

  • Redirect the user to the Asana authorization page
  • Handle the response from Asana
  • Manage access and refresh tokens

To keep things simple, the application is built using a Python micro web framework called Bottle. The application will require a total of less than 70 lines of reasonably easy-to-understand code to perform all 3 tasks above. The Bottle framework helps keep technical details to a minimum so you can focus on the logic, which you can then apply in another framework or in PHP if you want.

The tutorial covers the following tasks:

This tutorial is the third part of a series on adding OAuth to a Zendesk app:

Set up the server-side application

To develop and test the application, you'll need Python and a few Python libraries, as described next. You'll also need a command-line interface like the command prompt in Windows or the Terminal on the Mac.

Install Python 3

Install the latest version of Python on your computer if you don't already have it. Python is a powerful but beginner-friendly scripting and programming language with a clear and readable syntax. Visit the Python website to learn more. To download and install it, see http://www.python.org/download/.

Install pip (Python 3.3 or earlier only)

If you have Python 3.4 or better, you already have pip. Skip this section.

If you have version 3.3 or earlier, install pip, a simple tool for installing and managing Python packages. See these instructions.

Install Requests

After installing pip, use the following pip command in your command-line interface to download and install Requests, a library that simplifies making HTTP requests:

$ pip install requests

If you have any problems, see the Requests instructions.

Install Bottle

Use the following pip command to download and install Bottle, a micro web framework for Python.

$ pip install bottle

If you have any problems, see the Bottle instructions.

Create a starter file for the application

  1. Create a folder named asana_auth.
  2. In the new folder, create a text file named auth.py.
  3. Add the following lines to the file:

    import osfrom urllib.parse import urlencode
    import requestsfrom bottle import route, redirect, request, response, template, run
    # your code goes here
    if os.environ.get('APP_LOCATION') == 'heroku':    run(host="0.0.0.0", port=int(os.environ.get("PORT", 5000)))else:    run(host='localhost', port=8080, debug=True)

The first 4 lines import various necessary resources. You'll use the urlencode method to encode some URL parameters required by Asana. You'll use the requests library to make HTTP requests to the Asana API. You'll use the various Bottle methods to run the web application. To learn more about the Bottle framework, see Bottle: Python Web Framework on the Bottle website.

The last block starts the app on Heroku. If the app is not on Heroku, it runs the app on the Bottle framework's built-in development server at http://localhost:8080. You can use the local server to test your changes locally. However, because Asana doesn't allow unencrypted connections for OAuth redirect URLs, you'll need to use the Heroku server or another server that supports https to specifically test the app's authentication flow.

Coding in Python

When copying the examples in this tutorial, make sure to indent lines exactly as shown. Indentation matters in Python.

If you're interested in taking a deeper dive into Python after this tutorial, see the following free resources:

Register the application with Asana

You need to tell Asana about your application to use OAuth authentication. The application you need to register is the Bottle application, not your Zendesk app. It's the one that will interact with Asana in the authorization flow.

To register the application

  1. After signing in to Asana, click your user icon in the upper-right side, and select My Profile Settings.
  2. In the My Profile Settings dialog box, click the Apps tab.
  3. Click Manage Developer Apps on the lower side of the dialog box.
  4. Under the "Apps You Own" heading, click the Add New Application link.
  5. Complete the following fields in the page that appears:

    • App Name - Enter Zendesk App OAuth Tutorial or something along those lines. This is the name that users will see when asked to grant access to the application.
    • App URL - Enter the following URL for now:

      https://my-example-app.herokuapp.com

      Because you don't know the app URL yet, use the my-example-app placeholder for now. After deploying the app to Heroku later in the tutorial, you can update these Asana settings.

    • Redirect URL - Enter the following URL:

      https://my-example-app.herokuapp.com/auth/handle_decision

      This is the URL where Asana will send the user's permission decision. You'll create the resource at this URL to handle the decision later.

    • Agreement - Click the checkbox to Accept the API terms and conditions.

    Example:

  6. Click Create.

    New Client ID and Client Secret fields appear:

  7. Copy and save the Client ID and Client Secret values somewhere safe.
  8. For Authorization Endpoint, make sure the Authorization Code Grant option is selected.
  9. Click Save.

Send the user to the Asana authorization page

In the previous tutorial, you modified the Zendesk app to show a sign-in button to kick off the authorization flow:

When the user clicks the button, the browser requests the following resource from the server:

<a href="https://my-example-app.herokuapp.com/auth/asana?state={{state}}" ... >

When the server-side application receives the request for /auth/asana, it should package the required authorization parameters and redirect the user to the Asana authorization page.

To send the user to the authorization page

  1. In the auth.py file, add the following route after the import statements but before the run() statement. Make sure your code is indented as shown.

    @route('/auth/asana')def asana_auth():    params = {        'response_type': 'code',        'redirect_uri': 'https://my-example-app.herokuapp.com/auth/handle_decision',        'client_id': 'your_client_id',        'state': request.query.state    }    url = 'https://app.asana.com/-/oauth_authorize?' + urlencode(params)    redirect(url)
  2. Replace the value of your_client_id with your client id from the registration page.

How it works

A Bottle application consists of routes that map HTTP requests to functions. The return value of each function is sent in the HTTP response.

When the server receives a request for /auth/asana, it runs the asana_auth() function:

@route('/auth/asana')def asana_auth():    ...

The function starts by defining a few parameters required by Asana:

...    params = {        'response_type': 'code',        'redirect_uri': 'https://my-example-app.herokuapp.com/auth/handle_decision',        'client_id': 'your_client_id',        'state': request.query.state    }

Because the application uses the authorization code grant flow, the first parameter specifies 'code' as the response type.

The 'redirect_uri' URL is where you want Asana to send the user's decision. It must be identical to the URL you specified as the Redirect URL on the registration page. You'll create a route for the URL in the next section.

The 'client_id' parameter is the client id from the registration page.

The 'state' parameter specifies a ticket id in Zendesk Support so you can return to it later. The request.query.state property is a Bottle expression that retrieves the value of the state parameter in the request's query string -- in this case, the ticket ID inserted by your Zendesk app:

... href="https://my-example-app.herokuapp.com/auth/asana?state={{state}}"

Next, the parameters are URL-encoded and appended as a query string to a URL specified by Asana:

url = 'https://app.asana.com/-/oauth_authorize?' + urlencode(params)

Finally, the function redirects the user and the parameters to the Asana URL:

redirect(url)

Asana processes the parameters and then, if the user is signed in, displays an authorization page in the user's browser:

In the next section, you'll add a route to handle the user's authorization decision sent back by Asana.

Handle the user's authorization decision

After the user decides to allow or deny access to the Bottle application, Asana sends the decision and a few other bits of information to the URL you specified in the redirect_uri parameter:

https://my-example-app.herokuapp.com/auth/handle_decision

If the user decided to authorize the application, Asana adds a query-string parameter named code that contains an authorization code. Example:

{redirect_uri}?code=7xqwtlf3rrdj8uyeb1yf

If the user decided not to authorize the application, Asana's response includes a query-string parameter called error_description that contains the following string: "The user denied the authorization request". Example:

{redirect_uri}?error=access_denied&error_description=The%20user%20denied%20the%20authorization%20request.

You can use these parameters to control the flow of the application:

  • If the word "error" is found anywhere in the query string, show an error message.
  • If not, exchange the authorization code for an access token

To handle the user's decision

  1. Add the following route corresponding to the redirect URL. Make sure your code is indented as shown.

    @route('/auth/handle_decision')def handle_decision():    if 'error' in request.query_string:        return request.query.error_description      params = {        'grant_type': 'authorization_code',        'code': request.query.code,        'client_id': 'your_client_id',        'client_secret': 'your_client_secret',        'redirect_uri': 'https://my-example-app.herokuapp.com/auth/handle_decision'    }    url = 'https://app.asana.com/-/oauth_token'    r = requests.post(url, data=params)    if r.status_code != 200:        error_msg = 'Failed to get access token with error {}'.format(r.status_code)        return error_msg    else:        data = r.json()        response.set_cookie('sheet', data['access_token'], max_age=data['expires_in'])        response.set_cookie('clean_sheet', data['refresh_token'])        redirect('https://your_subdomain.zendesk.com/agent/tickets/{}'.format(request.query.state))
  2. Replace the values of your_client_id, your_client_secret, and your_subdomain with your information.

How it works

The handle_decision() function starts by checking to see if the string 'error' appears in the query string:

if 'error' in request.query_string:    return request.query.error_description

In the Bottle framework, the request object contains the current HTTP request and the query_string property contains the request's query string, if any. If the word 'error' is found in the string, the app sends the error_description property in the HTTP response to the user's browser. Example:

The user denied the authorization request.

If 'error' is not found, then an authorization code must have been sent. The application retrieves the code from the query string and includes it with the other parameters that must be included in the request for an access token:

params = {    'grant_type': 'authorization_code',    'code': request.query.code,    'client_id': 'your_client_id',    'client_secret': 'your_client_secret',    'redirect_uri': 'https://my-example-app.herokuapp.com/auth/handle_decision'}
  • 'grant_type' is the string 'authorization_code' because you're submitting an authorization code
  • 'code' is the actual authorization code, which is retrieved from the query string using the Bottle expression request.query.code
  • 'client_id' and 'client_secret' are the values from the registration page
  • The 'redirect_uri' value is the same redirect URL as before

Next, make the POST request to Asana endpoint to get an access token:

url = 'https://app.asana.com/-/oauth_token'r = requests.post(url, data=params)

The requests.post() method from the requests library makes the request. The response from Asana is assigned to the r variable. Among other things, it should contain an access token and a refresh token.

The application checks for errors:

if r.status_code != 200:    error_msg = 'Failed to get access token with error {}'.format(r.status_code)    return error_msg

If the request was successful (the HTTP status code is 200), the application builds the HTTP response and sends it to the user's browser:

else:    data = r.json()    response.set_cookie('sheet', data['access_token'], max_age=data['expires_in'])    response.set_cookie('clean_sheet', data['refresh_token'])    redirect('https://your_subdomain.zendesk.com/agent/tickets/{}'.format(request.query.state))

The HTTP response carries instructions to set two cookies in the user's browser:

  • The 'sheet' cookie contains the access token (data['access_token']) and the cookie's expiry time in seconds (data['expires_in']). An access token from Asana is valid for about 1 hour. The cookie is deleted automatically when the time expires.
  • The 'clean_sheet' cookie contains a refresh token (data['refresh_token']). When the access token expires, you can exchange the refresh token for a new access token. You don't have to go through the whole authorization flow again. A refresh token from Asana is valid for about 1 year.

Finally, the user is redirected back to the Zendesk Support ticket they were viewing when they kicked off the authorization flow.

Note: To keep things simple, the tokens are unencrypted on the user's machine. Encrypting them is relatively simple with a third-party library like cryptography. See Fernet (symmetric encryption) in their docs.

Pass the token to the Zendesk app

The access token is now stored in a cookie on the user's computer. When the user needs it, the application has to get it and pass it to the Zendesk app.

The Zendesk app uses a nested iframe to communicate with the server application. When it needs a token, the Zendesk app creates an iframe that requests the following page from the server:

<iframe src="https://my-example-app.herokuapp.com/auth/user_token" ... >

The Zendesk app listens for a message event from the page in the iframe:

$(window).on("message", function (event) {      ...      if (msg.token == 'undefined') {        startAuth(client);      } else {        ...      }    });

The handler expects the message to contain either a valid access token or the string 'undefined'.

In this section, you'll update the Bottle application to get the cookie and pass an access token to the Zendesk app through the iframe. Briefly, the process has the following logic:

  1. When the iframe requests the server page, all existing cookies in the page's domain (my-example-app.herokuapp.com in this case) are sent to the server in the HTTP request
  2. The Bottle application checks the request to see if the cookie with the access token exists
  3. If the cookie exists, the application inserts the access token in the page sent to the iframe
  4. If the cookie doesn't exist, the application checks to see if a cookie with the refresh token exists
  5. If the second cookie exists, the application exchanges the refresh token for a new access token and inserts it in the page sent to the iframe
  6. If neither cookie exists, the application inserts 'undefined' in the page sent to the iframe
  7. When the page loads in the iframe, it sends a message to the app containing either a valid token or 'undefined'

To pass the token to the Zendesk app

  1. Add the following route for /auth/user_token. Make sure your code is indented as shown.

    @route('/auth/user_token')def get_cookies():    access_token = request.get_cookie('sheet')    refresh_token = request.get_cookie('clean_sheet')    if access_token:        token = access_token    elif refresh_token:        params = {            'grant_type': 'refresh_token',            'refresh_token': refresh_token,            'client_id': 'your_client_id',            'client_secret': 'your_client_secret',            'redirect_uri': 'https://my-example-app.herokuapp.com/auth/handle_decision'        }        url = 'https://app.asana.com/-/oauth_token'        r = requests.post(url, data=params)        if r.status_code != 200:            error_msg = 'Failed to get access token with error {}'.format(r.status_code)            return error_msg        else:            data = r.json()            response.set_cookie('sheet', data['access_token'], max_age=data['expires_in'])            token = data['access_token']    else:        token = 'undefined'    return template('auth', token=token)
  2. Replace the values of your_client_id and your_client_secret with your information.
  3. Create a folder called views in your asana_auth folder.
  4. In the new folder, create a text file named auth.tpl.

    A .tpl file is a Bottle template. It works like a Handlebars template in a Zendesk app.

  5. Add the following code to the file:

    <script type="text/javascript">  window.parent.postMessage({token: '{{token}}'}, '*');</script>

    The code sends a message with the token value to the Zendesk app.

How the route works

The route runs the get_cookies() function. The first thing the function does is check for two cookies in the HTTP request:

access_token = request.get_cookie('sheet')refresh_token = request.get_cookie('clean_sheet')

If the access token exists, it assigns it to the token variable.

if access_token:    token = access_token

If it doesn't exist, the function checks the refresh token. If it exists, the function makes a POST request to exchange it for a new access token. It starts by preparing the request:

elif refresh_token:    params = {        'grant_type': 'refresh_token',        'refresh_token': refresh_token,        'client_id': '{your_client_id}',        'client_secret': '{your_client_secret}',        'redirect_uri': 'https://my-example-app.herokuapp.com/auth/handle_decision'    }

Because it's submitting a refresh token, the 'grant_type' is the string 'refresh_token'. Also, a new parameter named 'refresh_token' replaces the 'code' parameter.

Next, the function makes the POST request to the Asana endpoint:

url = 'https://app.asana.com/-/oauth_token'    r = requests.post(url, data=params)

The response from Asana is assigned to the r variable. It should contain the new access token. If there are no errors, the function assigns the new token to the token variable and sets a new cookie:

data = r.json()     token = data['access_token']    response.set_cookie('sheet', token, max_age=data['expires_in'])

If neither cookies exist, the function assigns the string 'undefined' to the token variable:

else:    token = 'undefined'

Finally, the function passes the token variable to the 'auth' template to be sent in the HTTP response:

return template('auth', token=token)

How the template works

The template contains JavaScript for sending a message to the Zendesk app from the iframe. It has no display elements.

First, a script uses the JavaScript postMessage() method to send a message event to the window's parent -- in this case, your app's iframe.html page:

window.parent.postMessage({token: '{{token}}'}, '*');

For more information, see Window.postMessage() on the Mozilla Developer Network.

The token variable passed by the template() method is used as an argument in postMessage(). This is the message passed to the Zendesk app.

The Zendesk app receives the message and uses the value in the message handler. Snippet from main.js file:

if (msg.token == 'undefined') {  startAuth(client);} else {  ...}

The Bottle application is complete.

Code complete

Here's the completed auth.py file for the external application:

All the files for both applications are attached to this article.

Deploy the application to Heroku

When you're ready to test and deploy your application, move the Bottle application to a web server. If you don't have access to one at work, you can use a PaaS (platform as a service) option like Heroku. Depending on the service, you can probably deploy it for free. The Bottle application shouldn't require a lot of server resources because it's only used by agents or admins, not the general public.

  1. For step-by-step instructions, see Deploying a Bottle web app on Heroku on Github. Keep in mind the following notes when you carry out the procedure:

  2. Once deployed, replace all instances of the https://my-example-app.herokuapp.com domain in your app with the new Heroku domain. Example: https://fast-sierra-15737.herokuapp.com. The domain appears in the following places:

    • Asana - the redirect_uri value on the registration page. See Register the application with Asana above
    • auth.py - the redirect_uri value everywhere. Remember to push the changes to Heroku. See Push updates to Heroku on Github
    • iframe.html - the URL in both the start_auth-hdbs and auth_iframe-hdbs templates
    • main.js - the origin comparison in the message handler (if (origin !== "https://my-example-app.herokuapp.com")...)

Test the integration

You should test that everything works.

  1. In your command-line interface, navigate to the add_oauth folder containing your Zendesk app files.
  2. Run the following command to start the Zendesk app:

    $ zat server
  3. In a web browser, navigate to any ticket in Zendesk Support. The URL should look something like this:

    https://subdomain.zendesk.com/agent/tickets/321

    Note: To ensure the cookies are set and saved normally, use a normal browser window, not an incognito or private one.

  4. Append ?zat=true to the ticket URL, and reload the page. The URL should look like this:

    https://subdomain.zendesk.com/agent/tickets/321?zat=true

  5. If you're using Chrome or Firefox and nothing happens, look for a shield icon in the Address bar. The browsers block the app from loading in Zendesk Support locally. Click the shield icon and agree to load an unsafe script (Chrome) or to disable protection on the page (Firefox).
  6. Test the new authentication and debug any problems.

    If something goes wrong, you can check the activity logs provided by Heroku:

    1. Open a new command-line window.
    2. Navigate to the folder containing your Python app.
    3. Log in to Heroku (heroku login).
    4. Run heroku logs --tail. The tail option displays the newest log entries after the older ones.
    5. Check the entries for clues of what went wrong.

    You can leave the log window open as you debug and update the app.

    Important: If you make any modifications to the auth.py file, remember to push the updates to Heroku. See Push updates to Heroku on Github.

  7. When you're done with the debugging session, you can shut down the ZAT server by switching to the CLI and pressing Control+C.

A successful test should look as follows when using the local ZAT server:

  1. In your app, click the Add a task to Asana button.

  2. If this is the first time you run the app, you should see the Sign In with Asana button. Click it.

    The app sends a request to your Bottle application, which redirects you to the Asana authorization page with the required parameters. If you're not currently signed in to Asana, you'll be asked to sign in first.

  3. After returning to the ticket after granting authorization, append ?zat=true to the ticket URL, and reload the page. If the app doesn't appear, remember to click the browser shield icon again and agree to load an unsafe script (Chrome) or to disable protection on the page (Firefox).
  4. Clicking Add a task to Asana again should display the task form now. It means the app has received an access token.

  5. From here, you should be able to add a task to the project in Asana.