In the previous tutorial in this series, you set up a Zendesk app that integrates Asana functionality, and created an Asana project. In this tutorial, you'll modify the app to use OAuth authentication when it makes API requests to Asana. The tutorial covers the following tasks:

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

Sample app code review

The main.js file of the sample app consists of the following 3 functions:

$(function() {  var client = ZAFClient.init();  client.invoke('resize', { width: '100%', height: '400px' });  showForm(client);});
function showForm(client) {  var source = $("#add_task-hdbs").html();  var template = Handlebars.compile(source);  var html = template();  $("#content").html(html);
  $("#add-btn").click(function(event) {    event.preventDefault();    if ($("#name").val().length == 0) {      client.invoke('notify', 'Name can\'t be blank.', 'error');    } else {                  // good to go      var task = {        data: {          name: $("#name").val(),          notes: $("#notes").val(),          projects: [parseInt($("#project-id").val())]        }      };      sendTaskData(task, client);    }  });}
function sendTaskData(task, client) {  token = '0/863edfasfcd7ad90be6fa';  var settings = {    url: 'https://app.asana.com/api/1.0/tasks',    headers: {"Authorization": "Bearer " + token},    type: 'POST',    contentType: 'application/json',    data: JSON.stringify(task)  };  client.request(settings).then(    function() {      client.invoke('notify', 'Task successfully added to Asana.');      $('#task-form')[0].reset();    },    function(response) {      var msg = 'Error ' + response.status + ' ' + response.statusText;      client.invoke('notify', msg, 'error');    }  );  client.invoke('notify', 'Task sent! Please wait...');}

The first function is a self-invoking anonymous function that runs after the iframe.html file is loaded in the browser. It creates the ZAF client, resizes the window, and runs the showForm() function.

The second function, showForm(), displays the form and gathers the form data when the user clicks the button with the add-btn id.

The third function, sendTaskData(), makes the POST request to the Asana API with the task data. This function uses a personal access token from Asana to authenticate the requests.

The app's user interface, iframe.html, consists of the following markup:

<html><head>  <link href="https://cdn.jsdelivr.net/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet">  <link href="main.css" rel="stylesheet"></head><body>  <div id="content"></div>
  <script id="add_task-hdbs" type="text/x-handlebars-template">    <form id="task-form">    <label for="name">Task name</label>    <input type="text" name="name" id="name" maxlength="50" placeholder="50 characters or less" />    <label for="notes">Notes</label>    <textarea name="notes" id="notes" placeholder="optional" />    <input type="hidden" name="project-id" id="project-id" value="1556596472676" />    <input type="hidden" name="project-name" id="project-name" value="Tech Notes" />    <button id="add-btn" class="btn btn-primary btn-small ">Add Task</button>    </form>  </script>
  <script src="https://cdn.jsdelivr.net/jquery/3.0.0/jquery.min.js"></script>  <script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.5/handlebars.min.js"></script>  <script type="text/javascript" src="https://assets.zendesk.com/apps/sdk/2.0/zaf_sdk.js"></script>  <script type="text/javascript" src="main.js"></script></body></html>

The page consists of a form with fields for the task name and for notes, as well as 2 hidden fields containing the Asana project name and project ID.

Planned changes

Before the user adds each task, the app should check to see if a cookie containing an OAuth access token exists in the user's browser.

  • If a token exists, the app should use it to make requests to the Asana API.
  • If a token doesn't exist, the app should kick off the authorization flow to get the token.

This requires making the following changes to the sample app:

  • Show a start page with a button that triggers a check for the token before the form is displayed. You don't want users to complete the form if they don't have a token. They first need to grant your app permission to create tasks in Asana
  • To get around cross domain issues of accessing cookies, create an iframe in your app's iframe to check for the token, and then pass the results to the parent iframe using the postMessage() method in JavaScript
  • If the token exists, use it in the POST request to Asana
  • If the token doesn't exist, show a button to kick off the OAuth authorization flow

Show a start page before displaying the form

The current version of the app displays a form for adding a task when the app starts. After an agent adds a task, the form resets. The first change is to show a button that agents must click before they can view the form. The click event will send a signal to the app to check if the user has a token before displaying the form.

What happens after the user clicks the button is covered in the next section.

To show a button before the user adds a task

  1. Add the following Handlebars template in the iframe.html file and save the file:

    <script id="start-hdbs" type="text/x-handlebars-template">  <button class="btn-large btn-primary" type="button" id="check-token">Add a task to Asana</button></script>
  2. Switch to the main.js file and add the following function before the showForm() function:

    function showStart() {  var source = $("#start-hdbs").html();  var template = Handlebars.compile(source);  var html = template();  $("#content").html(html);}

    The app displays the start-hdbs template when the showStart() function is called.

  3. In the anonymous function at the top of the file, replace the following statement:

    showForm(client);

    with the following statement:

    showStart();

    Instead of showing the task form when the iframe finishes loading, the app shows the start page. The showStart() function doesn't use the ZAF client, so you don't need to pass it as an argument.

  4. Locate the sendTaskData() function and replace the following statement:

    $('#task-form')[0].reset();

    with the following statement:

    window.location.reload();

    This reloads the app for a fresh start. Instead of resetting the form after adding a task, the app should show the "Add a task to Asana" button again.

Check for the token

In a later part of this tutorial, you'll create a small server-side app to get an OAuth token from Asana and store it in a cookie on the user's system. Unfortunately, as discussed in Part 1 of the tutorial, browser cross domain issues prevent Zendesk apps from reading cookies set by applications in other domains.

To get around the restriction, you can embed an iframe in your app that requests a page containing the token from the server, and then use the postMessage() method in JavaScript to send the token to the parent page.

You'll create the requested page in Part 3 of the tutorial. In this section, you create the iframe that loads the page, and then add logic to handle the message it sends to the parent page. If the message contains a valid token, then the app should display the form to add a task. If the message says that the token is undefined, then the app should switch to a template that lets the user kick off the authorization flow.

Create the iframe

  1. Add the following Handlebars template in the iframe.html file and save the file:

    <script id="auth_iframe-hdbs" type="text/x-handlebars-template">  <iframe src="https://my-example-app.herokuapp.com/auth/user_token" frameborder="0">  </iframe></script>

    The <iframe> tag loads the user_token page specified in src in an iframe in your app page. You'll create the user_token page in Part 3 of the tutorial. The page will only contain JavaScript so nothing will be displayed in the app page.

  2. Switch to the main.js file and add the following event handler in the anonymous function at the top of the file:

    $("#check-token").click(function(event) {  event.preventDefault();  client.invoke('notify', 'Authenticating...');  var source = $("#auth_iframe-hdbs").html();  var template = Handlebars.compile(source);  var html = template();  $("#content").html(html);});

    When the user clicks the element with the check-token id, the handler notifies the user it's authenticating them, then compiles and displays your new auth_iframe-hdbs template. The check-token id is the id of the "Add a Task to Asana" button in the start-hdbs template you added in the previous section. The iframe requests the user_token page from the server, which kicks off the server-side process that gets a token.

  3. After the process to get a token has started, listen for news of the result with the following event handler:

    $(window).on("message", function(event) {  var origin = event.origin || event.originalEvent.origin;  if (origin !== "https://my-example-app.herokuapp.com")    return;  var msg = event.data || event.originalEvent.data;  if (msg.token == 'undefined') {    startAuth(client);  } else {    showForm(msg.token, client);  }});

    The user_token page loaded in the iframe will contain either a valid token or the string "undefined". The loaded page sends the information in a message to the iframe's parent page, which in this case is your iframe.html page. (You'll add this functionality in Part 3 of the tutorial.) When the parent page receives the message, the event handler detects it:

    $(window).on("message", ...)

    However, this expression detects any message. For example, your iframe.html page also receives messages from Zendesk, which the handler also detects. To identify and ignore these messages, the handler checks the origin of the message. It should be the domain of the url specified in the iframe in your auth_iframe-hdbs template -- https://my-example-app.herokuapp.com:

    var origin = event.origin || event.originalEvent.origin;if (origin !== "https://my-example-app.herokuapp.com")  return;

    Note: While most browsers pass message properties in the event object, Chrome passes them in an object named event.originalEvent.

    After confirming the message originated from your iframe, the handler retrieves the message information:

    var msg = event.data || event.originalEvent.data;

    The data assigned to the msg variable is a {token: 'value'} object (which you'll create in Part 3 of the tutorial). As a result, the msg.token expression will either be an access token or 'undefined'.

    If the message doesn't contain an access token -- if (msg.token == 'undefined') -- the handler runs startAuth() to kick off the authorization flow. See Token not found: Start the authorization flow below.

    If the message contains an access token -- if msg.token is anything but 'undefined' -- the handler passes the token to the showForm() function and runs it. It also passes the client object to the function so it doesn't go out of scope:

    } else {  showForm(msg.token, client);}

    See Token found: Use it in the request below.

Token found: Use it in the request

Let's take the case where the app gets an access token. Before you can use it in an API request, you must get the task information for the request from the user. The sample app has a template with a form to collect the information from the user. It uses a function named showForm() to display the form template, and then collect the form data when the user clicks the button:

function showForm(client) {  var source = $("#add_task-hdbs").html();  var template = Handlebars.compile(source);  var html = template();  $("#content").html(html);
  $("#add-btn").click(function(event) {    event.preventDefault();    if ($("#name").val().length == 0) {      client.invoke('notify', 'Name can\'t be blank.', 'error');    } else {                  // good to go      var task = {        data: {          name: $("#name").val(),          notes: $("#notes").val(),          projects: [parseInt($("#project-id").val())]        }      };      sendTaskData(task, client);    }  });}

The message handler in the previous section passes an additional argument, msg.token, to this function. Therefore, the showForm() function should be modified to:

  • receive the token as an argument
  • pass the token to sendTaskData(), the function tasked with making the Asana API request

To route the token to the API request

  1. Add an argument named token to the showForm() function signature:

    function showForm(token, client) {
      // rest of function
    }
  2. In the function's add-btn event handler, add the token variable as an argument to the sendTaskData() function (in bold):

    function showForm(token, client) {
    
      // form template code here...
    
      $("#add-btn").click(function(event) {
        event.preventDefault();
        if ($("#name").val().length == 0) {
          client.invoke('notify', 'Name can\'t be blank.', 'error');
        } else {    
          // bundle the form data in a task variable
          sendTaskData(task, token, client);
        }
      });  
    }

    The sendTaskData() function makes the actual API request. It needs the token to authorize the request.

  3. Locate the sendTaskData() function definition and add an argument named token to its signature (in bold):

    function sendTaskData(task, token, client) {
      // rest of function
    }
  4. Delete the token variable assignment:

    function sendTaskData(task, token, client) {
      token = '0/863edfasfcd7ad90be6fa';
      // rest of function
    }

    You don't need it anymore because the token's value is now provided by the token argument:

    function sendTaskData(task, token, client) {
      var settings = {
        url: 'https://app.asana.com/api/1.0/tasks',
        headers: {"Authorization": "Bearer " + token},
        ...
     };
     // rest of function
    }

    The app is now configured to use OAuth access tokens to make authenticated API requests.

Token not found: Start the authorization flow

Now let's take the case where the app receives an 'undefined' value after checking for a token. In that case, the message handler calls the startAuth() function (in bold):

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

The startAuth() function should show a page with a button that lets the agent kick off the authorization flow:

To show a button that starts the authorization flow

  1. Add the following startAuth() function to main.js:

    function startAuth(client) {  client.get('ticket.id').then(    function(data) {      var ticket_id = data['ticket.id'];      var source = $("#start_auth-hdbs").html();      var template = Handlebars.compile(source);      var html = template({state: ticket_id});      $("#content").html(html);    }  );}

    The function retrieves and passes the id of the currently opened ticket to a template named start_auth-hdbs. The id is passed to the template in the {state: ticket_id} object in the template() method.

    Why get the ticket id? The application state is lost when the user leaves Zendesk Support to go to Asana to grant permission. The state parameter is a standard OAuth parameter you can use to redirect the user to the original page in Zendesk Support after they grant permission in Asana.

  2. Right-click the following button and download it to your app's assets folder:

    Asana provides the button to developers for authentication. A purple version is also available on Github.

  3. In your iframe.html file, create the start_auth-hdbs template:

    <script id="start_auth-hdbs" type="text/x-handlebars-template">  <a id="start-auth" href="https://my-example-app.herokuapp.com/auth/asana?state={{state}}"  target="_blank">    <img src="asana-oauth-button-white.png" alt="sign-in button" />  </a></script>

    When the user clicks the button, the app requests the /auth/asana resource from your external application. You haven't built it yet but the application will package a few parameters and redirect the user to the Asana website to sign in and grant permission.

    The Handlebars expression, {{state}}, inserts the ticket id as the value of the state parameter in the URL.

    The template should look like this in the app:

    After your server-side application processes the request, it redirects the user to the Asana authorization page (or to the sign-in page first if they're signed out):

App code complete

Here's the completed main.js file:

Here's the completed iframe.html file:

Next steps

In this part of the tutorial, you modified the Zendesk app to support OAuth authentication. In the next part, you'll build a lightweight web app to manage the authorization flow. Click to go to the next tutorial now: Part 3: Managing the authorization flow.