メインコンテンツまでスキップ

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を呼び出す
  • トークンのリフレッシュを自動化する
  • 本番アプリに組み込む際のセキュリティ上の注意点を理解する

チュートリアルの流れ

  1. このチュートリアルで作るもの
    • スプレッドシートに3つのボタンを配置したサンプルアプリの概要と、動作フローを確認します。
  2. 事前準備
    • Google Workspace、apps-script-oauth2ライブラリ、クライアントID・シークレット、リダイレクトURIを準備します。
  3. 実装手順
    • スプレッドシートへのボタン配置から、OAuthサービス定義・認可・コールバック・API呼び出し・トークン削除まで7ステップで実装します。
  4. 動作確認
    • 各ボタンの期待結果と、よくある失敗の対処法を確認します。
  5. 本番利用時の注意
    • シークレットの保存方法、トークン表示の削除、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 ライブラリを使用します。

  1. Apps Scriptコードエディターを開きます
  2. メニューバーのライブラリ横にある+ボタンをクリックします
  3. 以下のスクリプトIDを入力して検索をクリックします: 1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF
  4. バージョンを選択し(最新を推奨)、追加をクリックします
  5. ライブラリの下にOAuth2が追加されたことを確認します
備考

詳細は公式ドキュメントを参照してください。

クライアントIDとシークレット

アプリをまだ登録していない場合は、こちらを参照してアプリを登録し、クライアントIDとシークレットを取得してください。

リダイレクトURIの登録

Apps Scriptの「プロジェクトの設定」からスクリプトIDをコピーし、以下の形式でリダイレクトURIを作成してください。

https://script.google.com/macros/d/{SCRIPT_ID}/usercallback

スクリプトIDのコピー

アプリポータルでの登録手順は以下のとおりです。

  1. アプリポータルにログイン
  2. アプリ開発 [開発者向け]を選択
  3. 対象のアプリケーションを選択
  4. リダイレクト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_IDCLIENT_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. 図形に関数を割り当てる

スプレッドシートの図形①〜③にそれぞれ関数を割り当てます。

  1. 図形を選択します
  2. 図形右端の縦3点メニューをクリックします
  3. スクリプトを割り当てを選択します
  4. 以下の関数名を入力して割り当てます
図形割り当てる関数
①認可開始 / Start AuthorizestartAuthorization
②APIを呼び出し、保護されたリソースを取得 / Call API to fetch protected resourceshowTenant
③トークンを取り消し / Revoke tokensrevokeToken
ヒント

誤って割り当てた場合は、図形を右クリックして選択するとメニューから設定を変更できます。


動作確認

はじめて実行する場合や時間が経過した後は、Googleからスクリプトへのアクセス許可を求める画面が表示されます。画面にしたがって許可してください。

認可画面の例 ログイン画面の例 権限確認画面の例

期待結果

  1. 認可の実行: ①のボタンをクリックします。別タブに認可画面が開くので、ログインして許可をクリックします。セルA4・A5にトークンが表示されれば成功です。
  2. APIの呼び出し: ②のボタンをクリックします。A14セルにAPIのレスポンスが表示されれば成功です。
  3. トークンの削除: ③のボタンをクリックします。セルA4・A5・A14がクリアされれば成功です。

よくある失敗

症状原因対処
APIリクエストで403エラースコープの設定ミスSCOPEの値を使用するAPIの必要スコープに合わせる
リダイレクトURIのエラーアプリポータルへの登録漏れスクリプトIDを確認し、正しいリダイレクトURIを登録する
「認可が拒否されました」と表示される認可画面でキャンセルした再度①を実行し、認可画面で「許可」をクリックする
Googleの権限確認画面でエラースクリプトへのアクセスを未許可権限確認画面の指示にしたがって許可する
トークン取得時に401エラークライアント認証方式の不一致認可サーバーが要求する方式に合わせてCLIENT_SECRET_POST(デフォルト)かCLIENT_SECRET_BASICを選択する

本番利用時の注意

このサンプルは学習用の最小構成です。本番アプリに組み込む際は以下の点を対処してください。

シークレットの保存

CLIENT_IDCLIENT_SECRETはコードにハードコーディングせず、スクリプトプロパティに保存してください。

// ハードコーディングの代わりに:
const CLIENT_ID =
PropertiesService.getScriptProperties().getProperty('CLIENT_ID');
const CLIENT_SECRET =
PropertiesService.getScriptProperties().getProperty('CLIENT_SECRET');

トークンの保存

ライブラリはPropertiesService.getUserProperties()にトークンを保存します。これは実行ユーザーごとに分離されたストアで、他のユーザーからはアクセスできません。本番でもそのまま使用できます。

トークンの表示を削除する

authCallbackshowTenant内でセル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, 'エラー');
}
}