Google Workspace アドオンから Google 以外のサービスに接続する

Google Workspace アドオン プロジェクトは、Apps Script の組み込みサービスと高度なサービスを使用して、多くの 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 オブジェクトのコンテンツを調べることができます。

アドオンは、承認が必要であることを検出すると、承認フローをトリガーします。

認可フローの呼び出し

認可フローを呼び出すには、Card サービスを使用して AuthorizationException オブジェクトを作成し、そのプロパティを設定して、throwException() 関数を呼び出します。例外をスローする前に、次のものを指定します。

  1. 必須: 認可 URL。これは Google 以外のサービスによって指定され、承認フローが開始したときにユーザーが誘導される場所です。この URL は、setAuthorizationUrl() 関数を使用して設定します。
  2. 必須: リソースの表示名の文字列。承認がリクエストされたときに、ユーザーに対してリソースを識別します。この名前は setResourceDisplayName() 関数を使用して設定します。
  3. カスタム承認プロンプトを作成するコールバック関数の名前。このコールバックは、承認を処理するための UI を構成する、ビルド済みの Card オブジェクトの配列を返します。これはオプションです。設定しない場合は、デフォルトの認証カードが使用されます。コールバック関数は、setCustomUiCallback() 関数を使用して設定します。

Google 以外の OAuth 構成の例

このコードサンプルは、OAuth を必要とする Google 以外の API を使用するようにアドオンを構成する方法を示しています。OAuth2 for Apps Script を利用して、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 オブジェクトの配列を返すカスタム UI コールバック関数を実装します。この配列には、カードを 1 つだけ含めてください。それを超える値を指定すると、それらのヘッダーがリストに表示され、ユーザー エクスペリエンスを混乱させる可能性があります。

返されるカードでは、次のことを行う必要があります。

  • アドオンがユーザーに代わって Google 以外のサービスにアクセスする権限をリクエストしていることをユーザーに明確に示します。
  • 許可した場合にアドオンで何ができるかを明確に説明します。
  • ユーザーをサービスの認可 URL に移動させるボタンまたは同様のウィジェットを含めます。このウィジェットの機能がユーザーにとってわかりやすいようにしてください。
  • 上記のウィジェットでは、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 やパスワードなどの認証情報を使用してログインする必要があることがよくあります。ユーザーが 1 つの Google Workspace ホストを使用しているときにサードパーティのサービスにログインした場合、別の Google Workspace ホストに切り替えるときに再度ログインする必要がないようにする必要があります。ログイン リクエストが繰り返されないようにするには、ユーザー プロパティまたは ID トークンを使用します。これらについては、次のセクションで説明します。

ユーザー プロパティ

管理者は、ユーザーのログインデータを Apps Script のユーザー プロパティに保存できます。たとえば、ログイン サービスから独自の JWT を作成してユーザー プロパティに記録したり、サービスのユーザー名とパスワードを記録したりできます。

ユーザー プロパティは、アドオンのスクリプト内でそのユーザーのみがアクセスできるようにスコープ設定されます。他のユーザーや他のスクリプトは、これらのプロパティにアクセスできません。詳しくは、PropertiesService をご覧ください。

ID トークン

サービスのログイン認証情報として Google ID トークンを使用できます。シングル サインオンを実現する方法です。ユーザーは Google ホストアプリを使用しているため、すでに Google にログインしています。