1.

Google OAuth /token エラー (Client error: POST) の原因と対処

編集
この記事の要点
  • Client error: POST https://accounts.google.com/o/oauth2/token は Google OAuth2 のトークンエンドポイント呼出が 400/401 で失敗
  • 主な原因: client_id / client_secret 不一致 / redirect_uri 不一致 / code 期限切れ・再利用 / scope 不正 / リフレッシュトークン期限切れ
  • まず確認: Google Cloud Console の OAuth クライアント設定で「承認済みリダイレクト URI」が完全一致しているか(末尾スラッシュ・スキーム含め)
  • Laravel Socialite で出ることが多い: .envGOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET / GOOGLE_REDIRECT を確認
  • Service Account は別ルート: oauth2/token ではなく JWT で署名して oauth2/v4/token へ。混同に注意

エラー全文の典型例

GuzzleHttp\Exception\ClientException:
Client error: `POST https://accounts.google.com/o/oauth2/token` resulted in a
400 Bad Request response:
{
  "error": "invalid_grant",
  "error_description": "Bad Request"
}

   at vendor/guzzlehttp/guzzle/src/Exception/RequestException.php:113
   from /vendor/laravel/socialite/src/Two/AbstractProvider.php:225

Laravel Socialite や PHP の google/apiclient、各種 OAuth ライブラリで Google にトークン交換要求を送ったとき、Google から 400 / 401 が返ってきた状態です。HTTP 400 + invalid_grant が最頻出。

エラー種別 (Google 公式)

error意味原因
invalid_grant付与コードが無効code 期限切れ / 既に使用済 / redirect_uri 不一致 / リフレッシュトークン失効
invalid_clientクライアント認証失敗client_id / client_secret 不一致
invalid_requestパラメータ不足・形式不正grant_type 漏れ / 必須パラメータ欠落
unauthorized_clientこのクライアントは grant_type を使えないクライアント種別ミス(Web/Native)
invalid_scopescope が無効未公開 API / 承認画面で未掲載のスコープ
access_deniedユーザー拒否同意画面で拒否を選択

原因 1: redirect_uri 不一致 — 最頻出

OAuth では、認可サーバーは「クライアント登録時の redirect_uri」と「トークン交換時に渡された redirect_uri」が完全一致するか確認します。1 文字でも違えば invalid_grant

確認手順:

  1. Google Cloud Console → APIs & Services → 認証情報
  2. OAuth 2.0 クライアント ID を開く
  3. 承認済みのリダイレクト URI 欄を確認
  4. アプリ側の redirect_uri とスキーム・ホスト・ポート・パス・末尾スラッシュまで一致しているか確認

よくあるズレ:

Console 登録アプリ送信結果
http://localhost:8000/auth/google/callbackhttp://localhost:8000/auth/google/callback/NG(末尾 /)
https://example.com/callbackhttp://example.com/callbackNG(http vs https)
https://example.com/callbackhttps://www.example.com/callbackNG(www の有無)
https://example.com:443/cbhttps://example.com/cbOK(デフォルトポートは省略可)

原因 2: client_id / client_secret 不一致

環境変数の取り違え、改行混入、コピペ漏れが多い:

# .env をダンプして確認(先頭・末尾の空白に注意)
cat -A .env | grep GOOGLE

# Laravel での確認
php artisan tinker
>>> config('services.google')
=> [
     "client_id" => "1234567890-abcdef.apps.googleusercontent.com",
     "client_secret" => "GOCSPX-xxxxxxxxxxxxxxxxxxxx",
     "redirect" => "http://localhost:8000/auth/google/callback",
   ]

# Console のクライアント ID と完全一致するか比較

原因 3: 認可コードの期限切れ・再利用

Google の認可コード(code=4/0Ad...)は発行から数分以内、かつ1 回しか使えません。よくある事故:

  • デバッガで止めている間に期限切れ
  • 同じ code で 2 回 token 交換した(リロード・テストで再実行)
  • 非同期処理でレース条件

対処: フローを最初からやり直す(認可エンドポイントへリダイレクト)。

原因 4: scope 設定の不整合

承認画面 (OAuth Consent Screen) で公開していない scope を要求すると invalid_scope。または、scope を変えたのに同意画面の登録が古い:

  1. Cloud Console → 認証情報 → OAuth 同意画面
  2. スコープセクションに使う scope が登録されているか
  3. 機密スコープ(Gmail / Drive 等)は審査が必要

原因 5: Service Account との混同

サーバー間通信でユーザーログイン不要の場合は OAuth ではなく Service Account を使います。エンドポイントもフローも違う:

方式用途credentials
OAuth 2.0 (Web Server)ユーザーのデータにアクセスclient_id + client_secret + code
Service Accountサーバー単体での GCP リソース操作JSON 鍵ファイル (private_key で署名 JWT)
API Key公開 API(YouTube 検索等)API キーのみ

Laravel Socialite で出る典型ケース

// config/services.php
'google' => [
    'client_id' => env('GOOGLE_CLIENT_ID'),
    'client_secret' => env('GOOGLE_CLIENT_SECRET'),
    'redirect' => env('GOOGLE_REDIRECT_URI'),
],

// .env
// GOOGLE_CLIENT_ID=1234-abcdef.apps.googleusercontent.com
// GOOGLE_CLIENT_SECRET=GOCSPX-xxxxxxxxxxx
// GOOGLE_REDIRECT_URI=http://localhost:8000/auth/google/callback

// routes/web.php
Route::get('/auth/google',         [GoogleAuth::class, 'redirect']);
Route::get('/auth/google/callback',[GoogleAuth::class, 'callback']);

// Controller
public function redirect()
{
    return Socialite::driver('google')->redirect();
}

public function callback()
{
    try {
        $user = Socialite::driver('google')->user();
    } catch (\Laravel\Socialite\Two\InvalidStateException $e) {
        // セッション切れ → ログイン画面へ
        return redirect('/login')->withErrors('セッションが切れました');
    } catch (\GuzzleHttp\Exception\ClientException $e) {
        // /token エラー
        \Log::error($e->getResponse()->getBody()->getContents());
        return redirect('/login')->withErrors('Google 連携に失敗しました');
    }
}

リフレッシュトークン期限切れ

長期保持しているリフレッシュトークンが無効になると invalid_grant。原因:

  • ユーザーが Google アカウント側でアクセス権を取り消した
  • パスワード変更
  • 6 か月以上未使用(テスト用アプリの場合は7 日で失効
  • Cloud Console で OAuth client を再生成した

対処: 再度 OAuth フローを実行してリフレッシュトークンを取り直す。access_type=offlineprompt=consent を必ず付ける:

return Socialite::driver('google')
    ->scopes(['openid', 'email', 'profile'])
    ->with(['access_type' => 'offline', 'prompt' => 'consent'])
    ->redirect();

デバッグの定石

  1. レスポンスボディを必ずログに出すerror / error_description
  2. 送信パラメータを Wireshark / mitmproxy / Laravel HTTP ログで確認
  3. Cloud Console の監査ログを確認
  4. cURL で手動再現:
curl -X POST https://accounts.google.com/o/oauth2/token \
  -d "code=4/0Ad..." \
  -d "client_id=1234-abc.apps.googleusercontent.com" \
  -d "client_secret=GOCSPX-..." \
  -d "redirect_uri=http://localhost:8000/auth/google/callback" \
  -d "grant_type=authorization_code"

FAQ

Q: ステージングと本番で同じクライアント ID を使ってよい?
A: 非推奨。redirect_uri を増やせば動くが、テスト中の不具合で本番ユーザーに影響しうる。環境ごとに別クライアント IDが推奨。

Q: テスト中に「verified ではないアプリ」と出る
A: OAuth 同意画面で「テストユーザー」に自分の Gmail を追加する。本番公開には Google 審査が必要(機密スコープ使用時)。

Q: ローカルでの redirect_uri は https にすべき?
A: localhost は http で許可される特殊扱い。それ以外は https 必須。

編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. Client error: POST https://accounts.google.com/o/oauth2/token
  2. Client error: GET https://www.googleapis.com/plus/v1/people/me?prettyPrint=false