GAS『OAuth2 Authorization』の原因と対処法
- 作成日 2025.10.22
- その他
Google Apps Scriptから外部API/Google APIにOAuth2で接続するとき、認可フローやクライアント設定、リダイレクトURI、スコープ、トークン保存の不整合で失敗する。ここでは発生条件の整理、最短復旧の手順、実用テンプレコード、典型的なエラーごとの修正ポイントをまとめる。
- 1. エラーメッセージと起きる場面
- 2. 発生条件の早見表
- 3. 最短復旧フロー(上から順に確認)
- 4. OAuth2ライブラリの導入と基本サービス定義
- 5. リダイレクトURIとコールバック関数を合わせる
- 6. トークン保存・更新・ヘッダー付与の実装
- 7. Google APIをOAuth2(HTTP)で呼ぶ最小例
- 8. invalid_grant/redirect_uri_mismatchの修正ポイント
- 9. スコープ設計と最小権限の原則
- 10. GCPプロジェクトの整合・検証ステータス
- 11. 複数ユーザー運用(Webアプリ/トリガーと実行主体)
- 12. よくある失敗→修正(クイック表)
- 13. 動作確認テンプレ(権限とAPI応答をログ)
- 14. 再発防止チェックリスト
エラーメッセージと起きる場面
「Authorization Error」「redirect_uri_mismatch」「invalid_client」「invalid_grant」「access_denied」「insufficient_scope」「unauthorized_client」などが代表。WebアプリのdoGet/doPost、HTMLサービスからの開始、UrlFetchAppでのAPI呼び出し時、トリガー実行時に発生しやすい。
発生条件の早見表
クライアントID/シークレットの誤りや環境(本番・テスト)取り違え。承認済みのリダイレクトURIにApps ScriptのコールバックURLが未登録。スコープ不足または過剰。ユーザー/組織側で同意が未完了。リフレッシュトークン未取得(offlineアクセス未指定)や失効。GCPプロジェクト不整合(別プロジェクトでデプロイ)。実行主体(オーナー/アクセスユーザー)の食い違い。
最短復旧フロー(上から順に確認)
クライアントID・シークレットが正しいか確認。承認済みリダイレクトURIに「Apps Scriptのコールバック」を追加。スコープは最小限で列挙。オーナーがまず認可を通す。リフレッシュトークン取得のためaccess_type=offlineとprompt=consentを付与。トークンはUserPropertiesなどに保存。401/invalid_grant時は再同意へフォールバック。
OAuth2ライブラリの導入と基本サービス定義
// 必要:Apps Scriptの「ライブラリ」で OAuth2 を追加済みであること
// 外部サービス例(authorization_code)
function getService() {
return OAuth2.createService('example') // サービス名(ユーザーごとのトークンを区別)
.setAuthorizationBaseUrl('https://provider.example.com/oauth/authorize')
.setTokenUrl('https://provider.example.com/oauth/token')
.setClientId(PropertiesService.getScriptProperties().getProperty('CLIENT_ID'))
.setClientSecret(PropertiesService.getScriptProperties().getProperty('CLIENT_SECRET'))
.setCallbackFunction('authCallback') // コールバックは後述
.setPropertyStore(PropertiesService.getUserProperties()) // ユーザー単位で保存
.setScope('read write') // プロバイダ仕様に合わせる(スペース区切りやカンマ区切り)
.setParam('access_type', 'offline') // Google系で長期トークンが必要なとき
.setParam('prompt', 'consent'); // 毎回同意画面を出してrefresh_token取得
}リダイレクトURIとコールバック関数を合わせる
// 1) コールバック(固定名でOK、サービス名は自動で付与される)
function authCallback(request) {
const service = getService();
const isOk = service.handleCallback(request);
return HtmlService.createHtmlOutput(isOk ? 'Success' : 'Denied');
}
// 2) 承認開始(WebアプリURLにアクセス → 認可画面へ)
function doGet(e) {
const service = getService();
if (!service.hasAccess()) {
const url = service.getAuthorizationUrl();
return HtmlService.createHtmlOutput('<a href="' + url + '">Authorize</a>');
}
return HtmlService.createHtmlOutput('Already authorized');
}
// ※ プロバイダのダッシュボードに登録するリダイレクトURI:
// https://script.google.com/macros/d/{SCRIPT_ID}/usercallback
// (OAuth2ライブラリの既定コールバックに合わせる)
トークン保存・更新・ヘッダー付与の実装
// 認可済みかを確認し、APIを呼ぶ
function callApi(endpoint) {
const service = getService();
if (!service.hasAccess()) throw new Error('Not authorized');
const headers = { Authorization: 'Bearer ' + service.getAccessToken() };
const res = UrlFetchApp.fetch(endpoint, { headers, muteHttpExceptions: true });
const code = res.getResponseCode();
if (code === 401) {
// 期限切れや撤回。再同意へ誘導(例:URLをログに出す)
service.reset(); // 既存トークンを破棄
console.log('Re-auth URL:', service.getAuthorizationUrl());
throw new Error('Unauthorized. Please re-authorize.');
}
return res.getContentText();
}Google APIをOAuth2(HTTP)で呼ぶ最小例
// 例:Google Drive API v3 をHTTPで直接呼ぶ場合
function getDriveFiles() {
const service = OAuth2.createService('gdrive')
.setAuthorizationBaseUrl('https://accounts.google.com/o/oauth2/v2/auth')
.setTokenUrl('https://oauth2.googleapis.com/token')
.setClientId(PropertiesService.getScriptProperties().getProperty('G_CLIENT_ID'))
.setClientSecret(PropertiesService.getScriptProperties().getProperty('G_CLIENT_SECRET'))
.setCallbackFunction('authCallback')
.setPropertyStore(PropertiesService.getUserProperties())
.setScope('https://www.googleapis.com/auth/drive.metadata.readonly')
.setParam('access_type', 'offline')
.setParam('prompt', 'consent');
if (!service.hasAccess()) {
throw new Error('Authorize: ' + service.getAuthorizationUrl());
}
const res = UrlFetchApp.fetch(
'https://www.googleapis.com/drive/v3/files?pageSize=10',
{ headers: { Authorization: 'Bearer ' + service.getAccessToken() } }
);
return JSON.parse(res.getContentText());
}invalid_grant/redirect_uri_mismatchの修正ポイント
invalid_grantは、リフレッシュトークン未発行、同意撤回、クライアント切り替え、時間ずれで出やすい。access_type=offlineとprompt=consentを付けて再同意し、古いトークンを破棄。redirect_uri_mismatchは、プロバイダ側の承認済みリダイレクトURIに「Apps Scriptのusercallback」を正確に登録して一致させる。
スコープ設計と最小権限の原則
必要なスコープだけを列挙。Google APIはURL形式のフルスコープを用いる。複数スコープはスペース区切り。不要なスコープは削除し、再同意を促してリスクと審査負荷を下げる。
GCPプロジェクトの整合・検証ステータス
クライアントIDを発行したGCPプロジェクトと、スクリプトが紐づくプロジェクトを混同しない。OAuth同意画面(テスト/本番)に応じてユーザー制限が変わる。社内利用はドメイン内ユーザーに限定。本番公開やセンシティブスコープは検証要件に注意。
複数ユーザー運用(Webアプリ/トリガーと実行主体)
ライブラリのPropertyStoreをUserPropertiesにしてユーザーごとにトークンを分離。ウェブアプリの「実行するユーザー」を設計に合わせる。トリガーで外部APIを叩く場合、トリガー作成者のトークンが必要になるため、作成者で認可を通す。
よくある失敗→修正(クイック表)
redirect_uri_mismatchはコールバックURLのタイプミス。invalid_clientはクライアントID/シークレットの環境違い。access_deniedはユーザーが同意を拒否。insufficient_scopeはスコープ不足で再同意が必要。invalid_grantはトークン撤回・期限切れ・再認可要。
動作確認テンプレ(権限とAPI応答をログ)
function diagAuth() {
const svc = getService();
if (!svc.hasAccess()) {
console.log('Authorize:', svc.getAuthorizationUrl());
return;
}
console.log('Access token (short):', (svc.getAccessToken() || '').slice(0,16));
try {
const text = callApi('https://httpbin.org/bearer');
console.log('OK:', text.slice(0,120));
} catch (e) {
console.error('API fail:', e.message);
}
}再発防止チェックリスト
クライアントID/シークレットは環境ごとに分ける。承認済みリダイレクトURIにusercallbackを登録。スコープは最小限で固定し、変更時は再同意。トークンはUserPropertiesに保存し、401発生時はreset→再同意。Webアプリ/トリガーの実行主体を把握し、必要なユーザーで認可を通す。GCPプロジェクトとデプロイ環境の整合を維持する。
-
前の記事
GAS『Cannot Find Method』の原因と対処法 2025.10.22
-
次の記事
GAS『Syntax Error: Unexpected Token』の原因と対処法 2025.10.23
コメントを書く