GA4 API の refresh_token が7日で死ぬ — invalid_grant の正体
cron で回していた GA4 取得が一週間ごとに invalid_grant で死ぬ人へ、原因と恒久対策を先に置きます

結論を3行で先に置きます。
- GA4 Data API の自動取得が「一週間ごとに
invalid_grantで止まる」なら、ほぼ確実に OAuth 同意画面が「テスト」公開ステータスのままです。テストステータスで発行された refresh_token は7日で失効します。 - その場しのぎは「再認証して新しい refresh_token を取り直す」。ただし7日後にまた死にます。私はこれを3回繰り返しました。
- 恒久対策は2択。(A) 同意画面を「本番(In production)」に切り替えるか、(B) そもそもユーザー OAuth をやめてサービスアカウントで GA4 にアクセスする。サーバー間の自動取得なら (B) が本命です。
以下、症状の出方、なぜ7日なのか、3つの対策の比較、そして自分が最終的にどう直したかを時系列で残します。同じところで再認証ループにハマっている人の時間を節約できれば。
症状 — 一週間は完璧に動く、そして突然止まる
私の構成はよくあるやつです。GA4 のプロパティを6サイトぶん、毎朝 cron で fetch-ga4-data.mjs を回して、活発ユーザー・セッション・チャネル・上位ページを JSON スナップショットに落とす。動き始めの一週間は何の問題もなく回ります。グラフも綺麗に伸びる。
ところがある朝、ログを見ると全サイトのフェッチがこう吐いて死んでいました。
Error: invalid_grant
at OAuth2Client.refreshTokenNoCache (.../google-auth-library/.../oauth2client.js)
at async runReport (.../fetch-ga4-data.mjs)
たちが悪いのは、「動いていたものが、コードを一切触っていないのに突然死ぬ」点です。最初はサーバーの時刻ずれ、次にスコープ、次にプロパティ権限を疑いました。全部ハズレです。invalid_grant は「リフレッシュトークンそのものが無効になった」というサインで、コードの問題ではありません。
なぜ7日なのか — 「テスト」公開ステータスの罠
Google Cloud の OAuth 同意画面には公開ステータスが3つあります。テスト(Testing) / 本番(In production)。多くの人は開発中にプロジェクトを作って、同意画面を「テスト」のまま放置します。自分専用のスクリプトだと、テストユーザーに自分のアカウントを足せば普通に動いてしまうので、本番に切り替える理由がないからです。
ところが Google の仕様で、公開ステータスが「テスト」のアプリが発行した refresh_token は、発行から7日で必ず失効します。これはドキュメントに明記されている挙動で、バグでも障害でもありません。だから「動き始めてちょうど一週間で死ぬ」という、妙に規則正しい壊れ方をするわけです。
ここに気づくのに時間がかかったのは、エラーメッセージ invalid_grant がいかにも「コードか権限の問題」に見えるからです。実際にはトークンのライフサイクルの問題で、コードは1行も悪くありません。
対策A — 同意画面を「本番」に切り替える
一番シンプルな恒久対策は、Google Cloud Console の「OAuth 同意画面」で公開ステータスを「本番(In production)」に切り替えることです。本番ステータスで発行された refresh_token は、明示的に失効させない限り(または6ヶ月間まったく使われない場合を除いて)失効しません。つまり7日縛りが消えます。
注意点が一つあります。GA4 の analytics.readonly は Google が「機微 (sensitive)」に分類するスコープです。これを使うアプリを本番にすると、一般公開する場合は審査(verification)が必要になります。ただし「自分のアカウントだけがアクセスする内部利用」であれば、審査を通さなくても本番ステータスで自分は使い続けられます(ログイン時に「確認されていないアプリ」の警告は出ますが、自分で許可すれば通る)。社内・個人運用ならこれで十分です。
対策B(本命)— ユーザー OAuth をやめてサービスアカウントにする
そもそも論として、サーバーが無人で GA4 を叩くだけなら、ユーザー OAuth(refresh_token 方式)は設計として向いていません。ユーザー OAuth は「人間がブラウザでログインして許可する」ことが前提のフローで、無人運用ではトークンの失効・再認証という弱点を常に抱え込みます。
サーバー間アクセスの正解はサービスアカウントです。手順はこうです。
- Google Cloud でサービスアカウントを作成し、JSON キーを発行する。
- そのサービスアカウントのメールアドレス(
xxxx@yyyy.iam.gserviceaccount.com)を、GA4 管理画面 → プロパティのアクセス管理で「閲覧者」として追加する。 - コードはユーザー OAuth ではなくサービスアカウントの JSON キーで認証する。
@google-analytics/dataのBetaAnalyticsDataClientにkeyFilenameを渡すだけ。
これだと refresh_token は登場せず、ブラウザログインも不要、7日失効もありません。一度組めば無人で回り続けます。最初からこうしておけば、私は再認証を3回もやらずに済みました。「自動化スクリプトの認証は、人間のログイン方式を流用してはいけない」という、後から見れば当たり前の教訓です。
その場しのぎの再認証フローも一応残す
とはいえ「今すぐ数字が見たい、サービスアカウント移行は後で」という状況もあります。その場合のその場しのぎが、prompt: 'consent' と access_type: 'offline' を付けて再度ブラウザ認証し、新しい refresh_token を取り直してファイルに保存する、というものです。
const authUrl = oauth2.generateAuthUrl({
access_type: 'offline',
prompt: 'consent', // これがないと refresh_token が返らないことがある
scope: ['https://www.googleapis.com/auth/analytics.readonly'],
});
prompt: 'consent' を付けないと、2回目以降の認証で refresh_token が undefined で返ってきて「保存したのに動かない」という二次災害が起きます。ここも一度踏みました。ただ繰り返しになりますが、これは7日後にまた死にます。週次の再認証を運用に組み込むくらいなら、対策A か B に倒すべきです。
まとめ
GA4 Data API が一週間ごとに invalid_grant で止まるのは、コードの不具合ではなく「テスト」公開ステータスの7日トークン失効が原因です。個人・内部運用なら同意画面を本番に切り替えるだけで止まります。新しく組むなら、最初からサービスアカウントにしておくのが、再認証地獄から永久に抜ける唯一の正解でした。
よくある質問
invalid_grant が出たらコードのどこを直せばいいですか?
まずコードは疑わなくて大丈夫です。invalid_grant は「リフレッシュトークンが無効になった」というサインで、スコープやプロパティ ID の誤りとは別物です。動いていたものが突然止まり、しかも約7日周期なら、OAuth 同意画面が「テスト」公開ステータスのままでトークンが失効している可能性がほぼ確実です。Google Cloud Console の OAuth 同意画面の公開ステータスを確認してください。
同意画面を本番にすると審査(verification)は必須ですか?
analytics.readonly は機微スコープなので、一般公開して不特定多数に使わせる場合は審査が必要です。ただし自分のアカウントだけがアクセスする内部利用なら、審査を通さなくても本番ステータスで使い続けられます。ログイン時に「確認されていないアプリ」の警告が出ますが、自分で許可すれば通ります。
サービスアカウントとユーザー OAuth はどちらを使うべきですか?
サーバーが無人で GA4 を取得するだけならサービスアカウント一択です。サービスアカウントのメールを GA4 プロパティに閲覧者として追加すれば、refresh_token もブラウザログインも不要になり、7日失効問題が構造的に消えます。ユーザー OAuth は人間がブラウザで許可する前提のフローなので、無人運用には向きません。
再認証しても refresh_token が取れず undefined になります。
認証 URL を生成するときに access_type: "offline" と prompt: "consent" の両方を付けてください。prompt: "consent" がないと、2回目以降の認証では refresh_token が返らず undefined になります。ただしテストステータスのままだと、取り直しても7日後にまた失効する点は変わりません。恒久対策は本番化かサービスアカウント移行です。