Create an Oauth2 integration

In this guide we will create an application that will integrate with Github and will show a list of repositories belonging to a ticket's owner in the ticket sidebar.

The aim of this guide is to document working with Oauth2 and to demonstrate how you can customize the application installer by adding a custom settings screen that validates the Github connection details by requesting an OAuth token.

You can view the complete app we build during this guide here: https://github.com/DeskproApps/dev-guide-oauth2

Github Authentication Basics

If you are not familiar with Github's authentication, it is recommended to read their guide on the matter: https://developer.github.com/v3/guides/basics-of-authentication

Later on this guide you will have to register an application, so make sure you read also the chapter Registering your app.

Deskpro Oauth2 Basics

Working with OAuth2 in Deskpro is different based on the type of OAuth2 flow supported by the provider. If the provider offers support for web based applications (2-legged flow) than you don't need to use any secure storage, but if this is not the case you will have to use secure storage and generic OAuth2 client that comes with the SDK. This tutorial focuses on the latter use cases.

Deskpro supports the authorize code and the refresh token flows, meaning that you will be able to also refresh tokens if the provider you are working with supports that.

Working with any OAuth2 provider without any support for web applications involves four steps:

  • configuring OAuth2 in your application manifest

  • reading application OAuth2 settings and storing a connection to an OAuth2 provider

  • obtaining an access token

  • making an authorized request

The SDK offers an OAuthFacade client for these operations which will be discussed with examples in the following chapters.

Building the application

Let's begin by cloning the boilerplate repository:

    git clone https://github.com/deskpro/apps-boilerplate dev-guide-oauth2
    cd dev-guide-api-key
    rm -rf .git && git init .

Open the file package.json and edit the following properties to replace references to the boilerplate application:

{
  "name": "dev-guide-oauth2",
  "version": "0.1.0",
  "description": "Create an OAuth2 integration",
  "deskpro": {
    "title": "dev-guide-oauth2"
  }
}

Configure OAuth2 storage and access

Add the following properties to the file package.json:

{
  "deskpro": {
    "storage": [
      {
        "name": "oauth:github",
        "isBackendOnly": true,
        "permRead": "EVERYBODY",
        "permWrite": "OWNER"
      },
      {
        "name": "oauth:github:tokens",
        "isBackendOnly": true,
        "permRead": "OWNER",
        "permWrite": "OWNER"
      }
    ],
    "externalApis": [
      "/^https?://([^.]+\\.)*github.com/?.*$/"
    ]
  }
}

You noticed we added two storage items, oauth:github and oauth:github:tokens. The former is used to store the connection information for our provider, Github while the latter is used to store the individual tokens granted to our app users. We also whitelisted requests to the github.com domain so we can use the proxy to make authenticated requests.

The connection information deserves more coverage. First of all, the connection requires that a storage item named oauth:<PROVIDER IDENTIFIER> must be defined in the storage section of your manifest. This convention based approach allows the server-side to quickly lookup any connection information from application storage.

The connection information is an object with the following structure:

  {
     "urlAuthorize":  "the url of the authorize endpoint",
     "urlAccessToken": "the url of the token endpoint",
     "urlRedirect": "the url the oauth provider will redirect the browser after the user grants access",
     "urlResourceOwnerDetails": "optional. an url used to retrieve information about the token's owner",
     "clientId": "the id of the client registered with the oauth provider",
     "clientSecret": "optional. an optional client secret"  
  }

This object usually includes sensitive information which should not be made public but needs to be used for authentication purposes, which is why it is recommended you always secure it as shown in the example above.

Configure OAuth2 settings

We have to allow an administrator to configure the application by setting their own OAuth2 credentials. We also want to provide a way for us to check their credentials are correct. The easiest way to do this is to add a list of settings and create a custom settings screen that can also verify the credentials by requesting an access token.

Add the following entries to your manifest in package.json:

{
  "deskpro": {
    "settings": [
      {
        "name": "githubClientId",
        "defaultValue": "",
        "title": "Github Client ID",
        "required": true,
        "type": "text"
      },
      {
        "name": "githubClientSecret",
        "defaultValue": "",
        "title": "Github Client Secret",
        "required": true,
        "type": "text"
      },
      {
        "name": "githubCallbackURL",
        "defaultValue": "",
        "title": "Github authorization callback URL",
        "required": true,
        "type": "text"
      }      
    ]
  },
  "devDependencies": {
    "@deskpro/apps-installer": "github:deskpro/apps-installer#v0.4.2"
  }  
}

Here we have two settings which will provided by the administrator (githubClientId and githubClientSecret) and githubCallbackURL which we will pre-fill as it will be required to register the application with Github.

Since we want to add a custom settings screen we need the installer source files and so we declare a dependency to the installer source package. Later, during the compile and package phases the build tool will discover our settings screen and will compile it along with the installer sources into a custom installer that will be added to our application bundle.

Don't forget to run npm run install to install the newly added dependency.

Adding the settings screen

We will add our own custom settings screen which will verify the OAuth2 credentials given by the administrator by requesting an OAuth2 token. If the operation is successful, the installation will continue otherwise an error message will be displayed. This is an approach we recommend whenever the administrator is required to configure an application.

Create a new file at src/installer/javascript/index.js. By convention, this module will be automatically included during the compile phase and its default export must be a React component. Next, set the file contents:

import React from 'react';
import PropTypes from 'prop-types';

class ScreenSettings extends React.Component {

  static propTypes = {
    /**
     * @type function a callback to invoke to finish the installation
     */
    finishInstall: PropTypes.func.isRequired,

    /**
     * @type string they type of installation: install or update
     */
    installType: PropTypes.string.isRequired,

    /**
     * @type {Array<Object>} the list of settings defined in the application manifest
     */
    settings: PropTypes.array.isRequired,

    /**
     * @type {Object} a map of setting name and value
     */
    values: PropTypes.object.isRequired,

    /**
     * @type {function} the settings form constructor
     */
    settingsForm: PropTypes.func.isRequired,

    /**
     * @type {AppClient} the Deskpro Application Client
     * @see https://deskpro.github.io/apps-sdk-core/reference/AppClient.html
     */
    dpapp: PropTypes.object.isRequired
  };

  constructor() {
    super();
    // initialize our state
    this.state = { values: null, error: null };
  }

  onBeforeSubmit(values) { return Promise.resolve(values); }

  /**
   * Callback, invoked when the settings form is submitted
   *
   * @param {Object} values
   */
  onSubmit = (values) => {
    const { finishInstall } = this.props;

    this.onBeforeSubmit(values)
      .then(values => finishInstall(values).then(({ onStatus }) => onStatus())) // finish install
      .catch(error => this.setState({ error })) // display an error message
    ;
  };

  /**
   * Displays the settings screen
   *
   * @returns {XML}
   */
  render() {

    const { settings, settingsForm: SettingsForm } = this.props;
    const values = this.state.values || this.props.values;

    let formRef;
    return (
      <div>
        <SettingsForm settings={ settings } values={ values } onSubmit={this.onSubmit} ref={ref => formRef = ref} />
        <button className={'btn-action'} onClick={() => formRef.submit()}>Update Settings</button>

        { this.renderError() }

      </div>
    );
  }

  /**
   * Renders an error message
   *
   * @returns {XML|null}
   */
  renderError()
  {
    if (this.state.error) {
      return (<div style={{backgroundColor: "red", color:"white", padding: "2%"}}>{ this.state.error }</div>);
    }

    return null;
  }
}

export default ScreenSettings;

This is just a React component implementation of a settings screen. Functionally, it is not different than the default settings screen. However it illustrates how you can hook into the install process and how you can add your on onSubmit handler where you can further manipulate the setting values.

Read the default OAuth2 settings

If you application is using OAuth2 it is more than likely the administrator will have to create an OAuth2 client on the OAuth provider's website. Many times the provider will ask for a callback url to redirect the browser to after the user has authorized your application and this is also the case for Github.

Each application has its own callback URL, which can be retrieved by calling the OauthFacade.settings method. Some of the settings can only be known at runtime, so it is recommended to always use the OauthFacade.settings.

For our settings screen, we want to show the callback URL in the settings form so the administrator can use it when registering the application on Github. Add the following methods to the ScreenSettings class after the body of the constructor method :

  componentDidMount() { this.setDefaultValues(); }

  /**
   * Retrieves the application oauth2 settings, such as the callback url
   */
  setDefaultValues()
  {
    // obtain a reference to the OAuth client
    const {
      /**
       * @type {OauthFacade} @see https://deskpro.github.io/apps-sdk-core/reference/OauthFacade.html
       */
      oauth
    } = this.props.dpapp;

    const { values } = this.props;

    // retrieve the default oauth settings for the app such as the redirect url
    oauth.settings('github')
      .then( oauthSettings => this.setState({
        values: {  ...values, githubCallbackURL: oauthSettings.urlRedirect }
      }))
      .catch( error => this.setState({ values: null, error: 'Failed to read default app oauth settings' }) )
    ;
  }

Here we are asking for the OAuth settings using our provider's name ( github ) from the Deskpro instance that is hosting our application. Notice that the provider's name, github is also used in the manifest for the storage key of the OAuth connection, oauth:github.

We then assign the value of the urlRedirect property to our githubCallbackURL setting in the values map.

If we were to package the application and install the application up to this point, we will see the following screen:

When you get to this screen, your next action should be to copy the value of the Github callback URL fields and head over to Github and register your application: https://github.com/settings/applications/new . Paste the value into the Authorization callback URL field in Github and fill in the other fields:

On the next page, after clicking Register application you will be presented with a Client ID and a Client Secret which you should copy into the Github client ID and Github client secret fields from the settings from in Deskpro.

Save the OAuth2 settings

Replace the onBeforeSubmit hook with the following code:

  /**
   * Hook method called before the actual submit takes place.
   * Checks the form values by obtaining an access token
   * 
   * @param {Object} values
   * @returns {Promise.<Object, String>}
   */
  onBeforeSubmit(values)
  {
    // build the connnection information
    const connection = {
      urlRedirect: values.githubCallbackURL,
      urlAuthorize: `https://github.com/login/oauth/authorize`,
      urlAccessToken: `https://github.com/login/oauth/access_token`,
      clientId: values.githubClientId,
      clientSecret: values.githubClientSecret
    };

    const { oauth } = this.props.dpapp;
    return oauth.register('github', connection)            // register a connection to provider
      .then(connection => oauth.access('github'))          // request an oauth token from the provider
      .then(token => ({ ...values, githubClientSecret: "***" })  )
      .catch(error => Promise.reject('Invalid Oauth settings')) // display an error message
      ;
  }

A number of things happen here. This hook gets invoked with whatever values we have input into the form and the first thing that we do is create the connection object for our provider.

Then by calling OauthFacade.register we register the connection and we can immediately test the connection by calling the OauthFacade.access method. Notice that the provider's name, github is also used in the manifest for the storage key of the OAuth connection, oauth:github. The OauthFacade.register method stores the connection objection into oauth:github storage key we have declared in the application manifest.

If the information is correct, we will receive an OAuth2 token from Github and the next thing we do is hide the actual value of githubClientSecret since we don't want it to be stored as a setting, which can be read or written to by anybody.

Packaging and installing the application

Run the following command:

  npm run package

A file dist/app.zip was created. Go to the Deskpro Admin interface, open the Apps Menu and upload this file to start the installation process:

Click the green Install App button to advance to the next screen.

When you get to this screen, your next action should be to copy the value of the Github callback URL fields and head over to Github and register your application: https://github.com/settings/applications/new . Paste the value into the Authorization callback URL field in Github and fill in the other fields:

On the next page, after clicking Register application you will be presented with a Client ID and a Client Secret which you should copy into the Github client ID and Github client secret fields from the settings from in Deskpro.

Click the green button to finish installing the application

Notice the Dev Mode button in the upper right corner. Run the following command to start the developer server, then click the button to open a new tab with Deskpro running your application in development mode so we can conclude our tutorial:

  npm run dev

Reading the ticket owner's email

Now that we have installed the application and we know our OAuth setings are correct, we can build the actual application. We will need the ticket owner's email to begin with, then we need to check if we have an OAuth2 Github token for our agent. Finally we need to retrieve the list of repositories.

Open the file src/main/javascript/App.jsx and add this method to read the ticket owner's email:

  /**
   * Returns the ticket owner's email
   * 
   * @returns {Promise.<String, Error>}
   */
  readTicketOwnerEmail()
  {
    const { context } = this.props.dpapp;
    return context.getTabData().then(tabData => {

      const { emails, primary_email } = tabData.api_data.person;

      if (primary_email && primary_email.email) {
        return primary_email.email;
      }

      if (emails.length) {
        return emails[0].email;
      }

      return Promise.reject(new Error('could not find the email of the ticket owner'));
    });
  }

There's nothing really special going on for this method. We are using the Context API to read the data stored in the ticket's UI Tab and return the email.

Next, we should check if we have a valid OAuth2 token:

  /**
   * Checks the agent has a valid token and if not, requests a new token
   *
   * @returns {Promise.<null, Error>}
   */
  checkOAuthToken()
  {
    const {
      /**
       * @type {OauthFacade} @see https://deskpro.github.io/apps-sdk-core/reference/OauthFacade.html
       */
      oauth,
      /**
       * @type {StorageApiFacade} @see https://deskpro.github.io/apps-sdk-core/reference/StorageApiFacade.html
       */
      storage,
      /**
       * @type {DeskproAPIClient} @see https://deskpro.github.io/apps-sdk-core/reference/DeskproAPIClient.html
       */
      restApi
    } = this.props.dpapp;

    const headers = {
      'Accept': 'application/vnd.github.v3+json',
      'Authorization': 'token {{oauth:github:tokens}}'
    };
    return restApi
      .fetchCORS(`https://api.github.com/user`, { method: 'GET', headers })
      .catch(error => oauth.access('github').then(token => storage.setAppStorage('oauth:github:tokens', token.access_token)))
    ;
  }

This methods first makes a request to the user endpoint using the value stored under the oauth:github:tokens key. Remember we configured this key to be private and therefore each agent will have their own value. We are using the request placeholder syntax because we do not want to risk the token being exposed.

If the user endpoint responds with an error, then we request another token using oauth.access('github') which we then store under the key oauth:github:tokens for further requests. We are simplifying the error handling procedure a bit, in a real application we would check the error.errorData property for information about the error, for instance we would look for a status code of 401 in our case.

Next let's add the method that reads the ticket owner's repositories:

  /**
   * Returns a list of Github repositories
   *
   * @param {string} email the ticket owner's email
   * @returns {Promise.<Array<Object>, Error>}
   */
  readRepositories(email)
  {
    const headers = {
      'Accept': 'application/vnd.github.v3+json',
      'Authorization': 'token {{oauth:github:tokens}}'
    };
    const query = `${email} in:email`;

    const { restApi } = this.props.dpapp;
    return restApi.fetchCORS(
      `https://api.github.com/search/users?q=${encodeURIComponent(query)}`, { method: 'GET', headers }
    )
      .then(response => {
        // we have a match, fetch the
        if (response.body.total_count === 1) {
          const { repos_url: listReposUrl } = response.body.items[0];
          return restApi.fetchCORS(listReposUrl, { method: 'GET', headers }).then(response => response.body)
        }
        return [];
      })
  }

This method looks similar to the one we added before. Here we are using again the request placeholder syntax to reference our OAuth token. We start by searching for a user with the same email address, then if we get a match we make another request to list the repositories.

Let's connect now everything we've written so far. Add this method that will coordinate the previous three and save the list of repositories so we can then display them:

  componentDidMount() {
    this.readTicketOwnerEmail()
      .then(email => this.checkOAuthToken().then(() => email))
      .then(email => this.readRepositories(email))
      .then(repos => this.setState({ repos }))
    ;
  }

Finally replace the render method with:

  render() {
    const state = this.state || {};
    const { repos } = state;

    if (repos) {
      const links = repos.map(repo => <li key={repo.id}>
          <h3><a href={repo.html_url} target="_blank" >{repo.name}</a></h3>
          <p>{repo.description}</p>
        </li>
      );
      return (<div><h3>Ticket owner's repositories</h3><ul>{links}</ul></div>);
    }

    return (
      <h3>Loading repositories...</h3>
    );
  }

While in development mode, create a ticket using your own email (we know you have a list of repositories in Github) then open it. If you followed this tutorial, you should see the following screen:

Followed by

Final recap

You have now seen how to create an integration powered by OAuth2. We have covered:

  • configuring OAuth2 in your application manifest

  • reading application OAuth2 settings

  • storing a connection to an OAuth2 provider

  • obtaining an OAuth2 access token

  • making authorized requests

  • creating a custom settings screen

Last updated