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

Google Apps Script + APIキーでクライアントアプリを作る

Google Apps ScriptとスプレッドシートでAPIキー認証を用いたクライアントを構築し、マネーフォワード クラウドのAPIにアクセスする実装手順を解説します。コーディング環境のセットアップが不要なため、APIキーをJWTに交換してAPIを呼び出す流れを手軽に体験したい場合に適しています。

アプリポータルで発行したAPIキーを用いて /auth/exchange でJWTを取得し、そのJWTでAPI(保護されたリソース)にアクセスします。

対象読者: Google Apps ScriptでAPIキー認証とJWTの扱いを学びたい開発者。

学習内容

このチュートリアルを完了すると、以下のことができるようになります:

  • APIキーをJWTに交換するリクエストをGoogle Apps Scriptで実装する
  • 取得したJWTを使ってAPI(保護されたリソース)を呼び出す
  • JWTの有効期限を考慮した呼び出し制御の基本を理解する
  • 本番アプリに組み込む際のセキュリティ上の注意点を理解する

チュートリアルの流れ

  1. このチュートリアルで作るもの
    • スプレッドシートに3つのボタンを配置したサンプルアプリの概要と、動作フローを確認します。
  2. 事前準備
    • Google Workspace、Apps Scriptプロジェクト、APIキーを準備します。
  3. 実装手順
    • ボタン配置から、定数設定・JWT取得・API呼び出し・クリア・関数の割り当てまで6ステップで実装します。
  4. 動作確認
    • 各ボタンの期待結果と、よくある失敗の対処法を確認します。
  5. 本番利用時の注意
    • APIキーの保存、JWTのキャッシュとレート制限、スプレッドシート上のJWT表示について解説します。

このチュートリアルで作るもの

スプレッドシートに3つのボタンを配置し、APIキーをJWTに交換してからAPIを呼び出す流れを体験できるサンプルアプリを作ります。

注意

このサンプルはAPIキーとJWTの動作を学ぶための最小構成です。本番アプリへそのまま使用しないでください。本番利用時の注意はこちらを参照してください。

スプレッドシートのレイアウト

スプレッドシートの画面

コンポーネント役割
① JWT取得ボタン/auth/exchange を呼び、APIキーをJWTに交換する
② API呼び出しボタン保存したJWTを使って /v2/tenant/tenant_user にアクセスする
③ 情報クリアボタンユーザープロパティのJWTと、表示用セルをクリアする
④ JWT情報(A4・A5セル)①で得られたJWTとその有効期限・トークン種別を学習用に表示する
⑤ API呼び出し結果(A14セル)②で得られたレスポンスを学習用に表示する

動作フロー

①JWT取得クリック
↓ POST /auth/exchange (Authorization: Bearer APIキー)
JWTをユーザープロパティに保存、A4・A5セルに表示

②API呼び出しクリック
↓ GET /v2/tenant/tenant_user (Authorization: Bearer JWT)
APIレスポンスをA14セルに表示

③情報クリアクリック

ユーザープロパティのJWTと、A4・A5・A14セルをクリア

事前準備

Google Workspace

Google Workspaceのアカウントを用意し、ログインしてください。組織の設定によってはGoogle Apps Scriptの使用が制限されている場合があります。事前に確認してください。

Apps Script

以下の公式ドキュメントを参照して、スプレッドシートとそれにバインドされたApps Scriptプロジェクトを作成してください。

APIキー

本チュートリアルではマネーフォワード クラウドのAPIキーが必要です。まだお持ちでない場合は、次の手順に沿ってアプリポータルでAPIキーを発行してください。

本チュートリアルで使用されるAPIキーは、利用可能サービスとして「事業者情報」が選択されている必要があります。


実装手順

Apps Scriptのプロジェクトを開き、コード.gsに以下のコードを追加してください。初期状態で作成されているmyFunction関数は削除してください。

1. スプレッドシートにボタンを配置する

スプレッドシートを開き、挿入メニューから図形描画を選択して以下の3つのボタンを作成してください。

  • 「JWTを取得 / Get JWT」
  • 「APIを呼び出し、保護されたリソースを取得 / Call API to fetch protected resource」
  • 「保存された情報をクリア / Clear stored data」

A4・A5セルにJWT情報、A14セルにAPI呼び出し結果を表示します(コードから書き込みます)。

備考

図形描画の詳細はこちらを確認してください。

2. 定数を設定する

注意

このサンプルではAPI_KEYをハードコーディングしています。実際のアプリではスクリプトプロパティやシークレットマネージャーに保存してください。詳しくは本番利用時の注意 > APIキーの保存を参照してください。

const API_KEY = 'YOUR_API_KEY'; // アプリポータルで発行したAPIキー(mf_api_で始まる文字列)
const AUTHENTICATION_SERVER = 'https://api.biz.moneyforward.com';

3. JWTの取得を実装する

APIキーを Authorization ヘッダーに Bearer スキームで指定して /auth/exchange を呼び出します。リクエストボディは不要です。

レスポンスボディの access_token がJWTに相当します。JWTの有効期限は1時間(expires_in: 3600)です。

/auth/exchange にはレート制限があるため、本番アプリではJWTをキャッシュし、期限切れ前に再取得する実装が推奨されます。詳細は APIキーによる認証の仕組みを学ぶ > STEP 2. APIキーをJWTに交換する を参照してください。

function exchangeApiKeyForJwt() {
const sheet = SpreadsheetApp.getActiveSheet();

try {
const response = UrlFetchApp.fetch(
`${AUTHENTICATION_SERVER}/auth/exchange`,
{
method: 'post',
headers: {
Authorization: 'Bearer ' + API_KEY,
},
muteHttpExceptions: true,
},
);

const status = response.getResponseCode();
const bodyText = response.getContentText();

if (status !== 200) {
sheet.getRange('A14').setValue('JWTの取得に失敗しました: ' + bodyText);
SpreadsheetApp.getUi().showModalDialog(
HtmlService.createHtmlOutput(
'JWTの取得に失敗しました。詳細はA14セルを確認してください。',
),
'エラー',
);
return;
}

const token = JSON.parse(bodyText);
const props = PropertiesService.getUserProperties();
props.setProperty('MF_JWT', token.access_token);
props.setProperty(
'MF_JWT_EXPIRES_AT_MS',
String(Date.now() + token.expires_in * 1000),
);

sheet.getRange('A4').setValue('JWT: ' + token.access_token);
sheet
.getRange('A5')
.setValue(
'expires_in: ' +
token.expires_in +
' / token_type: ' +
token.token_type,
);
sheet.getRange('A14').setValue('');

const html = HtmlService.createHtmlOutput('JWTの取得に成功しました。');
SpreadsheetApp.getUi().showModalDialog(html, '成功');
} catch (error) {
console.error('JWTの取得中にエラーが発生しました:', error);
sheet.getRange('A4').setValue('');
sheet.getRange('A5').setValue('');
sheet.getRange('A14').setValue('エラー: ' + error.toString());
SpreadsheetApp.getUi().showModalDialog(
HtmlService.createHtmlOutput(
'JWTの取得中にエラーが発生しました。詳細はA14セルを確認してください。',
),
'エラー',
);
}
}

取得したJWTは後続のAPI呼び出しで使えるようユーザープロパティに保存します。A4・A5セルへの表示は学習用です。

4. API呼び出しを実装する

取得したJWTを Authorization ヘッダーに Bearer スキームで指定し、/v2/tenant/tenant_user から事業者ユーザー情報を取得します。

function fetchTenantUser() {
const props = PropertiesService.getUserProperties();
const jwt = props.getProperty('MF_JWT');
const expiresAtMs = Number(props.getProperty('MF_JWT_EXPIRES_AT_MS') || 0);

if (!jwt || !expiresAtMs || Date.now() >= expiresAtMs) {
const html = HtmlService.createHtmlOutput(
'有効なJWTがありません。先に「JWTを取得」を実行してください。',
);
SpreadsheetApp.getUi().showModalDialog(html, 'JWTが必要です');
return;
}

try {
const response = UrlFetchApp.fetch(
`${AUTHENTICATION_SERVER}/v2/tenant/tenant_user`,
{
method: 'get',
headers: {
Authorization: 'Bearer ' + jwt,
},
muteHttpExceptions: true,
},
);

const status = response.getResponseCode();
const bodyText = response.getContentText();
const sheet = SpreadsheetApp.getActiveSheet();

if (status !== 200) {
sheet.getRange('A14').setValue('API呼び出しに失敗しました: ' + bodyText);
SpreadsheetApp.getUi().showModalDialog(
HtmlService.createHtmlOutput(
'API呼び出しに失敗しました。詳細はA14セルを確認してください。',
),
'エラー',
);
return;
}

const data = JSON.parse(bodyText);
sheet.getRange('A14').setValue(JSON.stringify(data, null, 2));
} catch (error) {
console.error('API呼び出しに失敗しました:', error);
const sheet = SpreadsheetApp.getActiveSheet();
sheet
.getRange('A14')
.setValue('API呼び出しに失敗しました: ' + error.toString());
SpreadsheetApp.getUi().showModalDialog(
HtmlService.createHtmlOutput(
'API呼び出しに失敗しました。詳細はA14セルを確認してください。',
),
'エラー',
);
}
}

ユーザープロパティのJWTと有効期限を確認し、未保存または期限切れの場合はJWT取得を促します。

/v2/tenant/tenant_user は事業者ユーザー情報取得APIです。要件に応じて呼び出すAPIを変更してください。

5. 保存された情報のクリアを実装する

function clearStoredJwt() {
try {
const props = PropertiesService.getUserProperties();
props.deleteProperty('MF_JWT');
props.deleteProperty('MF_JWT_EXPIRES_AT_MS');

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, 'エラー');
}
}

6. 図形に関数を割り当てる

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

  1. 図形を選択します
  2. 図形右端の縦3点メニューをクリックします
  3. スクリプトを割り当てを選択します
  4. 以下の関数名を入力して割り当てます
図形割り当てる関数
① JWTを取得 / Get JWTexchangeApiKeyForJwt
② APIを呼び出し、保護されたリソースを取得 / Call API to fetch protected resourcefetchTenantUser
③ 保存された情報をクリア / Clear stored dataclearStoredJwt
ヒント

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


動作確認

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

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

期待結果

  1. JWTの取得: ①のボタンをクリックします。成功ダイアログが表示され、A4・A5セルにJWT情報が表示されれば成功です。
  2. APIの呼び出し: ②のボタンをクリックします。A14セルにJSON形式のレスポンスが表示されれば成功です。
  3. 保存された情報のクリア: ③のボタンをクリックします。A4・A5・A14セルがクリアされ、再度②のボタンをクリックすると「有効なJWTがありません」と表示されれば成功です。

JWTの有効期限が切れた後は、再度①のボタンをクリックしてください。

よくある失敗

症状原因対処
JWT取得で401やエラーレスポンスAPIキーが誤っている、または無効キーを確認または再発行してAPI_KEYを修正する
API呼び出しで401JWTの期限切れまたは未保存①のボタンをクリックしてJWTを再取得する
API呼び出しで403JWTにない権限でエンドポイントを呼んでいる呼び出すAPIと必要な権限をドキュメントで確認する
JWT取得が頻繁に失敗する/auth/exchange のレート制限JWTをキャッシュして再取得の頻度を抑える
「JWTが必要です」とだけ表示される③の実行後、または期限切れ①を実行してから②を実行する

本番利用時の注意

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

APIキーの保存

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

const API_KEY = PropertiesService.getScriptProperties().getProperty('API_KEY');

JWTのキャッシュとレート制限

/auth/exchange への呼び出しはレート制限の対象です。JWTは有効期限内で再利用し、不要な再交換を避けてください。設計の要点は APIキーによる認証の仕組みを学ぶ > STEP 4. 実装のベストプラクティス を参照してください。

JWTの表示を削除する

exchangeApiKeyForJwt 内でA4・A5セルにJWTを書き込む処理は学習用です。本番では削除してください。JWTがスプレッドシートに残ると、シートを閲覧できる第三者に漏洩するリスクがあります。


完成コード

// コード.gs
const API_KEY = 'YOUR_API_KEY'; // アプリポータルで発行したAPIキー(mf_api_で始まる文字列)
const AUTHENTICATION_SERVER = 'https://api.biz.moneyforward.com';

function exchangeApiKeyForJwt() {
const sheet = SpreadsheetApp.getActiveSheet();

try {
const response = UrlFetchApp.fetch(
`${AUTHENTICATION_SERVER}/auth/exchange`,
{
method: 'post',
headers: {
Authorization: 'Bearer ' + API_KEY,
},
muteHttpExceptions: true,
},
);

const status = response.getResponseCode();
const bodyText = response.getContentText();

if (status !== 200) {
sheet.getRange('A14').setValue('JWTの取得に失敗しました: ' + bodyText);
SpreadsheetApp.getUi().showModalDialog(
HtmlService.createHtmlOutput(
'JWTの取得に失敗しました。詳細はA14セルを確認してください。',
),
'エラー',
);
return;
}

const token = JSON.parse(bodyText);
const props = PropertiesService.getUserProperties();
props.setProperty('MF_JWT', token.access_token);
props.setProperty(
'MF_JWT_EXPIRES_AT_MS',
String(Date.now() + token.expires_in * 1000),
);

sheet.getRange('A4').setValue('JWT: ' + token.access_token);
sheet
.getRange('A5')
.setValue(
'expires_in: ' +
token.expires_in +
' / token_type: ' +
token.token_type,
);
sheet.getRange('A14').setValue('');

const html = HtmlService.createHtmlOutput('JWTの取得に成功しました。');
SpreadsheetApp.getUi().showModalDialog(html, '成功');
} catch (error) {
console.error('JWTの取得中にエラーが発生しました:', error);
sheet.getRange('A4').setValue('');
sheet.getRange('A5').setValue('');
sheet.getRange('A14').setValue('エラー: ' + error.toString());
SpreadsheetApp.getUi().showModalDialog(
HtmlService.createHtmlOutput(
'JWTの取得中にエラーが発生しました。詳細はA14セルを確認してください。',
),
'エラー',
);
}
}

function fetchTenantUser() {
const props = PropertiesService.getUserProperties();
const jwt = props.getProperty('MF_JWT');
const expiresAtMs = Number(props.getProperty('MF_JWT_EXPIRES_AT_MS') || 0);

if (!jwt || !expiresAtMs || Date.now() >= expiresAtMs) {
const html = HtmlService.createHtmlOutput(
'有効なJWTがありません。先に「JWTを取得」を実行してください。',
);
SpreadsheetApp.getUi().showModalDialog(html, 'JWTが必要です');
return;
}

try {
const response = UrlFetchApp.fetch(
`${AUTHENTICATION_SERVER}/v2/tenant/tenant_user`,
{
method: 'get',
headers: {
Authorization: 'Bearer ' + jwt,
},
muteHttpExceptions: true,
},
);

const status = response.getResponseCode();
const bodyText = response.getContentText();
const sheet = SpreadsheetApp.getActiveSheet();

if (status !== 200) {
sheet.getRange('A14').setValue('API呼び出しに失敗しました: ' + bodyText);
SpreadsheetApp.getUi().showModalDialog(
HtmlService.createHtmlOutput(
'API呼び出しに失敗しました。詳細はA14セルを確認してください。',
),
'エラー',
);
return;
}

const data = JSON.parse(bodyText);
sheet.getRange('A14').setValue(JSON.stringify(data, null, 2));
} catch (error) {
console.error('API呼び出しに失敗しました:', error);
const sheet = SpreadsheetApp.getActiveSheet();
sheet
.getRange('A14')
.setValue('API呼び出しに失敗しました: ' + error.toString());
SpreadsheetApp.getUi().showModalDialog(
HtmlService.createHtmlOutput(
'API呼び出しに失敗しました。詳細はA14セルを確認してください。',
),
'エラー',
);
}
}

function clearStoredJwt() {
try {
const props = PropertiesService.getUserProperties();
props.deleteProperty('MF_JWT');
props.deleteProperty('MF_JWT_EXPIRES_AT_MS');

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, 'エラー');
}
}