GASのOAuthスコープを
最小限に絞る方法|appsscript.json

「このアプリがGoogleドライブのすべてのファイルにアクセスします」。GASの承認画面で不安になるあの表示は、 appsscript.jsonの1項目で必要な範囲まで絞り込めます。仕組みと具体的な書き方を解説します。

|対象: GAS / appsscript.json / OAuth / セキュリティ

Table of Contents

承認画面の文言はどこから来るのか

結論から言うと、書いたコードから自動で決まっています。GASを初めて実行すると「このアプリが〜へのアクセスをリクエストしています」という承認画面が出ます。あの一覧は、スクリプトが要求するOAuthスコープ(アカウントのどのデータに触れてよいかを表す権限の単位)を日本語に直したものです。

GASはappsscript.json(マニフェストと呼ばれるプロジェクトの設定ファイル)に指定がない場合、コードを解析して必要そうなスコープを自動で決めます。便利ですが、この自動判定は安全側に倒れるため広めになります。たとえばDriveAppを1回書いただけで、アカウント内の全ファイルへのアクセス権を求める表示に変わります。

{
  "timeZone": "Asia/Tokyo",
  "dependencies": {},
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8"
}

// oauthScopes が無い状態。
// GASがコードを解析して必要そうなスコープを自動で決めるため、
// 承認画面の内容はコードを変えるたびに黙って変わる。

これが実務で問題になるのは2つの場面です。1つは、社内メンバーに配布したときに「なぜドライブ全体の権限が必要なのか」と説明を求められる場面。もう1つは、スクリプトが乗っ取られたり、意図しないコードが混ざったりしたときの被害範囲がそのまま権限の広さになる点です。権限を絞ることは、説明コストと事故時の被害の両方を下げます。

appsscript.jsonを表示してoauthScopesを書く

マニフェストは初期状態ではエディタに表示されません。左メニューの「プロジェクトの設定」を開き、「『appsscript.json』マニフェスト ファイルをエディタで表示する」にチェックを入れてください。エディタのファイル一覧にappsscript.jsonが現れます。

そこにoauthScopesを配列で書きます。書いた瞬間から自動判定は使われなくなり、ここに書いたスコープだけが要求されます

{
  "timeZone": "Asia/Tokyo",
  "dependencies": {},
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8",
  "oauthScopes": [
    "https://www.googleapis.com/auth/spreadsheets.currentonly",
    "https://www.googleapis.com/auth/script.external_request",
    "https://www.googleapis.com/auth/script.scriptapp"
  ]
}

// このスクリプトができるのは次の3つだけ。
//  1. 紐づいているスプレッドシートの読み書き(他のシートは開けない)
//  2. 外部サイトへのHTTPリクエスト(Claude APIの呼び出し)
//  3. 自分自身のトリガーの作成・削除

よく使うスコープを整理しておきます。まずはこの範囲で組めないかを検討してください。

スコープ(末尾のみ)できること
spreadsheets.currentonly紐づいているスプレッドシートだけを読み書き
spreadsheetsアカウント内のすべてのスプレッドシートを読み書き
documents.currentonly紐づいているGoogleドキュメントだけを読み書き
script.external_requestUrlFetchAppでの外部APIへのHTTP通信
script.send_mailMailAppでのメール送信(受信は読めない)
script.scriptappトリガーの作成・削除
script.container.uiメニューやダイアログなどUIの表示
drive.fileスクリプトが作成した/利用者が選んだファイルのみ
driveドライブ内のすべてのファイルの表示・編集・削除

実際に書くときはhttps://www.googleapis.com/auth/を頭に付けたフルURLで指定します。進め方のコツは狭く書いてエラーで足すこと。足りなければ実行時に「必要な権限がありません」というエラーと必要なスコープのURLが表示されるので、それを追記すれば済みます。最初から広く書くと、絞る機会は二度と来ません。

スプレッドシート|currentonlyで足りるか見極める

スプレッドシートに紐づいたスクリプト(コンテナバインド。シートの「拡張機能 > Apps Script」から作ったもの)なら、多くの場合spreadsheets.currentonlyで足ります。名前のとおり今そのシートだけに権限が限定されます。

// スコープが変わる書き方の例:スプレッドシート

// 【狭い】spreadsheets.currentonly で足りる
// スクリプトが紐づいている(コンテナバインド)シートだけを触る
function readActiveSheet() {
  const sheet = SpreadsheetApp.getActive().getSheetByName("問い合わせ");
  const values = sheet.getDataRange().getValues();
  console.log("行数: " + values.length);
}

// 【広い】spreadsheets(=全スプレッドシート)が必要になる
// IDで別ファイルを開いた時点で、アカウント内の全シートへの権限を求められる
function readOtherSheet() {
  const ss = SpreadsheetApp.openById("1AbCdEf...");  // ここで広がる
  const values = ss.getSheets()[0].getDataRange().getValues();
  console.log("行数: " + values.length);
}

// 対処: 別ファイルの参照が1か所だけなら、
// IMPORTRANGE や 手動コピーで済ませられないか先に検討する。

分かれ目はopenById()openByUrl()です。IDで別ファイルを開く以上、「どのファイルを開くか」は実行時にしか分かりません。そのためGoogle側は全スプレッドシートへの権限を求めます。1つのマスタを参照したいだけなら、IMPORTRANGE関数でシート上に取り込み、スクリプトからは自分のシートとして読む方法も検討できます。

なお、スタンドアロン(どのファイルにも紐づかない単独)のスクリプトではcurrentonlyは使えません。「今のシート」が存在しないためです。この場合はspreadsheetsが必要になるので、権限を絞りたい処理はコンテナバインドで作るのが基本方針になります。

メールとドライブ|広がりやすい2つのAPI

承認画面が一気に重くなる原因の大半は、メールとドライブです。まずメールから見ます。

// スコープが変わる書き方の例:メール送信

// 【狭い】script.send_mail だけで動く
// 送信専用。受信トレイの中身は一切読めない
function notifyByMailApp() {
  MailApp.sendEmail({
    to: "team@example.com",
    subject: "日次処理が完了しました",
    body: "本日の集計が正常に終了しました。",
  });
}

// 【広い】gmail.* 系(受信メールの読み取りを含む)が必要になる
// 下書き作成やスレッド検索を使うと、メールを読む権限まで求められる
function createDraftByGmailApp() {
  GmailApp.createDraft("team@example.com", "確認依頼", "内容をご確認ください。");
}

// 判断基準: 「送るだけ」なら MailApp。
// 受信メールの検索・ラベル付けが必要なときだけ GmailApp を使う。

MailAppは送信専用のクラスで、script.send_mailという送信だけのスコープで動きます。一方GmailAppは検索・ラベル・下書きまで扱えるぶん、受信メールを読む権限まで要求します。通知を送るだけの処理でGmailAppを使っていないか、一度確認してみてください。ここを直すだけで承認画面の印象は大きく変わります。

// スコープが変わる書き方の例:ドライブ

// 【広い】drive スコープ。アカウント内の全ファイルを検索・編集できる
function findAllInvoices() {
  const files = DriveApp.searchFiles('title contains "請求書"');  // 全体検索
  while (files.hasNext()) {
    console.log(files.next().getName());
  }
}

// 【狭い】drive.file で足りる場合がある
// スクリプトが作ったファイル、利用者がファイル選択UIで選んだファイルだけを扱う
function createReportFile() {
  const blob = Utilities.newBlob("集計結果", "text/plain", "report.txt");
  const file = DriveApp.createFile(blob);   // 自分で作ったファイルには触れる
  console.log(file.getUrl());
}

// 注意: drive.file を指定した状態で searchFiles や getFolderById を呼ぶと
// 権限不足で失敗する。「作る・書く」だけの処理に向いたスコープ。

ドライブ側は、drive.fileが使えるかが分かれ目です。これはスクリプト自身が作成したファイルと、利用者がファイル選択画面で明示的に選んだファイルにだけ触れるスコープです。レポートを出力するだけの処理なら十分に足ります。反対に、既存フォルダを検索して回る処理では使えません。

どうしても検索が必要な場合は、処理対象を1つのフォルダに集める運用に寄せる手もあります。権限自体はdriveのままでも、スクリプトを実行する専用アカウントに対して、そのフォルダしか共有しないという絞り方です。スコープとアカウント権限は別の層なので、両方で絞ると効きます。

実装例|AI分類スクリプトの最小権限構成

実際によくある構成で確認します。問い合わせシートの本文をClaude APIに送り、分類結果を書き戻すスクリプトです。

// 実装例:問い合わせシートをAIで分類する最小権限スクリプト
// 必要なスコープは spreadsheets.currentonly と script.external_request の2つだけ

function classifyInquiries() {
  const sheet = SpreadsheetApp.getActive().getSheetByName("問い合わせ");
  const lastRow = sheet.getLastRow();
  if (lastRow < 2) return;

  // A列=本文, B列=分類
  const rows = sheet.getRange(2, 1, lastRow - 1, 2).getValues();
  const output = rows.map((row) => {
    const body = row[0];
    const done = row[1];
    if (!body || done) return [done];
    return [classifyByClaude_(body)];
  });

  sheet.getRange(2, 2, output.length, 1).setValues(output);
}

function classifyByClaude_(body) {
  const apiKey = PropertiesService.getScriptProperties().getProperty("ANTHROPIC_API_KEY");

  const res = UrlFetchApp.fetch("https://api.anthropic.com/v1/messages", {
    method: "post",
    contentType: "application/json",
    headers: { "x-api-key": apiKey, "anthropic-version": "2023-06-01" },
    payload: JSON.stringify({
      model: "claude-opus-5",
      max_tokens: 64,
      system: "問い合わせを 見積 / 不具合 / その他 のいずれか1語で分類し、その語だけを返してください。",
      messages: [{ role: "user", content: String(body).slice(0, 2000) }],
    }),
    muteHttpExceptions: true,
  });

  if (res.getResponseCode() !== 200) {
    console.error("APIエラー: " + res.getResponseCode());
    return "";
  }
  return JSON.parse(res.getContentText()).content[0].text.trim();
}

// PropertiesService と Utilities は追加スコープ不要。
// トリガーで定期実行するなら script.scriptapp を1行足す。

このスクリプトに必要なスコープはspreadsheets.currentonlyscript.external_requestの2つだけです。AIを使うと聞くと大がかりに感じますが、権限としては「このシートを読み書きする」「外部に通信する」以上のことをしていません。

ポイントはPropertiesService(設定値の保管庫)やUtilitiesには追加スコープが要らないことです。APIキーをコードに直書きせずPropertiesServiceに置く運用は、権限面でのコストがゼロで実現できます。

現在どのスコープが承認されているかは、コードからも確認できます。

// 現在の承認スコープをコードから確認する
function showGrantedScopes() {
  const token = ScriptApp.getOAuthToken();
  const url = "https://oauth2.googleapis.com/tokeninfo?access_token=" + token;

  const res = UrlFetchApp.fetch(url, { muteHttpExceptions: true });
  const info = JSON.parse(res.getContentText());

  // 実際に承認されているスコープが一覧で出る
  console.log(info.scope.split(" ").join("\n"));
}

// 実行には script.external_request スコープが必要。
// マニフェストに書いた内容と実際の承認内容がずれていないかの確認に使える。

ScriptApp.getOAuthToken()で現在のアクセストークンを取り、Googleのtokeninfoエンドポイントに問い合わせる方法です。マニフェストの記述と実際の承認内容がずれていないかを、実行ログで確認できます。

変更後の再承認と再デプロイ

スコープを書き換えたら、次の実行時に承認画面がもう一度出ます。増やしたときだけでなく、減らしたときも同じです。承認済みの内容が作り直されるためで、異常ではありません。利用者が複数いる場合は、事前に「承認画面が再度出ます」と伝えておくと問い合わせを減らせます。

見落としやすいのがWebアプリとして公開している場合です。GASのWebアプリはデプロイしたバージョンのコードで動きます。マニフェストを直しただけでは公開中のURLには反映されないため、「デプロイ > デプロイを管理」から新しいバージョンを作成し直してください。トリガーで動かしているスクリプトも、スコープ変更後に一度手動実行して承認を通しておくと、次回のトリガーが権限エラーで止まるのを防げます。

なお、既存の承認を明示的に取り消したい場合は、Googleアカウントのセキュリティ設定にある「サードパーティ製アプリとの連携」から該当のスクリプトのアクセス権を削除します。テスト時に「承認画面をもう一度最初から見たい」ときにも使えます。

実務での注意点

1. ライブラリのスコープも合算される

外部のGASライブラリを読み込むと、そのライブラリが必要とするスコープも承認対象になります。便利なライブラリを1つ入れただけで承認画面が長くなることがあるので、公開元と必要権限を確認してから使ってください。自作ライブラリを使う場合も、共通処理側で使うAPIが呼び出し側の権限要求に効いてきます。

2. コメントアウトしたコードでも判定される場合がある

自動判定はコード全体の解析で行われるため、使っていない関数の中に残ったDriveAppの1行が権限を広げていることがあります。oauthScopesを明示すればこの影響は受けませんが、いずれにせよ使わなくなったコードは消しておくのが安全です。

3. 「誰として実行するか」も合わせて設計する

スコープは「何ができるか」の設定であり、「誰のデータに対してか」は実行アカウントで決まります。管理者アカウントで動かしていれば、狭いスコープでもアクセスできる範囲は広いままです。業務自動化では、必要なファイルだけを共有した専用アカウントでトリガーを設定するのが定石です。

4. claspとGitで差分を残す

appsscript.jsonはGASの公式CLIであるclaspで手元に取り込めます。Gitで管理しておけば、「いつ誰がスコープを追加したか」が履歴に残ります。権限が知らないうちに広がっていく事態を防ぐには、レビュー対象のファイルにしてしまうのが確実です。

5. 社外配布では審査の対象になりうる

組織外の不特定多数に配布し、Gmailの読み取りなど機微なスコープを求める場合は、GoogleのOAuth検証が関わります。社内利用や自分専用のスクリプトでは基本的に対象外ですが、将来的に配布する可能性があるなら、この段階からスコープを狭く保っておくと後の手間が減ります。

まとめ

GASの承認画面が大げさに見えるのは、スコープの自動判定が安全側に倒れているからです。appsscript.jsonoauthScopesを書けば、要求する権限を自分で決められます。

絞り方の勘所は3つです。バインド型のシート操作はspreadsheets.currentonlyで足りないか確認する。送信だけのメールはMailAppに寄せる。ドライブはdrive.fileで組めないか考える。この3点だけで、多くのスクリプトは承認画面が数行に収まります。

権限を絞る作業は、機能を増やしません。それでも、利用者に安心して承認してもらえること、そして何かあったときに被害が広がらないことは、業務で動かし続けるスクリプトにとって確かな価値です。まずは狭く書いて、エラーが出たら足す。それだけで十分に始められます。

よくある質問

スクリプトがGoogleアカウントのどのデータに触れてよいかを表す権限の単位です。スプレッドシートを読む、メールを送る、外部サイトへ通信する、といった操作ごとにURL形式の識別子が決まっています。初回実行時に出る承認画面は、このスコープの一覧を人が読める文章に直したものです。

コードのどこかでDriveApp、または他のスプレッドシートを開くopenByIdを使っている可能性が高いです。GASはoauthScopesを書かないとコードを解析して必要そうなスコープを自動で決めますが、この自動判定は安全側に倒れるため広めになります。1行の便利メソッドが全ドライブ分の権限を呼び込むことがあります。

GASが自動でスコープを決めます。動作はしますが、必要以上に広い権限を利用者に求めることになり、コードを変えるたびに要求内容が黙って増減します。業務で配布するスクリプトなら、oauthScopesを明示して固定するほうが安全です。書いた瞬間に「このスクリプトができること」がファイル上で読めるようになります。

コードが必要とするスコープを書き忘れると、実行時に権限不足のエラーになります。逆に言えば、足りない場合はエラーメッセージに必要なスコープのURLが表示されるため、それを追記すれば直せます。まず狭く書いて、エラーが出たら足す進め方が確実です。

必要です。スコープを増やした場合はもちろん、減らした場合も承認情報が作り直されるため、次回実行時に承認画面が再度表示されます。Webアプリとして公開している場合は、新しいバージョンでデプロイし直さないと変更が反映されない点にも注意してください。

社内で使う分には基本的に不要です。同じGoogle Workspace組織内の利用や、自分だけが使うスクリプトは審査の対象外です。審査が関わるのは、組織外の不特定多数へ配布し、Gmailの読み取りなど機微なスコープを求める場合です。この観点でも、スコープを狭く保つメリットがあります。

安心して承認できる
業務スクリプトを。

権限設計から実装・運用まで。社内で長く使えるGAS自動化を、必要最小限の構成でご提案します。