透過 Google Workspace 外掛程式連線至非 Google 服務

Google Workspace 外掛程式專案可以透過 Apps Script 內建進階服務直接連線至許多 Google 產品。

此外,您也可以存取非 Google 的 API 和服務。如果服務不需要授權,您通常只需提出適當的 UrlFetch 要求,然後讓外掛程式解讀回應。

不過,如果非 Google 服務確實需要授權,您就必須為該服務設定 OAuth。您可以使用 Apps Script 的 OAuth2 程式庫來簡化這項程序 (另提供 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 物件陣列,組合 UI 來處理授權。此為選用步驟;如果未設定,系統會使用預設的授權資訊卡。請使用 setCustomUiCallback() 函式設定回呼函式。

非 Google OAuth 設定範例

本程式碼範例說明如何設定外掛程式,以便使用需要 OAuth 的非 Google API。並使用 Apps Script 的 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 服務授權卡

根據預設,授權提示沒有任何品牌宣傳內容,而且只會使用顯示名稱字串來指出外掛程式嘗試存取的資源。不過,外掛程式可以定義用途相同的自訂授權資訊卡,可包含其他資訊和品牌宣傳內容。

您可以實作自訂 UI 回呼函式來定義自訂提示,這個函式會傳回已建構的 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 Script 的 OAuth2 程式庫可協助您建立及管理第三方服務的連線。

第三方系統通常會要求使用者以使用者 ID、密碼或其他憑證登入。當使用者在使用某個 Google Workspace 主機登入第三方服務時,您必須確保他們切換至其他 Google Workspace 主機時,不須再次登入。為避免重複登入要求,請使用使用者屬性或 ID 權杖。我們會在以下各節中說明。

使用者屬性

您可以將使用者的登入資料儲存在 Apps Script 的使用者屬性中。舉例來說,您可以從他們的登入服務建立自己的 JWT 並記錄在使用者屬性中,或是記錄該服務的使用者名稱和密碼。

使用者屬性設有限制,確保使用者只能在外掛程式指令碼中存取這些屬性。其他使用者和其他指令碼無法存取這些屬性。詳情請參閱 PropertiesService

ID 權杖

您可以使用 Google ID 權杖做為服務的登入憑證。這是執行單一登入的方式。由於使用者是 Google 代管應用程式,因此使用者已登入 Google。