+ AIと REST API で悪戦苦闘した時系列 〜Chrome拡張を作ろうとしたら地獄を見た話〜
🎯 先に結論を言う(検索で来た人はここだけ読めばOK)
原因:WordPressにブラウザでログインしたまま、Chrome拡張からREST APIを叩いていた。
Chrome拡張は host_permissions が設定されていると、そのドメインのブラウザセッションクッキーを fetch リクエストに自動で乗せる。WordPressはそのクッキーを見て「あ、ログイン済みのユーザーだな、クッキー認証で処理しよう」と切り替える。ところが REST APIのクッキー認証には nonce(ワンタイムトークン)が必要で、それがないから弾かれる。アプリパスワードは合ってる。ユーザー名も合ってる。でも401。
解決策:credentials: 'omit' を fetch オプションに追加するだけ。
const response = await fetch(url, {
method: 'POST',
headers: {
'Authorization': `Basic ${credentials}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(wpPayload),
credentials: 'omit' // ← これだけ
});
🏗️ 事の発端:Chrome拡張を作ろうとしたら…
ことの始まりは「GASで作ったWordPress投稿ツールをChrome拡張に移植したい」というヒラメキだった。
GAS版は完成していて、MarkdownをWordPressブロックエディタ形式に変換してREST API経由で投稿するという代物。これをChrome拡張(Manifest V3)に移植しようとした。
AIと一緒に作業を始め、数分程度で動くものができた。構成はこんな感じ:
wp-extension/
├── manifest.json # Manifest V3
├── background.js # Service Worker(WP APIへのPOST担当)
├── newtab.html/css/js # 左右2ペインUI
├── converter.js # GASから移植した変換ロジック
└── showdown.min.js # Markdownライブラリ(ローカル同梱)
最初の投稿テスト:成功。 「やった!完璧じゃん」
そしてUIを少し改善(サイト追加フォームをトグルにして下部に移動)した直後から、地獄が始まった。
📅 時系列:AIと一緒に沼にはまっていく記録
✅ Day 1 – 拡張機能完成&初投稿成功
- Chrome拡張をゼロから構築
- Showdownライブラリはローカル同梱(CDN+evalはMV3のCSP制約でNG)
- background.jsからfetchでREST API叩く構成(CORS回避)
- 投稿成功 🎉
💀 UI改修後 – 突然の401
UIをリファクタしたら 401 エラーが出るようになった。
エラー: WPエラー (401): このユーザーとして投稿を編集する権限がありません。
「UIだけ変えたのになんで?」
AIの最初の診断:
サイトをchrome.storage.localに再登録し直してください。GAS版のデータは別物です。
登録し直した。→ 同じエラー。
🔍 フェーズ1:「認証情報が悪いんじゃないか疑惑」
チェックリスト:
- ✅ ユーザー名はメールアドレスじゃなくログイン名
- ✅ アプリパスワードはWP管理画面から発行したもの
- ✅ スペース込みのパスワードをそのままコピー
新しいアプリパスワードを発行して試した。→ 同じエラー。
🧪 フェーズ2:デバッグログを仕込む
background.js に console.log を追加して、service workerのDevToolsで観察。
console.log('[WP Poster] user:', user);
console.log('[WP Poster] pass length:', pass.length);
console.log('[WP Poster] btoa result:', credentials);
console.log('[WP Poster] response status:', response.status);
console.log('[WP Poster] response body:', JSON.stringify(resData));
ログの結果:
[WP Poster] URL: https://rinyan.net/wp-json/wp/v2/posts
[WP Poster] user: mjflash
[WP Poster] pass length: 29
[WP Poster] btoa result: bWpxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxJ5RQ==
[WP Poster] response status: 401
[WP Poster] response body: {"code":"rest_cannot_create","message":"このユーザーとして投稿を編集する権限がありません。","data":{"status":401}}
URLもユーザー名もbase64エンコードも全部正しい。
なのに401。
「コードは完璧。問題はWordPress側では?」
🔒 フェーズ3:「WordPressのセキュリティプラグイン疑惑」
AIの仮説:
Wordfenceなどのセキュリティプラグインが
Origin: chrome-extension://xxxを不審と判断してブロックしているのでは?
そこで別のCLIツールから同じ認証情報で同じエンドポイントを叩いてみた。
→ 普通に通った。
「WordPress側の問題ではなさそう」
セキュリティプラグイン犯人説、崩れる。
🔄 フェーズ4:「リダイレクトでAuthorizationが消えてる疑惑」
AIの新仮説:
rinyan.net→www.rinyan.netのようなリダイレクトが起きていて、ブラウザ仕様でリダイレクト時にAuthorizationヘッダーが自動的に脱落するのでは?
CLIはリダイレクト後も認証情報を維持するが、ChromeのfetchはクロスオリジンリダイレクトでAuthorizationを捨てる。なかなか筋の通った仮説だった。
対策コード:
// リダイレクト後に再送する
if (response.status === 401 && response.url && response.url !== url) {
response = await fetch(response.url, requestOptions);
}
→ 同じエラー。
リダイレクト犯人説、崩れる。
🔑 フェーズ5:「認証情報をハードコートして変数を全排除」
「もしかして変数の受け渡しどこかでバグってるんじゃ?」と考え、認証情報を直接コードに書いてテスト。
const HC_DOMAIN = 'rinyan.net';
const HC_USER = 'mjflash';
const HC_PASS = 'Ys2f OWy8 ujaG r0hL ifE4 rByE';
しかしそもそもUIのサイト選択チェックで止まっていて投稿できない罠。→ サイト選択チェックも外す。→ 同じエラー。
認証情報受け渡しバグ説、崩れる。
💡 フェーズ6:真犯人発覚(クッキーだった)
「超初心者レベルのミスを探してほしい」と言われたときに気づいた。
AIの最終仮説(これが正解):
Chrome拡張は
host_permissionsが設定されていると、そのドメインのセッションクッキーをfetchに自動で乗せる。WordPressにログインしたまま使っていると、クッキーを見たWordPressが「クッキー認証モード」に切り替わり、nonceがないから401を返す。
まるでこういうことだ:
銀行窓口に「暗証番号付きのキャッシュカード」を持参したのに、顔を覚えている窓口員が「あ、○○さんですよね、ではサインだけで」と処理しようとする。でもサインを用意していないので「本人確認できません」と弾かれる。
暗証番号(アプリパスワード)は完璧だった。でも顔(クッキー)のせいで別の認証フローに誘導されていた。
修正:
credentials: 'omit' // クッキーを送らない
→ 通った。🎉🎉🎉
📊 原因一覧と検証結果まとめ
| 疑惑 | 検証 | 結果 |
|---|---|---|
| 認証情報の入力ミス | ストレージ確認・再登録 | ❌ 違った |
| アプリパスワード失効 | 新規発行 | ❌ 違った |
| WordPress側のブロック | CLI で同じ認証情報テスト | ❌ 違った |
| リダイレクトでAuth脱落 | response.url 確認・再送ロジック追加 | ❌ 違った |
| 変数受け渡しのバグ | ハードコートで全変数排除 | ❌ 違った |
| クッキーによる認証競合 | credentials: ‘omit’ 追加 | ✅ 解決! |
🎓 今回の学び
Chrome拡張の host_permissions の罠
host_permissions: ["https://*/*"] を設定した拡張機能のservice workerがfetchを投げると、対象サイトのブラウザセッションクッキーが自動的に付与される。
通常のWebページからのクロスオリジンfetchではクッキーは送られない(credentials: 'same-origin' がデフォルト)が、Chrome拡張は別扱い。
WordPressの認証優先順位
WordPressのREST APIはリクエストを受け取ったとき、認証方式を以下の順で試みる:
- クッキー認証(ブラウザでログイン中のユーザー)
- アプリパスワード認証(Basic Auth)
- その他のプラグイン認証
クッキーが存在するとクッキー認証が優先される。しかしREST APIのクッキー認証にはnonceが必要なため、nonceなしのリクエストは401になる。
正解コード(再掲)
const response = await fetch(url, {
method: 'POST',
headers: {
'Authorization': `Basic ${btoa(`${user}:${pass}`)}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(payload),
credentials: 'omit' // ← これが命綱
});
🤖 AIとハマった感想
今回のデバッグで印象的だったのは、AIが「コードは正しい」と何度も確認しながら、外部要因(WordPress側、リダイレクト)を丁寧に潰していったこと。
最終的に「超初心者レベルのチョーがつく凡ミスを探してほしい」(カンマがピリオドになってるとかパスワードのコピペミスで最後に半角スペースが一緒についてきてるとか0)と投げかけたときに、「おい、お前、WordPressにログインしたままじゃないか?」という視点が出てきて解決した。
技術的な調査は網羅的だったが、「使用環境の状態」という非コード的な要素が原因だったというオチ。
エラーメッセージの rest_cannot_create(=ユーザーが認証されていない)と status: 401 は最初から「アプリパスワード認証が届いていない」を示していた。届いていたのに届いていなかった理由がクッキーによる認証経路の乗っ取りだった。
同じ沼にはまる人が一人でも減れば幸いです。
※普通 Chrome拡張機能からWordPressに投稿する時は結果を見るために絶対にログインしてるやろが!と思った。
買い物たとえ
合言葉(アプリパスワード)を覚えてきたのに「会員さんは合言葉不要です、指紋認証でどうぞ」と優遇された。しかーし指紋は登録していない。合言葉は完璧だった。親切心で弾かれた。
夫婦関係たとえ
ちゃんと謝った(アプリパスワード)のに「先週も同じこと謝ったよね(クッキー検知)、先週の謝罪が本物だったかまず証明して(nonce)」と言われた。証明できない。今日の謝罪は完璧だった。過去の自分に弾かれた。
例えが難しいけど、ぴったりのがあればぜひ教えてください。
制作環境:Chrome拡張 Manifest V3 / WordPress 7.0 / Claude Sonnet 4.6
コメント