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

Google 以外の OAuth 構成の例

このコードサンプルは、Google 以外の API を使用する OAuth を使用するためのアドオンを構成する方法を示しています。また、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 以外のサービスの承認カード

デフォルトでは、認証プロンプトにはブランド化がなく、表示名の文字列のみを使用してアドオンがアクセスしようとしているリソースが示されます。ただし、アドオンでは、同じ目的に特化された承認カードを定義し、追加情報とブランドを含めることができます。

カスタム プロンプトを定義するには、ビルドされた 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 にログインしています。