🐶

ログインが必要なGAS WebAppを自動認証する

2025/02/28に公開1

こんにちは!生徒会情報機構(TRANs)のKombumori/FRLPです。
もうかれこれ半年以上(推定)Zennをサボっていたのですが、TRANsのnoteのプロジェクトが動き出しそうなので、これをいい機会にとまずはZennにひと記事書くことにいたしました…。
なおこの記事の内容は半年前から書こう書こうと思いつつモチベ下降のために書いてこなかったものです。

GASのWebAppのログイン

Google Apps Sctipt(GAS)はGoogle スプレッドシートなどのGoogle Workspaceと呼ばれる製品群に含まれる、Googleのサービス周りの処理を自動化できるサービスです。とはいっても、実際にはGoogleのサービスのみならず、世の中のある程度のことはできます。(過言…?)
さて、そのGASではいわゆるWebApp(Webアプリ)を作成することができます。GASのWebAppは世界中に公開することも可能である一方、Googleアカウントによるログインによって利用できるユーザーに制限を設けることもできます。あるいは、ユーザーにログインさせざるを得ない状況が存在します。たとえば…

  • 利用しているGoogleサービスのAPIが認証を必要とするモノである場合
  • 組織下のアカウントで、組織によって外部公開できないように制限されている場合

このような場合、GASのWebAppではログインが必要になりますが、ここで致命的な問題があります。
GASのWebAppを、それ自体をAPIとして活用するケースでは、自動で認証してあげる必要が生じるのです。
PythonからGASを呼び出すならseleniumなどでブラウザ操作すればできなくはないかもしれません。
がしかし、たとえば他のGASからGASを呼び出したい場合はどうでしょう。seleniumは使えないのでUrlFetchApp.fetch()でなんとか呼び出してあげる必要があります。

これをなんとか実現していきましょう。ちなみにこれを実現することで、WebAppを特定のユーザー権限で走らせるということが可能になるので、今回は呼び出す側のGASと呼び出される側のGASを別のGoogleアカウントにして試していきます。

とりあえず普通に呼び出してみよう

まずは呼び出される側のコードを書きます。

コード.gs(呼び出される側)
function doGet(e) {
  payload = JSON.stringify({message: "success"})
  console.log(payload)
  ContentService.createTextOutput()
  var output = ContentService.createTextOutput();
  output.setMimeType(ContentService.MimeType.JSON);
  output.setContent(payload);
  return output;
}

GETリクエストを飛ばすと{"message":"success"}と返してくるGAS WebAppです。
これを「次のユーザーとして実行:」を「自分(〜)」、「アクセスできるユーザー」を「Googleアカウントを持つ全員」としてデプロイします。
生成されたWebAppのリンクを開くと、ログインが求められるはずです。好きなアカウントでログインして、{"message":"success"}と返してくるか確認してみましょう。
次に、呼び出される側とは別のGoogleアカウントで以下のコードを実行します。

コード.gs(呼び出す側)
function callWebApp() {
  const webAppURL = "https://script.google.com/macros/s/XXX/exec" //先ほどデプロイしたWebAppのURL
  const responce = UrlFetchApp.fetch(webAppURL).getContentText() 
  console.log(responce)
}

「外部サービスへの接続」権限が必要となるので付与してあげてください。するとおそらくLogging output too large. Truncating output.と出力され、うじゃーっと出てきます。明らかに{"message":"success"}ではありません。出力をDriveに保存するなりすればわかりますが、これはログインしないままWebAppにアクセスした場合に表示されるログインを求める表示です。
うまくいきませんでしたね。

headerにOAuth Tokenをつけても…

自動認証したいならOAuthですね。試してみましょう。
呼び出す側のコードを以下のように変更しましょう。

コード.gs(呼び出す側)
function callWebApp() {
  const webAppURL = "https://script.google.com/macros/s/XXX/exec" //先ほどデプロイしたWebAppのURL
  const OAuthToken = ScriptApp.getOAuthToken();
  const options = {
    'headers': {'Authorization': 'Bearer '+  OAuthToken}
  };
  const responce = UrlFetchApp.fetch(webAppURL, options).getContentText()
  console.log(responce)
}

実行してみると…
Exception: Request failed for https://script.google.com returned code 401.ですって!
権限不足のようです。

実は、これでもできるという報告がStack Overflowにも上がっています。
https://ja.stackoverflow.com/questions/57617/組織内で-webアプリケーションとして導入-をした際の認証について

自己解決しました。
GCPの認証情報から「APIキー」や「OAuth 2.0 クライアントID」などをいろいろ試しましたが、
シンプルに解決・・・(投稿前にやってたつもりだったけど)
headersにScriptApp.getOAuthToken()を使用すれば通りました。

いわゆるなんで動くかわからないですね。実は、これにはちゃんと再現性があります。

結論:じゃあどうするのか

他のStack Overflowを見てみましょう。
https://stackoverflow.com/questions/29229361/calling-a-google-apps-script-web-app-with-access-token
まあ要するに、以下の条件を満たすときheaderにOAuth Tokenを追加することで実行できるわけです。

  1. そのOAuthのユーザーに呼び出される側のGASプロジェクトの閲覧権限があること
  2. 呼び出す側のGASプロジェクトにhttps://www.googleapis.com/auth/drive.readonlyスコープがあること

まず、1のとおり、呼び出される側のGASに、呼び出す側に使っているGoogleアカウントの閲覧権限を追加します。いわゆるファイルの共有ですね。
そして、2のスコープは手で追加しましょう。呼び出す側プロジェクトの設定から 「appsscript.json」マニフェスト ファイルをエディタで表示するを有効にします。
そして、呼び出す側のappsscript.jsonを以下のように書き換えます。appsscript.jsonはGASの各種設定をマニュアルで行えるもので、今回は権限設定をマニュアルで行います。他にも、デプロイ時の設定などなども設定可能です。

appsscript.json(呼び出す側)
{
  "timeZone": "Asia/Tokyo",
  "dependencies": {
  },
  "oauthScopes": 
+  [
+    "https://www.googleapis.com/auth/drive.readonly",
+    "https://www.googleapis.com/auth/script.external_request"
+  ],
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8"
}

そして、先ほどのcallWebApp()を実行します。
追加で「Google ドライブのすべてのファイルの表示、ダウンロード」権限が求められるので付与してあげると、見事{"message":"success"}が出力されるはずです。

終わりに

手間は多いですが単純な話ですね。これを使うと、できることの幅が意外と広がります。ぜひお試しあれ!

すたっく・おーばーふろーまとめただけだって?それは…言わないお約束。
思ったこと:書き始めると意外とすぐ書き上がるものですね。

TRANs - 生徒会情報機構

Discussion