⏰
Google Apps Script(GAS)でTOTP認証を作ってみた
背景
- 2要素認証が必須化され、アカウント共有のために作成しました。
作ったもの

GASで動作するTOTP認証アプリのデモ画面
主な機能:
- 📱 複数のサービスのTOTP認証コードを一括表示
- ⏱️ 30秒ごとに自動更新される認証コード
- 📋 ワンクリックでコードをコピー
- 📊 残り時間の視覚的表示
- ➕ QRコード画像から新規サービスを追加
技術スタック
- Google Apps Script - サーバーサイド
- Google Spreadsheet - シークレットキーの保存
- authenticator (npm) - TOTP認証コード生成ライブラリ
- jsQR (CDN) - QRコード読み取り
- Webpack + Polyfill - Node.jsライブラリをGASで動かす
実装のポイント
1. スプレッドシートからシークレットキーを読み込む
TOTPのシークレットキーをスプレッドシートで管理します。
シート名は secrets とし、以下の形式で保存
| service | account | secret |
|---|---|---|
| user@example.com | <SECRET_KEY> | |
| GitHub | username | <SECRET_KEY> |
function loadSecretsFromSheet() {
const scriptProperties = PropertiesService.getScriptProperties();
const spreadsheetId = scriptProperties.getProperty('SPREADSHEET_ID');
const spreadsheet = SpreadsheetApp.openById(spreadsheetId);
const sheet = spreadsheet.getSheetByName('secrets');
const data = sheet.getDataRange().getValues();
const secrets = [];
for (let i = 1; i < data.length; i++) { // ヘッダー行をスキップ
const row = data[i];
if (row[0] && row[1] && row[2]) {
secrets.push({
service: row[0],
account: row[1],
secret: row[2]
});
}
}
return secrets;
}
- スプレッドシートIDはスクリプトプロパティに保存(ハードコードを避ける)
2. TOTPコードの生成
authenticatorパッケージを使用します。このパッケージは内部でcryptoモジュールを使うため、そのままではGASで動きません。
解決策: WebpackとPolyfillを使って、cryptoモジュールをバンドル時に含めることで、GAS環境でも動作するようにします(詳細は次のセクションで説明)。
const authenticator = require('authenticator');
function generateTOTP(secret) {
return authenticator.generateToken(secret);
}
function generateAllTokens() {
const secrets = loadSecretsFromSheet();
return secrets.map(item => {
const token = generateTOTP(item.secret);
return {
service: item.service,
account: item.account,
token: token,
remainingSeconds: 30 - (Math.floor(Date.now() / 1000) % 30)
};
});
}
残り秒数は Date.now() を使って計算し、クライアント側でカウントダウン表示します。
3. WebpackでNode.jsライブラリをバンドル
前のセクションで触れた通り、GASはcryptoモジュールをサポートしていません。この問題を解決するために、Webpackとnode-polyfill-webpack-pluginを使います。
webpack.config.js:
const path = require('path');
const webpack = require('webpack');
const GasPlugin = require('gas-webpack-plugin');
const NodePolyfillPlugin = require('node-polyfill-webpack-plugin');
module.exports = {
mode: 'production',
entry: './src/index.js',
output: {
filename: 'Code.js',
path: path.resolve(__dirname, 'dist'),
globalObject: 'this'
},
plugins: [
new GasPlugin(),
new NodePolyfillPlugin(),
new webpack.DefinePlugin({
'process.env': JSON.stringify({}),
'process.version': JSON.stringify(''),
'process.browser': JSON.stringify(true)
})
],
resolve: {
fallback: {
"process": require.resolve("process/browser")
}
}
};
ポイント:
-
NodePolyfillPluginがNode.jsのcryptoモジュールをブラウザ互換のコードに置き換え -
GasPluginがGASで必要なglobal.doGetなどのエクスポートを自動生成 - ビルドすると、約1000行の
dist/Code.jsが生成される(Polyfillを含むため大きくなる)
この仕組みにより、Node.js専用のライブラリをGAS環境で動かせるようになります。
セキュリティ上の注意
⚠️ このアプリはシークレットキーをスプレッドシートに保存します。以下の点に注意してください:
-
スプレッドシートの共有設定を適切に管理
-
Webアプリの公開範囲の設定
-
URLは秘密にする
-
本番環境での使用は自己責任で
セットアップ方法
このアプリを実際に動かしてみたい方は、GitHubリポジトリのREADMEをご覧ください:
📦 リポジトリ: https://github.com/hibipo-ro/gas-totp
以下の手順で簡単にセットアップできます:
- リポジトリをクローン
-
npm install&npm run build - Google スプレッドシートを作成
- GASにデプロイ(clasp または手動)
詳細な手順とトラブルシューティングはREADMEに記載しています。
Discussion