Connecting to non-Google Services

Your add-on project can directly connect to many Google products with Apps Script's built-in and advanced services.

You can also access non-Google APIs and services. If the service does not require authorization, you can typically just make an appropriate UrlFetch request and then have your add-on interpret the response.

However, if the non-Google service does require authorization, you'll have to configure OAuth for that service. You can make this process easier by using the OAuth2 for Apps Script library (there is also an OAuth1 version).

Using an OAuth service

When using an OAuth service object to connect to a non-Google service, your add-on needs to detect when authorization is required and, when it is, invoke the authorization flow.

The authorization flow consists of:

  1. Alerting the user that auth is needed and providing a link to start the process.
  2. Acquiring authorization from the non-Google service.
  3. Refreshing the add-on to retry accessing the protected resource.

When non-Google authorization is needed, the Gmail client handles these details. Your add-on only needs to detect when authorization is needed and invoke the authorization flow when necessary.

Detecting that authorization is required

A request may not have authorization to access a protected resource for a variety of reasons, such as:

  • The access token has not been generated yet or is expired.
  • The access token does not cover the requested resource.
  • The access token does not cover the request's required scopes.

Your add-on code should detect these cases. The OAuth library hasAccess() function can tell you if you currently have access to a service. Alternatively, when using UrlFetchApp fetch() requests, you can set the muteHttpExceptions parameter to true. This prevents the request from throwing an exception on request failure and allows you to examine the request response code and content in the returned HttpResponse object.

When the add-on detects that authorization is required, it should trigger the authorization flow.

Invoking the authorization flow

You invoke the authorization flow by using the Card service to create an AuthorizationException object, setting its properties, and then calling the throwException() function. Before throwing the exception, you provide the following:

  1. Required. An authorization URL. This is specified by the non-Google service and is the location the user is taken to when the authorization flow starts. You set this URL using the setAuthorizationUrl() function.
  2. Required. A resource display name string. Identifies the resource to the user when authorization is requested. You set this name using the setResourceDisplayName() function.
  3. The name of a callback function that creates a custom authorization prompt. This callback returns an array of built Card objects that compose a UI for handling authorization. This is optional; if not set the default authorization card is used. You set the callback function using the setCustomUiCallback() function.

Non-Google OAuth configuration example

This code sample shows how to configure an add-on to use a non-Google API requiring OAuth. It makes use of the OAuth2 for Apps Script to construct a service for accessing the API.

/**
 * Attempts to access a non-Google API using a constructed service
 * object.
 *
 * If your add-on needs access to non-Google APIs that require OAuth,
 * you need to implement this method. You can use the OAuth1 and
 * OAuth2 Apps Script libraries to help implement it.
 *
 * @param {String} url         The URL to access.
 * @param {String} method_opt  The HTTP method. Defaults to GET.
 * @param {Object} headers_opt The HTTP headers. Defaults to an empty
 *                             object. The Authorization field is added
 *                             to the headers in this method.
 * @returns {HttpResponse} the result from the UrlFetchApp.fetch() call.
 */
function accessProtectedResource(url, method_opt, headers_opt) {
  var service = getOAuthService();
  var maybeAuthorized = service.hasAccess();
  if (maybeAuthorized) {
    // A token is present, but it may be expired or invalid. Make a
    // request and check the response code to be sure.

    // Make the UrlFetch request and return the result.
    var accessToken = service.getAccessToken();
    var method = method_opt || 'get';
    var headers = headers_opt || {};
    headers['Authorization'] =
        Utilities.formatString('Bearer %s', accessToken);
    var resp = UrlFetchApp.fetch(url, {
      'headers': headers,
      'method' : method,
      'muteHttpExceptions': true, // Prevents thrown HTTP exceptions.
    });

    var code = resp.getResponseCode();
    if (code >= 200 && code < 300) {
      return resp.getContentText("utf-8"); // Success
    } else if (code == 401 || code == 403) {
       // Not fully authorized for this action.
       maybeAuthorized = false;
    } else {
       // Handle other response codes by logging them and throwing an
       // exception.
       console.error("Backend server error (%s): %s", code.toString(),
                     resp.getContentText("utf-8"));
       throw ("Backend server error: " + code);
    }
  }

  if (!maybeAuthorized) {
    // Invoke the authorization flow using the default authorization
    // prompt card.
    CardService.newAuthorizationException()
        .setAuthorizationUrl(service.getAuthorizationUrl())
        .setResourceDisplayName("Display name to show to the user")
        .throwException();
  }
}

/**
 * Create a new OAuth service to facilitate accessing an API.
 * This example assumes there is a single service that the add-on needs to
 * access. Its name is used when persisting the authorized token, so ensure
 * it is unique within the scope of the property store. You must set the
 * client secret and client ID, which are obtained when registering your
 * add-on with the API.
 *
 * See the Apps Script OAuth2 Library documentation for more
 * information:
 *   https://github.com/googlesamples/apps-script-oauth2#1-create-the-oauth2-service
 *
 *  @returns A configured OAuth2 service object.
 */
function getOAuthService() {
  return OAuth2.createService('SERVICE_NAME')
      .setAuthorizationBaseUrl('SERVICE_AUTH_URL')
      .setTokenUrl('SERVICE_AUTH_TOKEN_URL')
      .setClientId('CLIENT_ID')
      .setClientSecret('CLIENT_SECRET')
      .setScope('SERVICE_SCOPE_REQUESTS')
      .setCallbackFunction('authCallback')
      .setCache(CacheService.getUserCache())
      .setPropertyStore(PropertiesService.getUserProperties());
}

/**
 * Boilerplate code to determine if a request is authorized and returns
 * a corresponding HTML message. When the user completes the OAuth2 flow
 * on the service provider's website, this function is invoked from the
 * service. In order for authorization to succeed you must make sure that
 * the service knows how to call this function by setting the correct
 * redirect URL.
 *
 * The redirect URL to enter is:
 * https://script.google.com/macros/d/<Apps Script ID>/usercallback
 *
 * See the Apps Script OAuth2 Library documentation for more
 * information:
 *   https://github.com/googlesamples/apps-script-oauth2#1-create-the-oauth2-service
 *
 *  @param {Object} callbackRequest The request data received from the
 *                  callback function. Pass it to the service's
 *                  handleCallback() method to complete the
 *                  authorization process.
 *  @returns {HtmlOutput} a success or denied HTML message to display to
 *           the user. Also sets a timer to close the window
 *           automatically.
 */
function authCallback(callbackRequest) {
  var authorized = getOAuthService().handleCallback(callbackRequest);
  if (authorized) {
    return HtmlService.createHtmlOutput(
      'Success! <script>setTimeout(function() { top.window.close() }, 1);</script>');
  } else {
    return HtmlService.createHtmlOutput('Denied');
  }
}

/**
 * Unauthorizes the non-Google service. This is useful for OAuth
 * development/testing.  Run this method (Run > resetOAuth in the script
 * editor) to reset OAuth to re-prompt the user for OAuth.
 */
function resetOAuth() {
  getOAuthService().reset();
}

Creating a custom authorization prompt

non-Google service authorization card

By default, an authorization prompt does not have any branding and only uses the display name string to indicate what resource the add-on is attempting to access. Your add-on can, however, define a customized authorization card that serves the same purpose and can include additional information and branding.

You define a custom prompt by implementing a custom UI callback function that returns an array of built Card objects. This array should contain only a single card. If more are provided, their headers are displayed in a list, which can result in a confusing user experience.

The returned card must do the following:

  • Make it clear to the user that the add-on is asking for permission to access a non-Google service on their behalf.
  • Make it clear what the add-on is able to do if authorized.
  • Contain a button or similar widget that takes the user to the service's authorization URL. Make sure this widget's function is obvious to the user.
  • The above widget must use the OnClose.RELOAD_ADD_ON setting in its OpenLink object to ensure the add-on reloads after authorization is received.
  • All links opened from the authorization prompt must use HTTPS.

You direct the authorization flow to use your card by calling the setCustomUiCallback() function on your AuthorizationException object.

The following example shows a custom authorization prompt callback function:

/**
 * Returns an array of cards that comprise the customized authorization
 * prompt. Includes a button that opens the proper authorization link
 * for a non-Google service.
 *
 * When creating the text button, using the
 * setOnClose(CardService.OnClose.RELOAD_ADD_ON) function forces the add-on
 * to refresh once the authorization flow completes.
 *
 * @returns {Card[]} The card representing the custom authorization prompt.
 */
function create3PAuthorizationUi() {
  var service = getOAuthService();
  var authUrl = service.getAuthorizationUrl();
  var authButton = CardService.newTextButton()
      .setText('Begin Authorization')
      .setAuthorizationAction(CardService.newAuthorizationAction()
          .setAuthorizationUrl(authUrl));

  var promptText =
      'To show you information from your 3P account that is relevant' +
      ' to the recipients of the email, this add-on needs authorization' +
      ' to: <ul><li>Read recipients of the email</li>' +
      '         <li>Read contact information from 3P account</li></ul>.';

  var card = CardService.newCardBuilder()
      .setHeader(CardService.newCardHeader()
          .setTitle('Authorization Required'))
      .addSection(CardService.newCardSection()
          .setHeader('This add-on needs access to your 3P account.')
          .addWidget(CardService.newTextParagraph()
              .setText(promptText))
          .addWidget(CardService.newButtonSet()
              .addButton(authButton)))
      .build();
  return [card];
}

/**
 * When connecting to the non-Google service, pass the name of the
 * custom UI callback function to the AuthorizationException object
 */
function accessProtectedResource(url, method_opt, headers_opt) {
  var service = getOAuthService();
  if (service.hasAccess()) {
    // Make the UrlFetch request and return the result.
    // ...
  } else {
    // Invoke the authorization flow using a custom authorization
    // prompt card.
    CardService.newAuthorizationException()
        .setAuthorizationUrl(service.getAuthorizationUrl())
        .setResourceDisplayName("Display name to show to the user")
        .setCustomUiCallback('create3PAuthorizationUi')
        .throwException();
  }
}

Authorization requirements checks

The Gmail add-on platform performs an authorization requirements check every time an add-on is rendered. If you use non-Google services, this initial authorization check requires you to implement a function in your add-on that to support these checks.

This function should attempt to connect to each of the non-Google service endpoint your add-on uses. Gmail calls this function prior to rendering your add-on. If any required authorizations are missing, the add-on presents them to the user and requests authorization, before the rest of the add-on starts.

All add-ons that require non-Google authorization should implement the authorization check function, and invoke the authorization flow in it. The previous sections explain how to connect to non-Google services, but you must determine the best way to check for the necessary authorizations. For example, you might read some test data from the non-Google service to ensure that the access token you have is still valid. You should verify that the token is valid rather than just checking for its existence.

The following code snippet shows an example of the initial authorization check:

/**
 * This method is called by the Gmail add-on platform prior to
 * rendering the add-on. This implementation goes through each of the
 * non-Google services that the add-on uses, attempting to read some data
 * in order to verify the user has authorized the use of the service.
 *
 * See the above examples for suggestions on implementing
 * accessProtectedResource().
 */
function get3PAuthorizationUrls() {
  accessProtectedResource("https://api.service1.com/read");
  accessProtectedResource("https://api.service2.com/probe");
  accessProtectedResource("https://api.service3.com/check_logged_in");
}

Finally, you must update your project manifest to tell Gmail which function to check with. This is done by setting the gmail.authorizationCheckFunction property in the mainfest to use the function name as its value. For example:

  "gmail": {
    "name": "Quickstart Toolbar Label",
    "logoUrl": "https://www.gstatic.com/images/icons/material/system/2x/bookmark_black_24dp.png",
    "primaryColor": "#4285F4",
    "secondaryColor": "#4285F4",
    "authorizationCheckFunction": "get3PAuthorizationUrls",
    "contextualTriggers": [{
      "unconditional": {},
      "onTriggerFunction": "getContextualAddOn",
    }]
  }