ブログ | XIMIX

GASからサービスアカウントを利用してIAP保護下のGAEにリクエストを送る方法

作成者: XIMIX 北脇|2023.12.05

 

はじめに

CI部の北脇です。
Google Apps Script (GAS) から Identity-Aware Proxy(IAP) で保護された Google App Engine (GAE) にリクエストを送信する方法を紹介します。

具体的には、サービスアカウントを使用して認証し、リクエストを送信する方法を取り上げます。

GAEへのデプロイとIAPの設定

nestJSで作成したプログラムをGAEにデプロイします。
APIとしては「/api/test」にPOSTリクエストを送信すると"Hello World!"をレスポンスする簡単ななものとなっています。

上記アプリケーションがGAE上にデプロイします。

次にIAPの設定になります。
コンソールからIAPの画面に遷移しGAEが保護されていることを確認します。

IAPの画面からOAuthの構成画面に遷移しクライアントIDを取得します。

これでGAEとIAPの設定は完了しました。

サービスアカウントの取得

サービスアカウントを作成します。
ロールとしては「IAPで保護されたウェブアプリユーザー」を選択します。

サービスアカウント作成後ダウンロードします。

これでGoogle Cloud側の準備は完了しました。

実装例

以下のスクリプトはGoogle Apps Script (GAS) から Google App Engine (GAE) にPOSTリクエストを送るものです。
ただし、この状態ではIdentity-Aware Proxy (IAP) で保護された GAEに対してPOSTリクエストが送信できる認証情報が何も設定されていません。
function myFunction() {
const apiUrl = 'https://{GAEのドメイン}/api/test';

  try{
    const options = {
    "method": "post",
    };
    let res = UrlFetchApp.fetch(apiUrl, options)
    let data = res.getContentText('UTF-8')
    console.log(data)
  }catch(e){
    console.log(e)
  }
}

このままでは401エラー(Unauthorized)が返されます。
エラーメッセージは以下の通りです。
{ [Exception: Request failed for https://{GAEのドメイン} returned code 401. Truncated server response: Invalid IAP credentials: empty token (use muteHttpExceptions option to examine full response)] name: 'Exception' }
そこで、前述で作成したサービスアカウントを利用してHTTPリクエストを送信できるように、このプログラムを修正します。
サービスアカウントとIAPのクライアントIDからIDトークンを取得し、APIリクエスト時にAuthorizationヘッダを付けることで、IAPで保護されたGAEに対してリクエストを送信することが可能になります。
PRIVATE_KEY = "{サービスアカウントのプライベートキー}"
PRIVATE_KEY_ID = "{サービスアカウントのプライベートキーID}"
CLIENT_EMAIL = "サービスアカウントのEメール";
CLIENT_ID = "{IAPのクライアントID}";


function myFunction() {
  //ApplwWatchRawテーブル用のAPIのエンドポイント
const apiUrl = 'https://{GAEのドメイン}/api/test';

  var headers = {
    'Authorization': 'Bearer ' +  getIdToken()
  }
  
  try{
    const options = {
      "headers": headers,
      "method": "post",
    };
    let res = UrlFetchApp.fetch(apiUrl, options)
    let data = res.getContentText('UTF-8')
    console.log(data)
  }catch(e){
    console.log(e)
  }
}

function getIdToken() {
  const options = {
    "method": "POST",
    "payload": {
      "grant_type": 'urn:ietf:params:oauth:grant-type:jwt-bearer',
      "assertion": getAssertion()
    },
    'muteHttpExceptions': true,
  };
  const response = JSON.parse(UrlFetchApp.fetch('https://oauth2.googleapis.com/token', options));
  console.log(response.id_token)
  return response.id_token;
}

function getAssertion() {
  const privateKey = PRIVATE_KEY
  const header = {
    alg: 'RS256',
    typ: 'JWT',
    kid:PRIVATE_KEY_ID
  };
  const now = new Date();
  const claimSet = {
    iss: CLIENT_EMAIL,
    aud: "https://www.googleapis.com/oauth2/v4/token",
    exp: (now.getTime() / 1000) + 3000,
    iat: now.getTime() / 1000,
    target_audience: CLIENT_ID
  };
  let toSign = Utilities.base64EncodeWebSafe(JSON.stringify(header)) + '.' + Utilities.base64EncodeWebSafe(JSON.stringify(claimSet));
  toSign = toSign.replace(/=+$/, '');
  const signatureBytes = Utilities.computeRsaSha256Signature(toSign, privateKey);
  let signature = Utilities.base64EncodeWebSafe(signatureBytes);
  signature = signature.replace(/=+$/, '');
  return toSign + '.' + signature;
};

サービスアカウントからプライベートキー、プライベートキーID、Eメールアドレスを設定します。
また、IAPのコンソール画面に記載されていたクライアントIDを設定します。

getIdtoken関数でIDトークンを取得しています。
上記サービスアカウントの情報やクライアントIDを利用して「oauth2.googleapis.com/token」にリクエストを送信します。

修正後再度GASを実行しましょう。

無事IAPで保護されたGAEに対してリクエストが通ることを確認できました!

Google Cloud、Google Workspace に関するご相談はXIMIXへ!

Google Cloud、Google Workspaceに関する お問い合わせはこちら
XIMIX(サイミクス)は商標登録出願中です