GA4 API を自作スクリプトで叩く — OAuth Desktop Client + Refresh Token 永続化の実録
MCP が access_token しかくれなかったので、Refresh Token を自前で管理することにした話
※この記事は実際の作業メモを元に、AIが記述してます.信憑性は個人ブログ相当と思ってください.ごめんね
1. 背景
zeta3.net は Cloudflare Pages でホストしてる静的ブログで、GA4 でアクセス解析してる. んで、データ取る方法として、最初は Stape って MCP サーバーが提供する GA4 ツール使ってた.
ところが、Stape の GA4 MCP が発行する OAuth トークン、こんな制約があった:
| 項目 | 値 |
|---|---|
| Access Token | ある(ya29. で始まる) |
| Refresh Token | ない |
| 有効期限 | 1時間(expires_in=3600) |
| トークン保存場所 | ~/.mcp-auth/ 以下 |
1時間ごとに再 OAuth が必要で、当然自動化できねーじゃんって話. Stape の有料プランに課金すれば refresh_token 対応してくれるらしいけど、そんなにお金かけることでもない.
「なら自分で OAuth Client 作って refresh_token 永続化すればいいじゃん」——ここが出発点.
2. 選択肢と意思決定
選択肢は2つ:
| 選択肢 | コスト | refresh_token | 持続性 |
|---|---|---|---|
| Stape 有料プラン | 課金 | たぶんもらえる | ベンダーロック |
| カスタム GCP OAuth Client | 無料 | 確実にもらえる | 完全自前 |
後者一択.GCP の Desktop OAuth Client 作成して、OAuth 2.0 authorization code フローを Playwright で自動化、refresh_token 取得して .ga4-credentials/ に永続化する.
3. セットアップ手順
3.1 GCP OAuth Client の作成
- GCP Console → APIs & Services → Credentials
- OAuth consent screen: External で作成、必要な scope は
analytics.readonlyのみ - Test user に自分の Google アカウント追加
- Credentials → Create Credentials → OAuth client ID → Desktop app 選択
- 発行された
client_secret.jsonダウンロード
ここでアプリケーションタイプ選びが重要.Web application を選ぶと redirect URI の登録が必要で、ローカルスクリプトから扱いにくい.Desktop app なら urn:ietf:wg:oauth:2.0:oob(out-of-band)って特殊な redirect URI が使えて、認証コードをブラウザに表示 → 手動コピーできる.
※OOB は将来的に非推奨になるらしいけど、2026年現在まだ使える.代わりに http://127.0.0.1:PORT の localhost redirect URI 使う手もある.
3.2 Playwright で OAuth フローを自動化
認証コードを手動コピーするのはだるいので、ブラウザ自動操作で authorization code 取得する.
const { chromium } = require('playwright');
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
const authUrl = 'https://accounts.google.com/o/oauth2/auth'
+ '?client_id=YOUR_CLIENT_ID'
+ '&redirect_uri=urn:ietf:wg:oauth:2.0:oob'
+ '&scope=https://www.googleapis.com/auth/analytics.readonly'
+ '&access_type=offline'
+ '&response_type=code';
await page.goto(authUrl);
// ここで手動ログイン or 既存セッション使う
const authCode = await page.locator('input[type="text"]').inputValue();
ポイント:
access_type=offline指定で refresh_token が発行される(デフォだと access_token しか返らん)headless: falseでブラウザ表示、初回のみ手動ログイン必要
3.3 トークン交換
取得した authorization code を Google の token endpoint に POST する:
curl -s -X POST "https://oauth2.googleapis.com/token" \
-d "code=$AUTH_CODE" \
-d "client_id=$CLIENT_ID" \
-d "client_secret=$CLIENT_SECRET" \
-d "redirect_uri=urn:ietf:wg:oauth:2.0:oob" \
-d "grant_type=authorization_code"
レスポンス:
{
"access_token": "ya29.a0...",
"expires_in": 3599,
"refresh_token": "1//0gABCDEF...",
"scope": "https://www.googleapis.com/auth/analytics.readonly",
"token_type": "Bearer"
}
ここで初めて refresh_token が取れる.これを .ga4-credentials/tokens.json に保存する.
3.4 認証情報の永続化
tokens.json の構造:
{
"client_id": "xxx.apps.googleusercontent.com",
"client_secret": "GOCSPX-...",
"refresh_token": "1//0gABCDEF...",
"ga4_property_id": "properties/538003166"
}
client_secret.json と tokens.json を .ga4-credentials/ に保存.このディレクトリは .gitignore に追加して誤公開防止.
4. 完成したスクリプト
ga4.sh — 50行、依存は curl + jq のみ:
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
CRED_DIR="$SCRIPT_DIR/.ga4-credentials"
CLIENT_ID=$(jq -r '.client_id' "$CRED_DIR/tokens.json")
CLIENT_SECRET=$(jq -r '.client_secret' "$CRED_DIR/tokens.json")
REFRESH_TOKEN=$(jq -r '.refresh_token' "$CRED_DIR/tokens.json")
PROPERTY=$(jq -r '.ga4_property_id' "$CRED_DIR/tokens.json")
TOKEN_RESPONSE=$(curl -s -X POST "https://oauth2.googleapis.com/token" \
-d "client_id=$CLIENT_ID" \
-d "client_secret=$CLIENT_SECRET" \
-d "refresh_token=$REFRESH_TOKEN" \
-d "grant_type=refresh_token")
ACCESS_TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r '.access_token')
curl -s -X POST "https://analyticsdata.googleapis.com/v1beta/$PROPERTY:runRealtimeReport" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"dimensions":[{"name":"country"}],
"metrics":[{"name":"activeUsers"},{"name":"eventCount"}],
"minuteRanges":[{"startMinutesAgo":10}]
}' | jq .
使い方:
./ga4.sh # レポート取得
./ga4.sh --access-token # アクセストークンのみ
5. OAuth 2.0 Refresh Token フローの解説
やっていることは OAuth 2.0 の Refresh Token Grant そのもの.
sequenceDiagram
participant Script as ga4.sh
participant Google as Google OAuth2
participant GA4 as GA4 Data API
Script->>Google: POST /token (grant_type=refresh_token)
Note over Script,Google: client_id + client_secret + refresh_token
Google-->>Script: { access_token: "ya29...", expires_in: 3599 }
Script->>GA4: GET /v1beta/properties/XXX:runRealtimeReport
Note over Script,GA4: Authorization: Bearer ya29...
GA4-->>Script: { rows: [...], totals: [...] }
重要ポイント:
- refresh_token: 一度取得すれば Google が失効させない限り半永続的.
access_type=offline指定の authorization code フローでのみ発行 - access_token: 1時間で死ぬ.refresh_token で都度取得
- grant_type=refresh_token: 認可コードフローを通らずに新しい access_token を得られる
- Desktop OAuth Client: ローカルスクリプト用途に最適.oob redirect URI が使える
6. ハマったポイント
gcloud ADC は使えなかった
gcloud auth application-default login \
--client-id-file=.ga4-credentials/client_secret.json \
--scopes=analytics.readonly
認証は通るんだけど、GA4 API の呼び出しで権限エラーになる.
ADC(Application Default Credentials)は cloud-platform scope が必要で、これは過剰すぎる.
結果的に使えなかった.
refresh_token は初回のみ
authorization code フローで refresh_token が返ってくるのは初回の同意時のみ. 2回目以降の認可では返ってこないから、一度取ったら大事に保管する.
失効した場合の復旧手順:
- Google Account → セキュリティ → 「サードパーティアプリとサービス」
- 該当アプリを削除
- 再度 OAuth フローを通す
7. 得られた知見
- MCP が access_token だけ渡してくるケースは意外とある.OAuth Client 自体はユーザーが GCP で管理してるのに、MCP サーバー側で意図的に refresh_token 捨ててる可能性が高い.トークン管理の複雑さを避けたいんだろうな.
- 自前 OAuth Client + refresh_token 永続化、たかだか50行のシェルスクリプトで実現できる.ベンダー依存避けたいなら良い選択肢.
- GCP OAuth Client のタイプ選びで後悔するパターンあるある.最初に Web application 選んで redirect URI でハマるぐらいなら Desktop App 一択.
- Playwright によるブラウザ自動操作、OAuth みたいな人間介護必須フローの自動化に強力.初回ログインだけ人間が頑張ればあとは自動.
8. おまけ: リフレッシュトークンのテスト
refresh_token が本当に永続的かどうか、簡単に試せる:
# 1時間待つ必要なし — すぐに新しい access_token を何度でも取れる
for i in 1 2 3; do
./ga4.sh --access-token
sleep 2
done
全部同じ refresh_token から新しい access_token が発行されるのを確認できる. GA4 API の rate limit には注意.