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

借助 Apps 脚本的内置高级服务,您的 Google Workspace 插件项目可以直接关联到许多 Google 产品。

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

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

使用 OAuth 服务

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

授权流程包括:

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

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

检测出需要授权

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

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

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

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

调用授权流程

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

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

非 Google OAuth 配置示例

此代码示例展示了如何将插件配置为使用非 Google API 的 OAuth。它利用适用于 Apps 脚本的 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 服务的权限。
  • 明确说明在获得授权后,插件能够执行哪些操作。
  • 包含可将用户转到服务授权网址的按钮或类似 widget。确保用户可以清楚了解此 widget 的功能。
  • 上述 widget 必须在其 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 托管应用中。