Identity-Aware Proxyで保護されたAPIをGoogleスプレッドシートのApps Scriptから実行する

こんにちは、ソフトウェア開発統括部の伊藤です。 フルスタックエンジニアとしてアプリとバックエンドそれぞれのチームに参加して開発をしています。

Googleスプレッドシートは社内でデータの編集と共有するプラットフォームとして非常に使い勝手が良く、またApps Scriptを使った拡張性もあり優れた製品です。 しかし、権限や機能などでApps Scriptだけでは実現できず、App EngineやCloud Runといった実行環境でコードを記述してAPIで連携したい場合があります。

その際に困るのが認証です。社内向けとはいえ強力な権限を持ちがちなサービスアカウントキーをGASのスクリプトプロパティに埋め込むのは危険性もあり、また実行したユーザーを特定することもできません。 そこで、Identity-Aware Proxyを用いてAPIを保護しつつ、実行したユーザーを特定させながらサーバー上でコードを動作させる方法をご紹介します。

Identity-Aware Proxyとは

Identity-Aware Proxy(IAP)は、Google Cloudで提供されている認証サービスで、開発者がデプロイしたApp EngineやCloud Load Balancing、Cloud Runの手前で自動的に認証し、認証されたリクエストのみがサービスに届くような仕組みです。 認証には既定ではGoogleアカウントが使用され、対応しているサービスでは平易な設定で認証をサービスに設定することができます。アプリケーションでユーザーを識別する必要がない場合であれば追加のコードも必要がありません。

ブラウザからのアクセスの場合はGoogleの認証画面にリダイレクトされて自動的に認証されますが、プログラムからのアクセスの場合は別途IDトークンをAuthorizationヘッダに加える必要があります。

Cloud Run向けにはCloud Load Balancingのバックエンドとして使用する場合のみIAPが使えていましたが、現在はCloud Runに直接設定する機能がプレビューで提供されています。

Apps ScriptからIDトークンを得る

Apps ScriptからIAP保護されているリソースにアクセスする場合は、サービスアカウントキーを使った認証をする方法がありますが、先述の通りこの方法にはリスクがあります。 そこで、Apps ScriptからGoogle CloudのOAuth認証を通して認証することで、Apps Scriptを実行しているユーザーとしてIAP保護されているリソースを使用できるようにします。

まず、Google CloudのAPIとサービス>認証情報で、OAuth 2.0 クライアント OAuthクライアントIDを作成します。 まずは、ウェブアプリケーションとして、名前だけ決定して作成します。この後、値が確定してから作成された認証情報の画面に戻ってきて追加設定が必要です。 作成後表示されたクライアントIDとクライアントシークレットをメモしておきます。

次に、スプレッドシートに紐づくApps Scriptを開き、Apps Script OAuth2ライブラリを追加します。 Apps Scriptのファイルリストにある「ライブラリ」で+をクリックして 1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF を検索して追加します。

また、ギアのアイコン「プロジェクトの設定」を開いて、スクリプトプロパティに CLIENT_ID CLIENT_SECRET としてクライアントIDとシークレットを設定しておきます。また、スクリプトIDをコピーします。

スクリプトIDがわかると、Google CloudのOAuthクライアントIDの承認済みリダイレクトURIが決まります。以下の値をGoogle Cloudコンソールから先ほど作成したクライアントIDに設定しておきます。

https://script.google.com/macros/d/${スクリプトID}/usercallback

追加した後、以下のコードを書き加えましょう。

const CLIENT_ID = PropertiesService.getScriptProperties().getProperty('CLIENT_ID');
const CLIENT_SECRET = PropertiesService.getScriptProperties().getProperty('CLIENT_SECRET');

function doAuthorize() {
  const oauthService = getOAuthService_();
  if (oauthService.hasAccess()) {
    return; // 認証済み
  }
  const authorizationUrl = oauthService.getAuthorizationUrl();
  const template = HtmlService.createTemplate('<a href="<?= authorizationUrl ?>" target="_blank">ここをクリックして認証</a>');
  template.authorizationUrl = authorizationUrl;
  const page = template.evaluate();
  SpreadsheetApp.getUi().showModalDialog(page, 'Authorization Required');
}

function getOAuthService_() {
  return OAuth2.createService('myapp')
    .setAuthorizationBaseUrl('https://accounts.google.com/o/oauth2/auth')
    .setTokenUrl('https://oauth2.googleapis.com/token')
    .setClientId(CLIENT_ID)
    .setClientSecret(CLIENT_SECRET)
    .setCallbackFunction('authCallback_')
    .setPropertyStore(PropertiesService.getUserProperties())
    .setScope('https://www.googleapis.com/auth/userinfo.email');
}

function authCallback_(request) {
  const oauthService = getOAuthService_();
  const authorized = oauthService.handleCallback(request);
  if (authorized) {
    return HtmlService.createHtmlOutput('認証成功。このタブは閉じてください。');
  } else {
    return HtmlService.createHtmlOutput('認証失敗。もう一度お試しください。');
  }
}

これで認証された状態を作ることができ、以下のようなコードで認証がかかったリクエストを送れるようになります。

この段階ではまだ受信側のIdentity-Aware Proxyの準備が整っていないので呼び出しに失敗します。

function doAccess() {
  const oauthService = getOAuthService_();
  if (!oauthService.hasAccess()) {
    SpreadsheetApp.getUi().alert("認証されていません");
    return; // 認証済み
  }
  
  const idToken = oauthService.getIdToken(oauthService.getAccessToken());
  const options = {
    method: 'GET',
    headers: {
      Authorization: 'Bearer ' + idToken
    },
  };
  const response = UrlFetchApp.fetch("https://.....", options); // Cloud IAPで保護されたURLを指定できる
  return JSON.parse(response.getContentText());
}

Apps ScriptのOAuthクライアントからの認証を許可する

ブラウザからアクセスされる場合は特に追加の設定は必要ありませんが、プログラムからアクセスされる場合はどのクライアントからのアクセスを許可するかをIdentity-Aware Proxyに指定しておく必要があります。

そこで、Apps Scriptが使用しているOAuthクライアントIDを、受け側のApp EngineやCloud Runに加える必要があります。

App Engineの場合

Google CloudコンソールのIdentity-Aware Proxy設定画面でApp Engineアプリの右にある...から設定を開きます。

カスタムOAuthを選択してカスタムクライアント IDに先ほど作成したクライアントIDを指定します。*1

Cloud Runの場合

IAP for Cloud Runの場合、gcloudコマンドでCloud RunのIdentity-Aware Proxy設定に反映させる必要があります。

まず、反映する設定をYAMLファイルとして作成します。programmaticClientsに先ほど作成したクライアントIDを指定します。 (${OAuth2 クライアントID} に先ほど作成したクライアントIDを指定します)

accessSettings:
  oauthSettings:
    programmaticClients:
      - ${OAuth2 クライアントID}

次に、gcloudコマンドでこれを反映します。

gcloud beta iap settings set iap_settings.yaml \
                --resource-type=cloud-run \
                --project=${GCLOUD_PROJECT} \
                --region=${REGION} \
                --service=${CLOUD_RUN_SERVICE_NAME}

APIを実行したユーザーを特定する

IAP経由でApp EngineやCloud Runサービスにアクセスされた場合、リクエストヘッダにx-goog-iap-jwt-assertionが付与されます。

このヘッダにはIAPが作成したJWTが入っており、これを検証することで実際にIAPを経由してきたアクセスなのかを検証でき、JWTクレームの情報からユーザーのメールアドレスを得ることができます。これにより、Apps Scriptを実行したユーザーをベースにした詳細な権限付与や、実行者の記録などをすることができます。

JWTの検証内容等についての詳しい内容は、Identity-Aware Proxyのドキュメント署名済みヘッダーによるアプリの保護を参考にしてください。

おわりに

GoogleスプレッドシートのApps Scriptから、IAP保護されたApp EngineやCloud Runに、スクリプト実行者の認証情報でアクセスする方法をご紹介しました。 柔軟なインターフェイスをスプレッドシートで用意しつつ、バックエンドで強固な処理を実現できる仕組みと思いますので、ぜひ活用を検討してみてはいかがでしょうか。

*1:この設定ができる以前からApp Engineを利用してきた方の場合は、OAuthクライアントとしてIAP-App-Engine-appのクライアントIDが設定されていることがあり、この場合はApps Scriptにもそちらを流用して設定する方が簡単に設定できるかもしれません。