Google Apps Script + OAuthでクライアントアプリを作る
Google Apps ScriptとスプレッドシートでOAuth 2.0クライアントを構築し、マネーフォワード クラウドのAPIにアクセスする実装手順を解説します。コーディング環境のセットアップが不要なため、OAuth 2.0の仕組みを手軽に体験したい場合に適しています。
対象読者: Google Apps ScriptでOAuth 2.0の仕組みを学びたい開発者。
学習内容
このチュートリアルを完了すると、以下のことができるようになります:
- OAuth 2.0の認可フローをGoogle Apps Scriptで実装する
- アクセストークンを取得してAPIを呼び出す
- トークンのリフレッシュを自動化する
- 本番アプリに組み込む際のセキュリティ上の注意点を理解する
チュートリアルの流れ
- このチュートリアルで作るもの
- スプレッドシートに3つのボタンを配置したサンプルアプリの概要と、動作フローを確認します。
- 事前準備
- Google Workspace、apps-script-oauth2ライブラリ、クライアントID・シークレット、リダイレクトURIを準備します。
- 実装手順
- スプレッドシートへのボタン配置から、OAuthサービス定義・認可・コールバック・API呼び出し・トークン削除まで7ステップで実装します。
- 動作確認
- 各ボタンの期待結果と、よくある失敗の対処法を確認します。
- 本番利用時の注意
- シークレットの保存方法、トークン表示の削除、
reset()とrevokeToken()の違いについて解説します。
- シークレットの保存方法、トークン表示の削除、
このチュートリアルで作るもの
スプレッドシートに3つのボタンを配置し、OAuth 2.0の認可フローをひと通り体験できるサンプルアプリを作ります。
注意
このサンプルはOAuthの動作を学ぶための最小構成です。本番アプリへそのまま使用しないでください。本番利用時の注意はこちらを参照してください。
スプレッドシートのレイアウト

| コンポーネント | 役割 |
|---|---|
| ① 認可開始ボタン | 認可URLを開き、ユーザーに権限を付与してもらう |
| ② API呼び出しボタン | アクセストークンを使ってAPIを呼び出す |
| ③ トークン削除ボタン | ローカルに保存されたトークンを削除する |
| ④ トークン表示エリア | トークン情報を学習用に表示する |
| ⑤ APIレスポンス表示エリア | APIレスポンスを学習用に表示する |
動作フロー
①認可開始クリック
↓
認可画面(ブラウザ新規タブ)
↓ ユーザーが「許可」をクリック
authCallback が呼ばれる(リダイレクトURI経由)
↓ ライブラリがトークンをプロパティストアへ自動保存
②API呼び出しクリック
↓ Bearer トークンをヘッダーに付与
APIレスポンスがA14セルに表示
↓
③トークン削除クリック
↓
プロパティストアのトークンが削除される
事前準備
Google Workspace
Google Workspaceのアカウントを用意し、ログインしてください。組織の設定によってはGoogle Apps Scriptの使用が制限されている場合があります。事前に確認してください。
Apps Script
以下の公式ドキュメントを参照して、スプレッドシートとそれにバインドされたApps Scriptプロジェクトを作成してください。
apps-script-oauth2 ライブラリ
本チュートリアルでは apps-script-oauth2 ライブラリを使用します。
- Apps Scriptコードエディターを開きます
- メニューバーの
ライブラリ横にある+ボタンをクリックします - 以下のスクリプトIDを入力して
検索をクリックします:1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF - バージョンを選択し(最新を推奨)、
追加をクリックします ライブラリの下にOAuth2が追加されたことを確認します
詳細は公式ドキュメントを参照してください。
クライアントIDとシークレット
アプリをまだ登録していない場合は、こちらを参照してアプリを登録し、クライアントIDとシークレットを取得してください。
リダイレクトURIの登録
Apps Scriptの「プロジェクトの設定」からスクリプトIDをコピーし、以下の形式でリダイレクトURIを作成してください。
https://script.google.com/macros/d/{SCRIPT_ID}/usercallback

アプリポータルでの登録手順は以下のとおりです。
- アプリポータルにログイン
アプリ開発 [開発者向け]を選択- 対象のアプリケーションを選択
リダイレクトURIの設定画面で上記のURIを登録
実装手順
Apps Scriptのプロジェクトを開き、コード.gsに以下のコードを追加してください。初期状態で作成されているmyFunction関数は削除してください。
1. スプレッドシートにボタンを配置する
スプレッドシートを開き、挿入メニューから図形描画を選択して以下の3つのボタンを作成してください。
- 「認可開始 / Start Authorize」
- 「APIを呼び出し、保護されたリソースを取得 / Call API to fetch protected resource」
- 「トークンを取り消し / Revoke tokens」
セルA4・A5にトークン情報、セルA14にAPIの実行結果を表示します(コードから書き込みます)。
図形描画の詳細はこちらを確認してください。
2. OAuthサービスを定義する
注意
このサンプルではCLIENT_IDとCLIENT_SECRETをハードコーディングしています。実際のアプリでは環境変数や秘密管理サービスに保存してください。詳しくは本番利用時の注意 > シークレットの保存を参照してください。
const CLIENT_ID = 'YOUR CLIENT_ID';
const CLIENT_SECRET = 'YOUR CLIENT_SECRET';
const AUTHORIZATION_SERVER = 'https://api.biz.moneyforward.com';
const SCOPE = ['mfc/admin/tenant.read']; // 必要なスコープを設定する
const oauthService = getOAuthService();
function getOAuthService() {
const service = OAuth2.createService('MFAPI')
.setAuthorizationBaseUrl(AUTHORIZATION_SERVER + '/authorize')
.setTokenUrl(AUTHORIZATION_SERVER + '/token')
.setClientId(CLIENT_ID)
.setCallbackFunction('authCallback')
.setScope(SCOPE)
.generateCodeVerifier()
.setPropertyStore(PropertiesService.getUserProperties());
// CLIENT_SECRET_POST(デフォルト)
service.setClientSecret(CLIENT_SECRET);
// CLIENT_SECRET_BASICを使う場合は上の行を削除し、以下を有効にする:
// service.setTokenHeaders({
// Authorization: 'Basic ' + Utilities.base64Encode(CLIENT_ID + ':' + CLIENT_SECRET),
// });
return service;
}
スコープの設定を誤った場合、認可は成功しますがAPIリクエスト時に403エラーが発生します。使用するAPIのドキュメントで必要なスコープを確認してください。
3. 認可開始を実装する
function startAuthorization() {
if (!oauthService.hasAccess()) {
const authorizationUrl = oauthService.getAuthorizationUrl();
const script = `
<script>
window.open('${authorizationUrl}', '_blank').focus();
</script>
<p>自動で別タブが開きます</p>
<p>別タブでの認可が完了したらこのダイアログは閉じてください</p>
`;
const html = HtmlService.createHtmlOutput(script);
SpreadsheetApp.getUi().showModalDialog(html, '認可を開始します');
} else {
const html = HtmlService.createHtmlOutput('認可済みです');
SpreadsheetApp.getUi().showModalDialog(html, '認可状態');
}
}
未認可の場合は認可URLを新しいタブで開きます。認可済みの場合はその旨を表示します。
4. コールバックを実装する
認可完了後、認可サーバーからこのコールバック関数にリダイレクトされます。アクセストークンとリフレッシュトークンはライブラリがプロパティストアに自動保存します。
function authCallback(request) {
const isAuthorized = oauthService.handleCallback(request);
const sheet = SpreadsheetApp.getActiveSheet();
try {
if (isAuthorized) {
// 学習用にトークンをスプレッドシートへ表示(本番では不要)
const token = oauthService.getToken();
sheet.getRange('A4').setValue('Access Token: ' + token.access_token);
sheet.getRange('A5').setValue('Refresh Token: ' + token.refresh_token);
sheet.getRange('A14').setValue('');
return HtmlService.createHtmlOutput(
'認可が完了しました。このタブは閉じてください。',
);
} else {
sheet.getRange('A4').setValue('認可が拒否されました');
sheet.getRange('A5').setValue('');
sheet.getRange('A14').setValue('');
return HtmlService.createHtmlOutput(
'認可が拒否されました。このタブは閉じてください。',
);
}
} catch (error) {
console.error('認可処理中にエラーが発生しました:', error);
sheet.getRange('A4').setValue('認可処理中にエラーが発生しました');
sheet.getRange('A5').setValue('');
sheet.getRange('A14').setValue('');
return HtmlService.createHtmlOutput(
'認可処理中にエラーが発生しました。詳細はログを確認してください。',
);
}
}
このライブラリはアクセストークンの期限切れを自動検出し、リフレッシュリクエストを内部で実行します。明示的なリフレッシュ処理は不要です。
5. API呼び出しを実装する
function showTenant() {
if (!oauthService.hasAccess()) {
const html = HtmlService.createHtmlOutput(
'認可が必要です。先に認可を行ってください。',
);
SpreadsheetApp.getUi().showModalDialog(html, '認可が必要です');
return;
}
try {
const response = UrlFetchApp.fetch(
'https://api.biz.moneyforward.com/v2/tenant',
{
headers: {
Authorization: 'Bearer ' + oauthService.getAccessToken(),
},
},
);
const myJson = JSON.parse(response.getContentText());
const sheet = SpreadsheetApp.getActiveSheet();
sheet.getRange('A14').setValue(JSON.stringify(myJson));
// 最新のトークン情報を表示(学習用)
const token = oauthService.getToken();
sheet.getRange('A4').setValue('Access Token: ' + token.access_token);
sheet.getRange('A5').setValue('Refresh Token: ' + token.refresh_token);
} catch (error) {
console.error('API呼び出しに失敗しました:', error);
const sheet = SpreadsheetApp.getActiveSheet();
sheet
.getRange('A14')
.setValue('API呼び出しに失敗しました: ' + error.toString());
const html = HtmlService.createHtmlOutput(
'API呼び出しに失敗しました。詳細はA14セルを確認してください。',
);
SpreadsheetApp.getUi().showModalDialog(html, 'エラー');
}
}
/v2/tenantは事業者情報取得APIです。要件に応じて呼び出すAPIを変更してください。
6. トークン削除を実装する
function revokeToken() {
try {
oauthService.reset();
const sheet = SpreadsheetApp.getActiveSheet();
sheet.getRange('A4').setValue('');
sheet.getRange('A5').setValue('');
sheet.getRange('A14').setValue('');
const html = HtmlService.createHtmlOutput('トークンの削除が完了しました');
SpreadsheetApp.getUi().showModalDialog(html, '成功');
} catch (error) {
console.error('トークンの削除に失敗しました:', error);
const html = HtmlService.createHtmlOutput(
'トークンの削除に失敗しました。詳細はログを確認してください。',
);
SpreadsheetApp.getUi().showModalDialog(html, 'エラー');
}
}
7. 図形に関数を割り当てる
スプレッドシートの図形①〜③にそれぞれ関数を割り当てます。
- 図形を選択します
- 図形右端の縦3点メニューをクリックします
スクリプトを割り当てを選択します- 以下の関数名を入力して割り当てます
| 図形 | 割り当てる関数 |
|---|---|
| ①認可開始 / Start Authorize | startAuthorization |
| ②APIを呼び出し、保護されたリソースを取得 / Call API to fetch protected resource | showTenant |
| ③トークンを取り消し / Revoke tokens | revokeToken |
誤って割り当てた場合は、図形を右クリックして選択するとメニューから設定を変更できます。
動作確認
はじめて実行する場合や時間が経過した後は、Googleからスクリプトへのアクセス許可を求める画面が表示されます。画面にしたがって許可してください。
期待結果
- 認可の実行: ①のボタンをクリックします。別タブに認可画面が開くので、ログインして
許可をクリックします。セルA4・A5にトークンが表示されれば成功です。 - APIの呼び出し: ②のボタンをクリックします。A14セルにAPIのレスポンスが表示されれば成功です。
- トークンの削除: ③のボタンをクリックします。セルA4・A5・A14がクリアされれば成功です。
よくある失敗
| 症状 | 原因 | 対処 |
|---|---|---|
| APIリクエストで403エラー | スコープの設定ミス | SCOPEの値を使用するAPIの必要スコープに合わせる |
| リダイレクトURIのエラー | アプリポータルへの登録漏れ | スクリプトIDを確認し、正しいリダイレクトURIを登録する |
| 「認可が拒否されました」と表示される | 認可画面でキャンセルした | 再度①を実行し、認可画面で「許可」をクリックする |
| Googleの権限確認画面でエラー | スクリプトへのアクセスを未許可 | 権限確認画面の指示にしたがって許可する |
| トークン取得時に401エラー | クライアント認証方式の不一致 | 認可サーバーが要求する方式に合わせてCLIENT_SECRET_POST(デフォルト)かCLIENT_SECRET_BASICを選択する |
本番利用時の注意
このサンプルは学習用の最小構成です。本番アプリに組み込む際は以下の点を対処してください。
シークレットの保存
CLIENT_IDとCLIENT_SECRETはコードにハードコーディングせず、スクリプトプロパティに保存してください。
// ハードコーディングの代わりに:
const CLIENT_ID =
PropertiesService.getScriptProperties().getProperty('CLIENT_ID');
const CLIENT_SECRET =
PropertiesService.getScriptProperties().getProperty('CLIENT_SECRET');
トークンの保存
ライブラリはPropertiesService.getUserProperties()にトークンを保存します。これは実行ユーザーごとに分離されたストアで、他のユーザーからはアクセスできません。本番でもそのまま使用できます。
トークンの表示を削除する
authCallbackとshowTenant内でセルA4・A5にトークンを書き込む処理は学習用です。本番では削除してください。トークンがスプレッドシートに残ると、シートを閲覧できる第三者にトークンが漏洩するリスクがあります。
reset と revoke の違い
このサンプルのrevokeToken()はoauthService.reset()を呼び出しています。reset()はプロパティストアに保存されたトークンを削除するだけで、認可サーバー側のトークンは無効化されません。
認可サーバー側も含めてトークンを完全に無効化するには、ライブラリのrevokeToken()メソッドを使用してください。
// 認可サーバー側も含めてトークンを無効化する
oauthService.revokeToken();
完成コード
// コード.gs
const CLIENT_ID = 'YOUR CLIENT_ID';
const CLIENT_SECRET = 'YOUR CLIENT_SECRET';
const AUTHORIZATION_SERVER = 'https://api.biz.moneyforward.com';
const SCOPE = ['mfc/admin/tenant.read'];
const oauthService = getOAuthService();
function getOAuthService() {
const service = OAuth2.createService('MFAPI')
.setAuthorizationBaseUrl(AUTHORIZATION_SERVER + '/authorize')
.setTokenUrl(AUTHORIZATION_SERVER + '/token')
.setClientId(CLIENT_ID)
.setCallbackFunction('authCallback')
.setScope(SCOPE)
.generateCodeVerifier()
.setPropertyStore(PropertiesService.getUserProperties());
// CLIENT_SECRET_POST(デフォルト)
service.setClientSecret(CLIENT_SECRET);
// CLIENT_SECRET_BASICを使う場合は上の行を削除し、以下を有効にする:
// service.setTokenHeaders({
// Authorization: 'Basic ' + Utilities.base64Encode(CLIENT_ID + ':' + CLIENT_SECRET),
// });
return service;
}
function startAuthorization() {
if (!oauthService.hasAccess()) {
const authorizationUrl = oauthService.getAuthorizationUrl();
const script = `
<script>
window.open('${authorizationUrl}', '_blank').focus();
</script>
<p>自動で別タブが開きます</p>
<p>別タブでの認可が完了したらこのダイアログは閉じてください</p>
`;
const html = HtmlService.createHtmlOutput(script);
SpreadsheetApp.getUi().showModalDialog(html, '認可を開始します');
} else {
const html = HtmlService.createHtmlOutput('認可済みです');
SpreadsheetApp.getUi().showModalDialog(html, '認可状態');
}
}
function authCallback(request) {
const isAuthorized = oauthService.handleCallback(request);
const sheet = SpreadsheetApp.getActiveSheet();
try {
if (isAuthorized) {
const token = oauthService.getToken();
sheet.getRange('A4').setValue('Access Token: ' + token.access_token);
sheet.getRange('A5').setValue('Refresh Token: ' + token.refresh_token);
sheet.getRange('A14').setValue('');
return HtmlService.createHtmlOutput(
'認可が完了しました。このタブは閉じてください。',
);
} else {
sheet.getRange('A4').setValue('認可が拒否されました');
sheet.getRange('A5').setValue('');
sheet.getRange('A14').setValue('');
return HtmlService.createHtmlOutput(
'認可が拒否されました。このタブは閉じてください。',
);
}
} catch (error) {
console.error('認可処理中にエラーが発生しました:', error);
sheet.getRange('A4').setValue('認可処理中にエラーが発生しました');
sheet.getRange('A5').setValue('');
sheet.getRange('A14').setValue('');
return HtmlService.createHtmlOutput(
'認可処理中にエラーが発生しました。詳細はログを確認してください。',
);
}
}
function showTenant() {
if (!oauthService.hasAccess()) {
const html = HtmlService.createHtmlOutput(
'認可が必要です。先に認可を行ってください。',
);
SpreadsheetApp.getUi().showModalDialog(html, '認可が必要です');
return;
}
try {
const response = UrlFetchApp.fetch(
'https://api.biz.moneyforward.com/v2/tenant',
{
headers: {
Authorization: 'Bearer ' + oauthService.getAccessToken(),
},
},
);
const myJson = JSON.parse(response.getContentText());
const sheet = SpreadsheetApp.getActiveSheet();
sheet.getRange('A14').setValue(JSON.stringify(myJson));
const token = oauthService.getToken();
sheet.getRange('A4').setValue('Access Token: ' + token.access_token);
sheet.getRange('A5').setValue('Refresh Token: ' + token.refresh_token);
} catch (error) {
console.error('API呼び出しに失敗しました:', error);
const sheet = SpreadsheetApp.getActiveSheet();
sheet
.getRange('A14')
.setValue('API呼び出しに失敗しました: ' + error.toString());
const html = HtmlService.createHtmlOutput(
'API呼び出しに失敗しました。詳細はA14セルを確認してください。',
);
SpreadsheetApp.getUi().showModalDialog(html, 'エラー');
}
}
function revokeToken() {
try {
oauthService.reset();
const sheet = SpreadsheetApp.getActiveSheet();
sheet.getRange('A4').setValue('');
sheet.getRange('A5').setValue('');
sheet.getRange('A14').setValue('');
const html = HtmlService.createHtmlOutput('トークンの削除が完了しました');
SpreadsheetApp.getUi().showModalDialog(html, '成功');
} catch (error) {
console.error('トークンの削除に失敗しました:', error);
const html = HtmlService.createHtmlOutput(
'トークンの削除に失敗しました。詳細はログを確認してください。',
);
SpreadsheetApp.getUi().showModalDialog(html, 'エラー');
}
}