GAS(Google Apps Script)のWebアプリを「決まった人だけ」に見せるには、実行するユーザー・利用者の承認・メールアドレスを取得できる条件を分けて確認します。わたしは取得できなかった経験を、ドメインだけで決まる制限として説明していました。この判断表では、その断定を訂正します。
Session.getActiveUser().getEmail()で利用者のメールアドレスを取得できる場合は、許可リストと照合する。空文字なら許可しない- 個人Gmailや別ドメインだから必ず空になる、とは判断しない。実行者と承認状態を確認する。URLキーは本人確認の代わりにはならない
- どの方式でも、
ScriptApp.getService().getUrl()が返すドメイン専用URL(/a/ドメイン名/macros/...)は正規化してから使う
ハマった経緯は前の記事に書きました。この記事は、これから作る人が判断と設定をそのまま進められるように、表と手順だけにまとめたものです。
判断表: 取得条件と必要な制限で方式を考える
| 確認した条件・目的 | 方式の候補 | 注意点 |
|---|---|---|
| 利用者のメールアドレスを取得できる | getActiveUser() のメールアドレスを許可リストと照合 |
空文字は拒否。コードに埋めた許可リストの変更には再デプロイが必要 |
| 本人確認が不要で、共有鍵だけの制限を許容できる | URLに秘密のキーを付ける(?key=...) |
URLが漏れれば鍵を知る人が通れる。機密情報の本人確認には使わない |
| 複数アプリで本人確認と利用許可の管理をまとめたい | 複数のGASアプリで共有する認証の仕組み(登録制)に寄せる | 登録照合の前に、信頼できる本人確認が必要 |
| 人ではなく、プログラムが定期的に書き込む | 閲覧用とは別のデプロイを用意し、送信側で認証情報を付ける | わたしの環境では「匿名で誰でも」に広げたら401になった(後述) |
わたしの当時の環境では、デプロイの実行者やOAuthスコープを変えても空文字の問題が解決しませんでした。ただし、この結果を全環境に一般化はできません。
Google公式のSessionリファレンスでは、利用者の承認なしに実行できる文脈ではメールアドレスを取得できない場合があり、「自分として実行」するWebアプリもその例に含まれます。一方、開発者自身の実行や同じWorkspaceドメインでは、通常この制限が適用されないとされています。同一ドメインなら必ず取得できる、という保証でもありません。
次に確認することは、公開中のデプロイの実行者と、対象アカウントの承認状態です。 その条件でメールアドレスが取得できるかを検証し、空文字のまま許可する処理にはしません。
手順1: メールアドレスを取得できる条件で照合する
利用者のメールアドレスを取得できることを確認した場合の最小例です。空文字が返ったときも「許可しない」側に倒します。
var ALLOWED = ['許可するアドレス1', '許可するアドレス2']; // 実際のメールアドレスを入れる
function doGet(e) {
var email = Session.getActiveUser().getEmail();
if (!email || ALLOWED.indexOf(email) === -1) {
return HtmlService.createHtmlOutput('このページを見る権限がありません。');
}
return HtmlService.createHtmlOutput('ようこそ');
}
appsscript.json の webapp は、わたしは次の組み合わせで使っています。
"webapp": {
"executeAs": "USER_ACCESSING",
"access": "ANYONE"
}
ANYONE は「Googleアカウントでログインしている全員」です。ここで入口を広めに開けて、中のコードで絞る形になります。
手順2: URLキーで制限する場合は、本人確認と区別する
「誰がアクセスしているか」ではなく「そのURLのキーを知っているか」で判定します。これは本人確認ではありません。ブラウザ履歴や転送先などにキーが残る可能性もあります。機密情報を扱う場面の認証の代替にはしません。
function doGet(e) {
var params = (e && e.parameter) || {};
var key = PropertiesService.getScriptProperties().getProperty('VIEW_KEY');
if (!key || params.key !== key) {
return HtmlService.createHtmlOutput('このページを見る権限がありません。');
}
return HtmlService.createHtmlOutput('ようこそ');
}
利用者には .../exec?key=... の形でブックマークしてもらいます。この例のキーはスクリプトプロパティに保存しています。漏れた場合は VIEW_KEY を変更して旧キーを失効させ、新しいキーを付けたURLを配り直します。コードを変えないキー更新だけなら再デプロイは不要です。
わたしはこの方式を10日ほど使ってから、やめました。理由は2つです。
- キーはURLごと共有されると、誰でも見られてしまう
- 併用していたメールアドレスの許可リストは、変えるたびにコード修正と再デプロイが必要だった
手順3: 人の出入りがあるなら、共通の認証の仕組みに寄せる
いまは、複数のGASアプリで共有している認証の仕組みに、閲覧者の管理を一本化しています。そこに登録したアカウント、または登録したGoogleグループのメンバーだけが見られる形です。各Webアプリ側は「この人は登録されているか」を問い合わせて、結果が ok のときだけ画面を返します。
function isAllowedViewer_() {
try {
return resolveAccess_().status === 'ok'; // 共通の認証の仕組みへ問い合わせる
} catch (err) {
console.error('access check failed: ' + err);
return false; // 問い合わせに失敗したら見せない
}
}
利点は、許可する人を増やしたり外したりするときに、各アプリのコードを触らずに済むことです。問い合わせに失敗したときは「見せない」側に倒しています。
手順4: ドメイン専用URLを正規化する
画面内のリンクに ScriptApp.getService().getUrl() を使うと、Workspaceのアカウントで実行したときに https://script.google.com/a/ドメイン名/macros/s/.../exec という形が返ることがあります。この形は、別ドメインや個人Gmailのアカウントから開くと正しく動きませんでした。汎用の形に直してから使います。
function toGenericWebAppUrl_(url) {
return String(url || '')
.replace(/\/a\/macros\/[^\/]+\//, '/macros/')
.replace(/\/a\/[^\/]+\/macros\//, '/macros/');
}
function appUrl_() {
try {
return toGenericWebAppUrl_(ScriptApp.getService().getUrl());
} catch (err) {
return '';
}
}
ついでに、GASの画面はiframeの中で表示されるので、href="?page=links" のような相対リンクは使えません。押すと画面が真っ白になり、エラーも出ません。リンクは appUrl_() で作った絶対URLに target="_top" を付けます(別のページに埋め込んで表示する場合は target="_parent")。
手順5: プログラム用のデプロイを分けるときの注意
わたしのWebアプリは、同じプロジェクトに2本のデプロイがあります。
- 閲覧用: 人がブックマークして開く
- 自動更新用: 手元のPCから2時間おきにデータをPOSTする
最初は自動更新用だけを ANYONE_ANONYMOUS(匿名で誰でも)にしていました。その後、自動更新用のために appsscript.json を ANYONE_ANONYMOUS / USER_DEPLOYING へ書き換えてデプロイしたところ、未認証のPOSTもトークン付きのPOSTも 401 になりました(ストレージ読み取りで UNAUTHENTICATED)。原因の切り分けはしていません。
いまは2本とも同じ USER_ACCESSING / ANYONE のまま更新し、送信側のPCがGoogleアカウントのアクセストークンを付けて送っています。
このとき分かったことを並べます。
appsscript.jsonのwebapp.accessを書き換えてclasp pushすると、その後のclasp deploy -i <デプロイID>は、書き換え後の値でそのデプロイを上書きする。pushの前にaccessの値を確認する- URLを変えずに更新するには、毎回
clasp deploy -i <同じデプロイID>を使う - 壊したら、直前の版を指定して同じデプロイIDへデプロイし直せば戻る
- 新しい版は、デプロイ直後の数十秒ほど古い版のまま応答することがある。1回の失敗で判断しない
まとめ
- 最初に、公開中のデプロイの実行者・利用者の承認・メールアドレスの取得可否を確認する。ドメインだけで決めない
- URLキーは共有鍵による制限で、本人確認ではない。人ごとの利用許可には、本人確認と登録照合を分ける
getService().getUrl()は正規化してから使う。リンクは絶対URLにするwebapp.accessを変えるときは、どのデプロイを上書きするかを先に確認する
その後: 管理画面とAPIで実行ユーザーを分けた
手順3で書いた共通の認証の仕組みは、その後、管理画面をAPI本体とは別のGASプロジェクトへ分けました。画面は USER_ACCESSING、DBを操作するAPIは USER_DEPLOYING で動かし、本人確認は画面側が渡すOAuthアクセストークンをAPI側で検証する形です。わたしの構成での対応記録であり、別ドメインでは getActiveUser() が常に使えないという意味ではありません。
GASのUSER_DEPLOYINGとUSER_ACCESSINGを分けた|管理画面に管理者が入れない
関連記事
- GASのWebアプリを「特定の人だけ」に公開できなかった理由|Session.getActiveUser()が空になる原因と代替案 — この記事の元になった、ハマった経緯の記録
- 自動タスクが1週間「成功」と報告しながら何もしていなかった|ダッシュボードから止められるようにした記録 — 同じWebアプリに操作盤を足した話
- 公開前チェックで使っている正規表現パターン集 — 公開前に機械で走査している中身
AIエージェントの運用記録はnoteにまとめています
こうしたWebアプリやスクリプトを、AIにどう作らせて、どこで失敗したかは、noteマガジン「AIエージェント運用ノート」に記録しています。
注記
この記事の内容は、わたしが運用しているツールの技術検証を一般化したものです。コード例は説明用に書き直した最小例で、実際のIDや設定値は含んでいません。


コメント