
GSC連携をサービスアカウント方式からOAuth2へ切り替えた話
Google Search Console(GSC)連携まわりで、少し厄介な問題に当たりました。
DMM AUTO POST では、Search Console API を使って投稿ごとの検索パフォーマンスを取得しています。これまでは Google Cloud のサービスアカウントを作り、そのメールアドレスを Search Console のプロパティにユーザー追加する方式で連携していました。
ところが今回、GSCの管理画面側でサービスアカウントのメールアドレスをプロパティに追加できない問題が起きました。
コード側のバグではなく、Google側のUIで詰まるタイプの問題です。
こうなると、プラグイン側でいくらJWT生成やAPI呼び出しを直しても根本解決になりません。ユーザーがGSC側でサービスアカウントを追加できない限り、認証が成立しないからです。
そこで、GSC連携の認証方式を サービスアカウント方式から OAuth2 Authorization Code Flow へ切り替える ことにしました。
もともとのGSC連携
従来の構成はこうです。
Google Cloud でサービスアカウントを作る
JSONキーを発行する
プラグイン設定画面にJSONを貼り付ける
Search Console の対象プロパティに、サービスアカウントのメールアドレスをユーザー追加する
プラグイン側でJWTを作り、access tokenを取得する
Search Console APIを呼ぶ
サーバーサイドの処理としては安定しています。
プラグインが自動処理をするには相性がよく、ユーザーが毎回Googleログインする必要もありません。
ただし、ひとつ大きな前提があります。
Search Console側にサービスアカウントをユーザー追加できること。
今回壊れたのは、まさにここでした。
サービスアカウント方式の弱点
サービスアカウント方式では、WordPress側で秘密鍵JSONを持ちます。
プラグインはその秘密鍵を使ってJWTを署名し、Google OAuth2 token endpoint から access token を取得します。
仕組みとしてはきれいですが、GSC連携では少し特殊です。
Search Console APIを使うには、そのサービスアカウントが対象プロパティに対する権限を持っている必要があります。
つまり、ユーザーにこう案内することになります。
「この xxxx@xxxx.iam.gserviceaccount.com を Search Console のプロパティに追加してください」
ところが、GSCの管理画面でその追加ができない。
これはユーザー体験としてかなりつらいです。
プラグイン設定は合っている。JSONも正しい。APIも有効。なのに、GSC側のUIで止まる。
こういう外部サービス依存の詰まり方は、利用者にとって原因が見えにくいです。
OAuth2 Authorization Code Flowへ切り替える
そこで、認証方式をOAuth2へ寄せることにしました。
新しい流れはこうです。
Google Cloud で OAuth2 クライアントIDとクライアントシークレットを作る
WordPress管理画面にIDとシークレットを保存する
「Googleアカウントで認証する」を押す
Googleの同意画面でGSC読み取り権限を許可する
callbackで認可コードを受け取る
access token と refresh token を保存する
以降は refresh token で access token を更新する
この方式なら、Search Consoleに別途サービスアカウントを追加する必要がありません。
ユーザー自身のGoogleアカウントで認証するので、そのアカウントがGSCプロパティに権限を持っていればAPIを呼べます。
今回の問題は「サービスアカウントをGSCに追加できない」ことだったので、そこを通らない設計に変えたわけです。
ただ置き換えればいいわけではなかった
最初は「サービスアカウントのJWT処理をOAuth2に置き換えれば終わり」と思いそうになります。
でも実際には、考えることがいくつもありました。
refresh_token を確実に取るには access_type=offline が必要
再認証時にも refresh_token を得やすくするには prompt=consent が必要
OAuthの state はnonceだけではなく、ランダム値を保存してcallbackで照合したい
webmasters.readonly だけではメールアドレスは取れない
token refresh のレスポンスには refresh_token が返らないことが多い
設定保存時にクライアントシークレットを空欄で上書きしてはいけない
既存ユーザーのサービスアカウント設定をいきなり消すと移行に失敗したとき戻れない
GSC連携OFFのときはOAuth経路でもAPIを呼んではいけない
OAuth2自体はよくある仕組みですが、WordPressプラグインの設定画面、既存ユーザーの移行、Search Console APIの仕様が重なると、意外と細かい落とし穴があります。
CodexとClaude Codeで壁打ちした
今回いちばん面白かったのは、実装そのものよりも設計の詰め方でした。
実はこの修正、CodexとClaude Codeの両方を使って壁打ちしました。
片方に設計を書かせて、もう片方にレビューさせる。
指摘を反映した設計をもう一度見せて、実装に落とす。
さらに実装後に、設計とコードのズレを確認する。
人間が「こういう問題を解決したい」という方向を持ち、AI同士をレビュー役として使う感じです。
今回だと、最初の設計ではこんな抜けがありました。
refresh_token を取るためのOAuthパラメータが足りない
メールアドレスを保存する設計なのに、必要なscopeが入っていない
state が wp_create_nonce() だけで少し弱い
「旧サービスアカウント処理を削除」と「移行中はフォールバック」が矛盾している
接続テストがまだ parse_service_account() に依存している
こういう矛盾は、自分ひとりで設計を書いていると見落としやすいです。
一方で、AIにレビューさせると「このメソッドはまだ旧方式に依存しています」「この保存キーはフォーム送信で消える可能性があります」と、コードベースの横断的な観点で突いてくれます。
もちろんAIの指摘が全部正しいわけではありません。
でも、設計を雑に通さないための相手としてはかなり便利です。
設計で決めたこと
最終的には、次の方針にしました。
OAuth2を優先する
OAuth2で接続済みなら、以降のGSC API呼び出しはOAuth2 tokenを使います。
access tokenが期限切れなら、保存済みのrefresh tokenで更新します。
ensure_access_token()
OAuth token が有効なら access_token を返す
期限切れなら refresh_access_token()
OAuth未接続なら旧サービスアカウント方式へフォールバック旧方式はすぐ消さない
既存ユーザーがいるので、サービスアカウント方式は移行期間中だけ残します。
OAuth2接続に成功したらOAuth2を優先しますが、gsc_sa_json はすぐには削除しません。
移行に失敗したとき、旧方式で戻れる余地を残すためです。
stateはランダム値で照合する
OAuth callbackのCSRF対策として、random_bytes() で生成したstateを user transient に保存し、callback時に照合して削除します。
state生成 → user transientに保存
callback → state一致確認
検証後 → transient削除WordPress nonceだけに寄せず、OAuthの往復に対応した形にしました。
refresh_tokenが空なら初回認証は失敗扱い
初回認証でrefresh tokenが取れないと、長期運用できません。
そこで、初回認証時に refresh_token が空ならエラーにします。
ただし再認証時は、既存のrefresh tokenがあればそれを維持します。Googleのtoken refreshや再認証では、毎回refresh tokenが返るとは限らないからです。
シークレットは空欄保存で維持する
管理画面のクライアントシークレット欄はpassword inputにしました。
ただし、保存済みの値をそのまま表示するわけにはいきません。
そこで、空欄のまま保存した場合は既存値を維持し、入力されたときだけ更新するようにしました。
これを忘れると、設定画面で別の項目を保存しただけでOAuthクライアントシークレットが空になってしまいます。
実装で直したところ
今回の実装では、主に次のファイルを触りました。
includes/gsc/class-dmmap-gsc-auth.php OAuth2認証URL生成、認可コード交換、refresh token更新、旧サービスアカウント方式フォールバック
includes/class-dmm-auto-post-admin.php OAuth2クライアントID/シークレット入力、Google認証ボタン、切断ボタン、callback処理
includes/class-dmm-auto-post.php 新しいoption keyのデフォルト値とサニタイズ
includes/class-dmm-auto-post-gsc.php 接続テスト結果の表示、旧方式依存の整理
画面としては、GSC連携タブがこう変わります。
GSC連携: ON/OFF
GSCプロパティURL
OAuth2 クライアントID
OAuth2 クライアントシークレット
Google認証状態
接続テスト
旧サービスアカウント設定旧サービスアカウントJSONは、通常は折りたたみ表示にしました。
OAuth2へ移行した人には見えすぎない。でも既存ユーザーの復旧導線としては残す。
このくらいの距離感がちょうどよさそうです。
レビューで見つかった地味に危ない点
実装後にも、Codex側でレビューしました。
そこで見つかったのが、次のような点です。
設定画面内でフォームを入れ子にしていた
WordPressの設定画面は、全体が options.php に送信するformになっています。
その中で切断ボタン用に別のformを出すと、HTMLとして入れ子フォームになります。
これはブラウザによって解釈が崩れます。
最終的には、切断処理は wp_nonce_url() 付きのリンクに変更しました。
GSC連携OFFでもOAuth経路なら動いてしまう
旧サービスアカウント方式では、parse_service_account() の中で gsc_enabled を見ていました。
しかしOAuth2経路では、そのチェックを通らずにaccess tokenを返せてしまう状態になっていました。
これは「GSC連携OFFなら取得しない」という既存仕様を破ります。
そこで ensure_access_token() の先頭で gsc_enabled を確認するようにしました。
refresh_tokenなしでも成功扱いになっていた
初回認証で refresh_token が取れていないのに、接続成功として保存できる状態もありました。
これだと画面上は成功に見えるのに、次回以降のAPI呼び出しで失敗します。
初回認証では refresh_token 必須にして、取れなければエラーにしました。
こういう修正は、動けばOKの実装では見落としやすいところです。
でも運用では、こういう「一見成功したように見える失敗」がいちばん困ります。
AIに丸投げではなく、レビュー相手として使う
今回の進め方でよかったのは、AIを単なるコード生成役にしなかったことです。
やったことは、どちらかというと設計レビューの往復でした。
問題を言語化する
解決方針を設計書にする
別のAIに設計の穴を探させる
指摘を反映する
実装する
実装後にもう一度レビューする
構文チェックする
AIが2つあると、片方が書いたものをもう片方に疑わせることができます。
これはかなり良いです。
人間がレビューするときも、実装者本人の目では見落とすことがあります。
AIも同じで、書いた本人役のAIとレビュー役のAIを分けると、少し視点が変わります。
もちろん最終判断は人間がします。
でも「この設計、どこが危ない?」と聞ける相手が常にいるのは大きいです。
まとめ
今回のGSC連携修正は、単にOAuth2対応を入れたというより、外部サービス依存の認証方式を見直した作業でした。
サービスアカウント方式は便利ですが、GSC側でユーザー追加できないと詰まります。
OAuth2 Authorization Code Flowに切り替えることで、ユーザー自身のGoogleアカウントで認証できるようになり、GSC側のサービスアカウント追加問題を回避できます。
ただし、OAuth2にはOAuth2の落とし穴があります。
refresh_token、state、設定保存、既存ユーザー移行、GSC連携OFF時の挙動。
このあたりを設計段階で潰せたのは、CodexとClaude Codeで壁打ちした効果が大きかったです。
AIに全部任せるのではなく、設計の矛盾を突くレビュー相手として使う。
今回のいちばんの学びは、そこでした。