通过 Google Workspace 插件连接到非 Google 服务

您的插件项目可以使用 Apps 脚本的内置高级服务直接连接到许多 Google 产品。

您还可以使用非 Google API 和服务。如果服务不需要授权,您通常只需发出适当的 UrlFetch 请求,然后让您的插件解读响应。

但是,如果非 Google 服务确实需要授权,您必须为该服务配置 OAuth。您可以使用 OAuth2 for Apps Script 库(也有一个 OAuth1 版本)来简化此过程。

使用 OAuth 服务

使用 OAuth 服务对象连接到非 Google 服务时,您的Google Workspace 插件需要检测何时需要授权,并在需要授权时检测授权流程。

授权流程包括:

  1. 提醒用户需要身份验证,并提供用于启动此流程的链接。
  2. 从非 Google 服务获取授权。
  3. 正在刷新该插件,以重新尝试访问受保护的资源。

当需要非 Google 授权时,Google Workspace 插件基础架构会处理这些详细信息。您的插件只需要检测何时需要授权,并在必要时调用授权流程。

检测到需要授权

请求可能由于各种原因而无权访问受保护的资源,例如:

  • 访问令牌尚未生成或已过期。
  • 访问令牌未涵盖请求的资源。
  • 访问令牌不涵盖请求所需的范围。

您的插件代码应该会检测这些情况。OAuth 库的 hasAccess() 函数可告知您当前是否有权访问某项服务。或者,在使用 UrlFetchApp fetch() 请求时,您可以将 muteHttpExceptions 参数设置为 true。这样可以防止请求在请求失败时抛出异常,并允许您检查返回的 HttpResponse 对象中的请求响应代码和内容。

当插件检测到需要授权时,应触发授权流程。

调用授权流程

如需调用授权流程,您可以使用卡服务创建 AuthorizationException 对象,设置其属性,然后调用 throwException() 函数。在抛出异常之前,请提供以下信息:

  1. 必需。授权网址。它由非 Google 服务指定,并且是授权流程启动时用户转到的位置。您可以使用 setAuthorizationUrl() 函数设置此网址。
  2. 必需。资源显示名称字符串。在请求授权时向用户标识资源。您可以使用 setResourceDisplayName() 函数设置此名称。
  3. 用于创建自定义授权提示的回调函数的名称。此回调会返回一个已构建的 Card 对象数组,这些对象用于组合界面来处理授权。此为可选;如果未设置,则使用默认授权卡。您可以使用 setCustomUiCallback() 函数设置回调函数。

非 Google OAuth 配置示例

此代码示例展示了如何将插件配置为使用需要 OAuth 的非 Google API。它利用 Apps2 的 OAuth2 脚本构建用于访问 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.
 * @return {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
 *
 *  @return 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.
 *  @return {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();
}

创建自定义授权提示

非 Google 服务授权卡

默认情况下,授权提示没有任何品牌信息,仅使用显示名字符串来指示插件尝试访问的资源。但是,您的插件可以定义用途相同的自定义授权卡,并且可以包含更多信息和品牌信息。

您可以通过实现会返回已构建 Card 对象的数组的自定义界面回调函数来定义自定义提示。此数组只能包含一张卡片。如果提供了更多标头,它们的标头会显示在列表中,从而导致用户体验混乱。

返回的卡片必须执行以下操作:

  • 向用户明确说明该插件正在请求代表他们访问非 Google 服务。
  • 明确说明授权后插件可以执行哪些操作。
  • 包含可将用户转到服务的授权网址的按钮或类似微件。请确保此微件的功能对用户而言显而易见。
  • 上面的微件必须在其 OpenLink 对象中使用 OnClose.RELOAD_ADD_ON 设置,以确保插件在收到授权后重新加载。
  • 通过授权提示打开的所有链接都必须使用 HTTPS

您可以通过对 AuthorizationException 对象调用 setCustomUiCallback() 函数来指示授权流程使用您的信用卡。

以下示例展示了自定义授权提示回调函数:

/**
 * 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.
 *
 * @return {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();
  }
}

跨应用 Google Workspace 管理第三方登录

插件的一个常见应用是提供在 Google Workspace主机应用中与第三方系统交互的接口。 Google Workspace Apps 脚本的 OAuth2 库可以帮助您创建和管理与第三方服务的连接。

第三方系统通常会要求用户使用用户 ID、密码或其他凭据登录。当用户在使用一台主机时 Google Workspace 登录第三方服务时,您必须确保他们在切换到另一台Google Workspace 主机时不必再次登录。为防止出现重复的登录请求,请使用用户属性或 ID 令牌。下面几部分将对此进行说明。

用户属性

您可以在 Apps 脚本的用户属性中存储用户的登录数据。例如,您可以从登录服务创建自己的 JWT,并将其记录在用户属性中,或者记录其服务的用户名和密码。

用户属性的范围限定为:仅在插件的脚本中可供用户访问。其他用户和其他脚本无法访问这些属性。如需了解详情,请参阅 PropertiesService

ID 令牌

您可以将 Google ID 令牌用作服务的登录凭据。这是实现单点登录的方式。用户已在 Google 托管应用中登录 Google。