GCPでOAuthクライアントを設定する際に少々戸惑ったのでまとめます。

フロー

  1. GCPでOAuthクライアントを準備する
    1. Google Auth Platform/クライアント
    2. クライアントを作成
    3. 諸々設定後、jsonをダウンロードし、プロジェクトのrootにcredentials.jsonとして配置
  2. npm run google:get-refresh-tokenを実行
    • スクリプトが認可URLを生成して表示する
  3. ブラウザでURLを開き、GoogleログインしてClassroom読み取り権限を許可する
  4. 認可後に出たauthorization codeをターミナルに貼り付ける
  5. レスポンスからrefresh_tokenを取得
  6. 開発環境なら.dev.versなどにGOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_REFRESH_TOKENを入力し、保存

get-refresh-token.mjs

import fs from 'node:fs/promises';
import readline from 'node:readline/promises';
import { stdin as input, stdout as output } from 'node:process';
import { google } from 'googleapis';

const SCOPES = ['https://www.googleapis.com/auth/classroom.courses.readonly'];  // 使用するAPIに合わせて
const CREDENTIALS_PATH = process.env.GOOGLE_CREDENTIALS_PATH ?? './credentials.json';
const REDIRECT_URI = process.env.GOOGLE_REDIRECT_URI ?? 'urn:ietf:wg:oauth:2.0:oob';

async function readCredentials() {
  const raw = await fs.readFile(CREDENTIALS_PATH, 'utf8');
  const json = JSON.parse(raw);
  const config = json.installed ?? json.web;

  if (!config?.client_id || !config?.client_secret) {
    throw new Error(
      `Invalid credentials format in ${CREDENTIALS_PATH}. Expected web/installed client_id and client_secret.`,
    );
  }

  return {
    clientId: config.client_id,
    clientSecret: config.client_secret,
  };
}

async function main() {
  const { clientId, clientSecret } = await readCredentials();
  const oauth2Client = new google.auth.OAuth2(clientId, clientSecret, REDIRECT_URI);

  const authUrl = oauth2Client.generateAuthUrl({
    access_type: 'offline',
    scope: SCOPES,
    prompt: 'consent',
  });

  console.log('\n1) Open this URL in your browser and allow access:\n');
  console.log(authUrl);

  const rl = readline.createInterface({ input, output });
  const code = await rl.question('\n2) Paste the authorization code here: ');
  rl.close();

  const { tokens } = await oauth2Client.getToken(code.trim());

  if (!tokens.refresh_token) {
    throw new Error(
      'No refresh_token returned. Re-run and ensure prompt=consent, then approve requested scopes.',
    );
  }

  console.log('\nRefresh token (set as GOOGLE_REFRESH_TOKEN):\n');
  console.log(tokens.refresh_token);

  if (tokens.access_token) {
    console.log('\nAccess token was also issued (short-lived):\n');
    console.log(tokens.access_token);
  }
}

main().catch((error) => {
  console.error('\nFailed to get refresh token:');
  console.error(error instanceof Error ? error.message : error);
  process.exitCode = 1;
});

所感(雑)

GoogleのAPIは結構多用することになりそうなので身につけたいと思いました