invalid_grant: Bad Request が返ったら、まずトークンファイルの更新時刻を見る──リフレッシュトークンが少なくとも29日間死んでいた記録
YouTube Data APIのリフレッシュトークンが失効し、呼び出しが `invalid_grant: Bad Request` しか返さない状態が続いた。手元に残っているClaude Codeのトランスクリプトを数えると、2026年7月16日02時37分(日本時間)から8月14日16時07分までのYouTube統計ツール呼び出し74件が全部このエラーで、成功は1件も無い。Googleの公式ドキュメントが挙げる失効理由を1つずつ潰したが、確実に除外できたのは「6か月未使用」「Gmailスコープ+パスワード変更」「管理者によるRestricted」の3つだけで、原因は特定できていない。
目次
Googleのリフレッシュトークンが死ぬと、APIはinvalid_grant: Bad Requestしか返さない。このエラーは原因を1つも教えてくれないので、公式ドキュメントが挙げる失効理由を手元の証拠で1つずつ潰していくしかない。そしてその証拠のうち最も強いのが、トークンを保存しているファイルの更新時刻である。google-authを使う典型的なコードは「リフレッシュに成功したときだけ」トークンファイルを書き戻すので、ファイルのmtimeが「最後にリフレッシュが通った瞬間」を指す。直し方はGoogle公式が書いているとおりブラウザでの再同意のみで、コード側でできることは無い。
invalid_grant: Bad Requestは失効理由を区別しない。公式がリストで挙げる理由は7つ、それとは別に「同意画面がTestingなら7日で失効」「1アカウント×1クライアントIDあたり100本の上限」という条件もあり、エラー本文からはどれなのか分からない- トークンファイルのmtimeが「最後に成功した時刻」になる。 当方の
token_ai_jitan.pickleは作成が2026年5月8日19時13分52秒、最終更新が5月15日14時02分47秒(日本時間)で止まっていた。リフレッシュ成功時にしか書き戻さないコードなので、この時刻より後は一度も通っていない- 公式の失効理由のうち、当方の観測で確実に除外できたのは「6か月未使用」「Gmailスコープ込みでパスワード変更」「管理者がRestrictedに設定(その場合のエラーコードは
admin_policy_enforcedだと公式が明記している)」の3つだけ。残りは手元の情報では判定できず、原因は特定できていない
症状:返ってくるのはこの1行だけ
MCPツール経由でYouTube Data APIを叩いたときのレスポンス本文をそのまま貼る。2026年8月14日16時07分(日本時間)に、この記事のために自分で再実行して同じものが返ることを確認している。
{
"error": "('invalid_grant: Bad Request', {'error': 'invalid_grant', 'error_description': 'Bad Request'})"
}
error_descriptionがBad Requestである。何が悪いのかは1文字も書かれていない。 リクエストの組み立てが悪いのか、トークンが死んだのか、クライアントIDが違うのか、この文字列からは判別できない。
Pythonのgoogle-authを使っている場合、この文字列はRefreshErrorの中身である。呼び出し側でtry/exceptしていないと、スタックトレースの一番下にこれが出る。
原因の候補:公式が挙げているのは「7つ+3つ」
Google公式の「Using OAuth 2.0 to Access Google APIs」(最終更新2026年5月26日UTC・2026年8月14日に本文取得)の"Refresh token expiration"節が、リフレッシュトークンが止まる理由を列挙している。原文をそのまま引く。
You must write your code to anticipate the possibility that a granted refresh token might no longer work. A refresh token might stop working for one of these reasons:
- The user has revoked your app's access.
- The refresh token has not been used for six months.
- The user changed passwords and the refresh token contains Gmail scopes.
- The user account has exceeded a maximum number of granted (live) refresh tokens.
- The user granted time-based access to your app and the access expired.
- If an admin set any of the services requested in your app's scopes to Restricted (the error is admin_policy_enforced).
- For Google Cloud Platform APIs - the session length set by the admin could have been exceeded.
(付与済みのリフレッシュトークンがもう動かない可能性を織り込んでコードを書かなければならない。リフレッシュトークンは次のいずれかの理由で動かなくなることがある:ユーザーがあなたのアプリのアクセスを取り消した/リフレッシュトークンが6か月間使われていない/ユーザーがパスワードを変更し、かつそのリフレッシュトークンがGmailスコープを含んでいる/ユーザーアカウントが付与済み(有効)リフレッシュトークンの上限数を超えた/ユーザーが時間制限付きアクセスを許可し、その期限が切れた/管理者がアプリのスコープに含まれるサービスをRestrictedに設定した(この場合のエラーはadmin_policy_enforced)/Google Cloud Platform APIの場合、管理者が設定したセッション長を超えた可能性がある)
同じ節は続けて、リストとは別枠で2つの条件を書いている。個人開発でハマるのはたいていこの別枠のほうである。
A Google Cloud Platform project with an OAuth consent screen configured for an external user type and a publishing status of "Testing" is issued a refresh token expiring in 7 days, unless the only OAuth scopes requested are a subset of name, email address, and user profile (through the userinfo.email, userinfo.profile, openid scopes, or their OpenID Connect equivalents).
(OAuth同意画面のユーザータイプが「外部」で公開ステータスが「テスト」のGoogle Cloud Platformプロジェクトには、7日で失効するリフレッシュトークンが発行される。ただし要求するOAuthスコープが名前・メールアドレス・ユーザープロフィールの部分集合だけの場合を除く)
There is currently a limit of 100 refresh tokens per Google Account per OAuth 2.0 client ID. If the limit is reached, creating a new refresh token automatically invalidates the oldest refresh token without warning. (...)
(現在、Googleアカウント1つ・OAuth 2.0クライアントID 1つあたり100個のリフレッシュトークンという上限がある。上限に達すると、新しいリフレッシュトークンを作った時点で最も古いリフレッシュトークンが警告なしに無効化される)
そしてクライアントそのものが消えるルールが、Google Cloudのヘルプ「Manage OAuth Clients」(2026年8月14日確認)にある。
Unused Client Deletion
OAuth 2.0 clients that have been inactive for six months are automatically deleted. (...) An OAuth 2.0 client is considered unused if neither of the following actions have occurred within the past six months:
- The client has not been used for any credential or token request via the Google OAuth2.0 endpoint.
- The client's settings have not been modified programmatically or manually within the Google Cloud Console. (...)
You will receive an email notification 30 days before an inactive client is scheduled for deletion. (...)
(未使用クライアントの削除:6か月間非アクティブなOAuth 2.0クライアントは自動的に削除される。(中略)過去6か月間に次のどちらも起きていないクライアントは未使用とみなされる:Google OAuth 2.0エンドポイント経由での認証情報またはトークンのリクエストに使われていない/Google Cloud Consoleでプログラム的または手動で設定が変更されていない。非アクティブなクライアントの削除予定日の30日前にメール通知が届く)
ただしクライアントが消えていた場合のエラーコードはinvalid_grantではない。「Using OAuth 2.0 for Web Server Applications」(最終更新2026年8月7日UTC・2026年8月14日に本文取得)のエラー一覧はこう書き分けている。
deleted_client The OAuth client being used to make the request has been deleted. Deletion can happen manually or automatically in the case of unused clients. Deleted clients can be restored within 30 days of the deletion.
invalid_grant When refreshing an access token or using incremental authorization, the token may have expired or has been invalidated. Authenticate the user again and ask for user consent to obtain new tokens. If you are continuing to see this error, ensure that your application has been configured correctly and that you are using the correct tokens and parameters in your request. Otherwise, the user account may have been deleted or disabled.
(deleted_client:リクエストに使われているOAuthクライアントが削除された。削除は手動でも、未使用クライアントの場合は自動でも起こりうる。削除されたクライアントは削除から30日以内なら復元できる/invalid_grant:アクセストークンのリフレッシュ時、または増分認可の使用時に、トークンが期限切れか無効化された可能性がある。ユーザーを再認証し、新しいトークンを得るために同意を求めること。このエラーが続く場合は、アプリケーションの設定が正しいこと、正しいトークンとパラメータを使っていることを確認すること。そうでなければ、ユーザーアカウントが削除または無効化された可能性がある)
公式がinvalid_grantに対して指示している対処は「ユーザーを再認証して同意を取り直せ」だけである。 コード側のリトライで直る種類のエラーではない。
潰し方:トークンファイルのmtimeが証拠になる
ここからが実際に効いた手順である。当方のMCPサーバー(script_mcp_server_v3.py)のトークン読み込み部分は、google-authのサンプルによくある形をしている。
with open(YT_TOKEN_PATH, "rb") as f:
creds = pickle.load(f)
if creds and not creds.valid and creds.expired and creds.refresh_token:
creds.refresh(Request())
with open(YT_TOKEN_PATH, "wb") as f:
pickle.dump(creds, f)
pickle.dumpはcreds.refresh(Request())が例外を投げずに返ったときにしか実行されない。 リフレッシュがinvalid_grantで落ちればファイルは書き換わらない。つまりファイルのmtimeは「最後にリフレッシュが通った時刻」で凍る。アクセストークンの寿命は1時間なので、その後に一度でも成功していればmtimeは動いていたはずである。
当方の実ファイルは次の状態だった(statで取得、日本時間)。
| 項目 | 値 |
|---|---|
| パス | ~/Desktop/token_ai_jitan.pickle |
| サイズ | 1,709バイト |
| 作成(birth) | 2026-05-08 19:13:52 |
| 最終更新(modify) | 2026-05-15 14:02:47 |
作成から最終更新までが6日18時間48分55秒、つまり7日にわずかに届かない。そして最終更新から先は一度も書き変わっていない。
ここから潰せたのが次の4点である。
「6か月未使用」は成立しない。 トークンファイルが存在し始めたのが2026年5月8日で、後述のとおり失敗を確認しているのが7月16日である。69日しか経っていない。 6か月には遠い。同じ理由で「クライアントが6か月非アクティブで自動削除」も成立しない(そもそもその場合のエラーはdeleted_clientである)。
「パスワード変更+Gmailスコープ」も成立しない。 このトークンを作る側のスクリプトが要求しているスコープはyoutube / youtube.force-ssl / yt-analytics.readonly / yt-analytics-monetary.readonlyで、再認証用に用意したスクリプトはyoutube.readonly / yt-analytics.readonlyである。Gmailスコープは1つも入っていない。
「管理者がRestrictedに設定」も、公式の記述で除外できる。 その場合のエラーコードはadmin_policy_enforcedだと公式が明記している。返ってきたのはinvalid_grantである。
「OAuthクライアントそのものが死んでいる」わけでもない。 同じcredentials.json(=同じクライアントID)を使う別のスクリプト(youtube_add_article_links.py・スコープはyoutube.force-ssl)で、2026年8月14日15時13分25秒にブラウザ同意を通した新しいトークンがtoken_ai_jitan_write.pickleとして作られている(birth・modifyともに同時刻、1,380バイト、パーミッション600)。同じ分にyoutube_description_backup_2026-08-14.json(48,084バイト・動画16本分のタイトルと概要欄)が書き出されているので、その時刻にYouTube Data APIは実データを返している。 死んでいるのはtoken_ai_jitan.pickleに保存されたリフレッシュトークンのほうであって、APIでもクライアントIDでもない。ただしこのトークンをどのGoogleアカウントで同意したかは確認していない(pickleを開いていないため)。叩いたのはvideos().list(id=...)=公開動画の読み取りなので、チャンネル所有アカウントであることの証明にはならない。
残ったのは、公式リストの4つ——「ユーザーが取り消した」「付与済み(有効)リフレッシュトークンの上限数を超えた」「時間制限付きアクセスの期限切れ」「GCP APIで管理者が設定したセッション長を超えた」——に、別枠の2条件「同意画面がTestingで7日失効」「1アカウント×1クライアントIDあたり100本上限で最古が押し出された」を足した6つである。このどれなのかは手元の情報では決められなかった。
対処:再認証しか無い(そして人間が要る)
公式の指示どおり、やることは「ブラウザで同意を取り直す」だけである。当方は再認証専用のスクリプトを1本用意した(~/Desktop/youtube_reauth.py・2026年8月14日10時52分37秒作成・3,405バイト)。骨格は次のとおり。
credentials.jsonの存在を確認する- 古いトークンを削除せず
.bak_20260814にリネームして退避する(新しい認証が失敗したときに戻せるようにするため) InstalledAppFlow.from_client_secrets_file(...)→flow.run_local_server(port=0)でブラウザを開き、同意を取る- 取れた資格情報をpickleで保存し、
os.chmod(..., 0o600)で権限を絞る - 保存して終わりにせず、その場で
channels().list(mine=True)を叩いて実データが返るか確認する
5番目を入れているのは、「保存できた=使える」ではないからである。認証は通ったのにチャンネルが取れないケース(アカウント違い・スコープ不足)を、その場で落として気づけるようにしている。
そしてこの手順は自動化できない。 run_local_serverはブラウザでのGoogleアカウント同意を要求するので、エージェントやcronから無人で走らせられない。リフレッシュトークンが死んだ瞬間に、その系は「人間が座らないと復旧しない」状態になる。 気づいてから人間が座るまでの時間が、そのまま停止時間になる。
観測条件:いつ・どの環境で・何回起きたか
環境はmacOS(Darwin 24.1.0)、Python 3.13、google-auth-oauthlib / google-api-python-client。YouTube Data APIとYouTube Analytics APIを、MCPサーバー(stdio)経由でエージェントから叩く構成である。launchdやcronの定時ジョブではなく、エージェントが必要なときに叩く形なので、「毎朝失敗メールが飛ぶ」ような気づき方はしない。
失敗回数は、手元に残っているClaude Codeのトランスクリプト(.jsonl・983ファイル)を全走査して数えた。数え方は、YouTube系MCPツールのtool_useブロックと、そのIDに対応するtool_resultを突き合わせ、tool_useのIDで重複を除いたユニーク件数である。2026年8月14日16時11分(日本時間)時点で次のとおり。
| 項目 | 値 |
|---|---|
| YouTube系ツールの呼び出しで結果が残っているもの(ID重複除去後) | 74件 |
うちinvalid_grantが返ったもの |
74件 |
| 成功したもの | 0件 |
| 最古 | 2026-07-15T17:37:25.463Z(日本時間 2026-07-16 02:37:25) |
| 最新 | 2026-08-14T07:07:25.544Z(日本時間 2026-08-14 16:07:25) |
| またがった日数 | 13日(UTC日付ベース) |
ツール別の内訳はyoutube_video_stats 34件、youtube_channel_summary 24件、youtube_recent_videos 10件、youtube_top_videos 5件、youtube_video_comments 1件である。
重複を除かずにツール名をgrepで数えると76件になる。 セッションを再開・分岐すると同じtool_useが2つの.jsonlに書かれるためで、当方の記録では2件がこれに当たった(どちらも2026-07-30T11:26のもので、youtube_channel_summaryとyoutube_recent_videosが1件ずつ)。ツール名のgrepヒット数をそのまま「実行回数」として書くと、この2件を二重に数える。
この74件には、この記事のために今日自分で再実行した1件が含まれている。 上の表の「最新」がそれである。件数を数えるときに自分の分を勘定に入れ忘れると数字がずれるので、明示しておく。
そして壊れている限り件数は増え続ける。 この記事の検品中(日本時間16時26分)にもう1件youtube_recent_videosが同じエラーを返し、同じ数え方で75件になった。上の表は16時11分時点のスナップショットである。
止まっている間に何が測れなくなったかも、実物で残っている。当サイトの広告掲載ページ(src/app/advertise/page.tsx)のソースには、次のコメントが入った状態でコミットされている。
// 2026-08-14更新: 登録者数のみ最新(8/13にYouTube Studioで確認した画面事実)。
// 視聴回数・登録者増は7月30日時点の値で、YouTube統計APIの認証が失効しており
// 現時点で取り直せない。古い数字を最新のように見せないため、各項目に時点を明記する。
登録者数は人間がYouTube Studioの画面を見て手で入れ、視聴回数と登録者増は7月30日時点の古い値のまま、時点を明記して据え置くという運用になっている。数字が消えるのではなく、「最新に見えるが最新ではない数字」が居座るのが、この種の失効の実害である。
正直な但し書き
原因は特定できていない。 残る候補6つ(ユーザーによる取り消し/付与済みリフレッシュトークンの上限超過/時間制限付きアクセスの期限切れ/管理者設定のセッション長超過/同意画面Testingの7日失効/1クライアントIDあたり100本上限)のどれなのかは、この記事の時点で判定していない。Google Cloud ConsoleのOAuth同意画面の公開ステータスも、Googleアカウントの「サードパーティ アプリとサービス」の取り消し履歴も、当方は確認していない。確認していないものを「当てはまらない」とは書かない。
「7日で失効」が最も観測と近いが、断定できない。 トークンファイルの作成から最終更新までが6日18時間48分55秒で7日をわずかに下回るのは、Testing公開ステータスの7日ルールと矛盾しない。ただし2026年5月15日14時02分の書き込みが「リフレッシュ成功による書き戻し」なのか「ブラウザで取り直した新規認証の保存」なのかを、当方は区別できていない。 同じパスにpickle.dumpで直接書く別スクリプト(~/Desktop/jitan_lab_agent/setup_oauth_v2.py・3,046バイト・mtime 2026年5月8日19時11分)が手元に存在するためである。後者だった場合、この6日18時間は7日ルールの証拠にならない。
「いつ壊れたか」の下限しか分かっていない。 手元のトランスクリプトは2026年7月11日22時36分53秒(日本時間)が最古で、それ以前のものは残っていない。最初に確認できる失敗が7月16日02時37分というだけで、その前に成功していた証拠は無い。 トークンファイルのmtimeを信じるなら5月15日以降ずっと死んでいた可能性もあるが、上の但し書きのとおりmtimeの意味自体が確定していない。「少なくとも29日間(7月16日〜8月14日)は死んでいた」までが、証拠で言える範囲である。
社内メモには別の日付が書いてある。 再認証スクリプトのdocstringには「この状態が2026-07-28から続いている」と書かれているが、上のトランスクリプト走査では7月16日に既に失敗している。この記事では、後から書かれたメモではなく実ログの日付を採った。
死んだトークンの中身は確認していない。 pickleに入っているスコープ一覧や発行時刻を読めば判定材料が増えるが、認証情報を含むファイルなので開いていない。スコープについて書いたのは、そのトークンを作る/使う側のスクリプトが要求しているスコープであって、トークンに実際に焼かれているスコープではない。
読み取り用トークンの再認証は、この記事の時点でまだ実行していない。 2026年8月14日16時11分の時点でtoken_ai_jitan.pickleのmtimeは5月15日のままであり、退避用の.bak_20260814も生成されていない。したがって「この手順で直った」とは書けない。書けるのは「公式が指示している対処はこれで、そのためのスクリプトはこう書いた」までである。 なお本文に書いたとおり、書き込み用トークンのほうは同じ日の15時13分にブラウザ同意を通しており、「同意し直せば新しいトークンは発行される」ことだけはその1件で確認できている。
Googleの日本語版ヘルプには機械翻訳の注記が付くページがある。 本記事の引用はすべて英語版の原文から取り、訳は当サイトで付けた。訳の責任は当サイトにある。
出典
- Using OAuth 2.0 to Access Google APIs | Authorization | Google for Developers(最終更新2026年5月26日UTC、2026年8月14日に本文取得): https://developers.google.com/identity/protocols/oauth2
- Using OAuth 2.0 for Web Server Applications | Authorization | Google for Developers(最終更新2026年8月7日UTC、2026年8月14日に本文取得): https://developers.google.com/identity/protocols/oauth2/web-server
- Manage OAuth Clients - Google Cloud Platform Console Help(2026年8月14日に本文取得): https://support.google.com/cloud/answer/15549257
- AI時短ラボ 実測ログ:
~/Desktop/token_ai_jitan.pickleと~/Desktop/token_ai_jitan_write.pickleのstat出力/~/Desktop/youtube_reauth.py/~/Desktop/youtube_add_article_links.py/~/Desktop/youtube_description_backup_2026-08-14.json/~/Desktop/script_mcp_server_v3.py/~/.claude/projects/配下のトランスクリプト全走査(いずれも2026年8月14日取得)
出典・参照資料
AIニュースの解説を動画でも
YouTubeでは注目ニュースの背景を解説し、Xでは新着記事をお知らせしています。
コメント
まだコメントはありません。最初のコメントを書いてみませんか?
AIについて聞きたいことはありますか?
質問箱で無料で受け付けています。回答は公開され、他の方の参考にもなります。
質問箱を見る →新しい記事をメールで受け取る
AIの新しい発表を、出典付きで整理して届けます。
関連記事
