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
Google 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環境で動かせるようになります。

セキュリティ上の注意

⚠️ このアプリはシークレットキーをスプレッドシートに保存します。以下の点に注意してください:

  1. スプレッドシートの共有設定を適切に管理

  2. Webアプリの公開範囲の設定

  3. URLは秘密にする

  4. 本番環境での使用は自己責任で

セットアップ方法

このアプリを実際に動かしてみたい方は、GitHubリポジトリのREADMEをご覧ください:

📦 リポジトリ: https://github.com/hibipo-ro/gas-totp

以下の手順で簡単にセットアップできます:

  1. リポジトリをクローン
  2. npm install & npm run build
  3. Google スプレッドシートを作成
  4. GASにデプロイ(clasp または手動)

詳細な手順とトラブルシューティングはREADMEに記載しています。


参考リンク

Discussion