【何これ】WPエラー (401): このユーザーとして投稿を編集する権限がありません。

+ 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.netwww.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はリクエストを受け取ったとき、認証方式を以下の順で試みる:

  1. クッキー認証(ブラウザでログイン中のユーザー)
  2. アプリパスワード認証(Basic Auth)
  3. その他のプラグイン認証

クッキーが存在するとクッキー認証が優先される。しかし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

コメント

この記事へのコメントはありません。

PAGE TOP