このエラーが発生する状況
テーマスクリプト、カスタムブロック、またはヘッドレスフロントエンドからWordPress REST APIを呼び出した際、データの代わりに403エラーが返される場合があります:
{ "code": "rest_cookie_invalid_nonce", "message": "Cookie nonce is invalid", "data": { "status": 403 } }
ユーザーはログイン済みで、Cookieも存在しています。それでもWordPressはリクエストを拒否します。APIコールで送信されたnonceがサーバー側の検証を通過しなかったためです。
エラーの原因
WordPress REST APIのCookie認証には、特定のnonceアクションが必要です:wp_rest。認証が必要なリクエストのたびに、WordPressはこのトークンを検証して、そのコールが正規のログインセッションから来たものであり、クロスサイトリクエストの偽造ではないことを確認します。以下のような状況でこの検証が失敗します:
- nonceアクションが間違っている — 最もよくある間違いです。
wp_create_nonce('anything_else')では誤ったハッシュが生成されます。アクションは必ずwp_restでなければなりません。 - nonceがキャッシュされている — キャッシュプラグインが古いHTMLを返し、そこに含まれるnonceはすでに数時間前に期限切れになっています。nonceの有効期間は12〜24時間ですが、キャッシュされたページはそれよりもはるかに長い時間同じnonceを返し続ける可能性があります。
- credentialsフラグが欠けている — fetchコールに
credentials: 'include'がないと、ブラウザはCookieを送信しません。この場合、ハッシュが正しくてもnonceの検証は失敗します。 - nonceが送信されていない —
X-WP-Nonceヘッダーまたは_wpnonceクエリパラメーターがリクエストに含まれていません。 - セッションの期限切れ — ユーザーのログインがタイムアウトし、そのセッションに紐づくnonceがすべて無効になっています。
簡単な修正方法
ステップ1 — 正しいnonceアクションを使用する
functions.phpまたはプラグインファイルで、アクションはwp_restでなければなりません — プラグインのスラッグや独自の文字列ではありません:
// 正しい
$nonce = wp_create_nonce( 'wp_rest' );
// 誤り — rest_cookie_invalid_nonceが発生する
$nonce = wp_create_nonce( 'my_plugin_action' );
ステップ2 — wp_localize_scriptでnonceをJavaScriptに渡す
wp_enqueue_scriptsにフックして、nonceとREST APIのルートURLをフロントエンドに渡します:
function my_enqueue_scripts() {
wp_enqueue_script(
'my-app',
get_template_directory_uri() . '/js/app.js',
[],
'1.0',
true
);
wp_localize_script( 'my-app', 'wpApiSettings', [
'root' => esc_url_raw( rest_url() ),
'nonce' => wp_create_nonce( 'wp_rest' ),
] );
}
add_action( 'wp_enqueue_scripts', 'my_enqueue_scripts' );
ステップ3 — fetchコールでnonceを送信する
ここで重要な点が2つあります:X-WP-Nonceヘッダーでトークンを送信し、credentials: 'include'でブラウザに認証Cookieを送るよう指示します。どちらか一方でも欠けると403エラーになります。
fetch( wpApiSettings.root + 'wp/v2/posts', {
method: 'GET',
credentials: 'include', // 必須 — 認証Cookieを送信する
headers: {
'X-WP-Nonce': wpApiSettings.nonce,
'Content-Type': 'application/json',
},
} )
.then( res => res.json() )
.then( data => console.log( data ) );
ヘッダーよりもクエリ文字列を使いたい場合は、代わりに?_wpnonce=を追加します:
const url = wpApiSettings.root + 'wp/v2/posts?_wpnonce=' + wpApiSettings.nonce;
fetch( url, { credentials: 'include' } );
恒久的な対策 — nonceの期限切れを適切に処理する
ページ読み込み時に埋め込まれたnonceはいつか期限が切れます。シングルページアプリや長期セッションでは、これが頻繁に問題になります — 一晩タブを開いたままにしたユーザーが戻ってくると、nonceはすでに古くなっています。新しいnonceをオンデマンドで取得できるAJAXエンドポイントを登録しておきましょう:
// functions.php
add_action( 'wp_ajax_wp_rest_nonce', function() {
wp_die( wp_create_nonce( 'wp_rest' ) );
} );
次に、すべてのfetchコールをヘルパー関数でラップして、nonceの期限切れによる403を検知して自動的にリトライするようにします:
async function apiFetch( url, options = {} ) {
const defaultHeaders = {
'X-WP-Nonce': wpApiSettings.nonce,
'Content-Type': 'application/json',
};
const response = await fetch( url, {
credentials: 'include',
...options,
headers: { ...defaultHeaders, ...( options.headers || {} ) },
} );
if ( response.status === 403 ) {
const body = await response.json();
if ( body.code === 'rest_cookie_invalid_nonce' ) {
const refreshRes = await fetch(
'/wp-admin/admin-ajax.php?action=wp_rest_nonce',
{ credentials: 'include' }
);
const newNonce = await refreshRes.text();
wpApiSettings.nonce = newNonce;
return fetch( url, {
credentials: 'include',
...options,
headers: { ...defaultHeaders, 'X-WP-Nonce': newNonce, ...( options.headers || {} ) },
} );
}
}
return response;
}
すべての生のfetch()呼び出しをapiFetch()に置き換えれば、nonceの期限切れ問題を気にする必要はなくなります。
キャッシュプラグインへの対応
WP Rocket、W3 Total Cache、LiteSpeed Cache — これらはいずれもwp_localize_scriptで注入されたnonceを含む、ページのHTMLをそのままキャッシュします。キャッシュされたページにアクセスしたユーザーは、場合によっては数時間前にすでに無効になったnonceを受け取ることになります。対処法は3つあります:
- 対象ページをキャッシュから除外する — ほとんどのプラグインは特定のURLをブロックリストに登録したり、ログイン中のユーザーのキャッシュを完全にスキップしたりする機能を持っています。
- 上記のnonce更新パターンを使用する — ページに埋め込まれたnonceを信頼せず、最初の実際のAPIコールの前に新しいnonceを取得します。
- キャッシュのTTLを短縮する — 認証が必要なREST API呼び出しを行うページでは、TTLを1〜2時間に抑えます。
最終手段として、サーバー側でnonceの有効期限を延長して期限切れまでの時間を稼ぐこともできます:
add_filter( 'nonce_life', function() {
return 24 * HOUR_IN_SECONDS; // デフォルトの12時間ではなく24時間にする
} );
これで期限切れによるエラーは減りますが、攻撃者が盗んだnonceを再利用できる時間も長くなります。他の方法が現実的でない場合にのみ使用してください。
動作確認
DevToolsを開いて「ネットワーク」タブに移動し、REST呼び出しを実行してリクエストを確認します:
- レスポンスのステータスが
403ではなく200になっている - リクエストヘッダーに
X-WP-Nonce: <hash>が含まれている - リクエストヘッダーに
Cookie: wordpress_logged_in_...が含まれている — 認証Cookieが正しく送信されていることを確認できる
すべてが正常に動作していることをエンドツーエンドで確認するためのコンソールテスト:
fetch( wpApiSettings.root + 'wp/v2/users/me', {
credentials: 'include',
headers: { 'X-WP-Nonce': wpApiSettings.nonce },
} )
.then( r => r.json() )
.then( u => console.log( 'Logged in as:', u.name ) );
// 403ではなく、ユーザー名が表示されるはずです
補足
Cookie + nonce認証は同一オリジンのみで機能します。ブラウザとWordPressが同じドメインを共有しており、ユーザーがアクティブなログインセッションを持っている場合に動作します。クロスオリジンのフロントエンドやサーバー間の通信には別の方法が必要です — WordPress 5.6から標準搭載されているアプリケーションパスワードを使用してください。「ユーザー」→「プロフィール」→「アプリケーションパスワード」に移動し、連携ごとにパスワードを作成して、HTTP Basicで認証します:
curl -u "your_username:xxxx xxxx xxxx xxxx xxxx xxxx" \
https://your-site.com/wp-json/wp/v2/posts
連携ごとに1つのパスワードを使用し、その連携が廃止された時点で即座に無効化してください。テストやサービスアカウント用に強力なランダムな認証情報を生成するには、toolcraft.app/en/tools/security/password-generatorを使用すると、サーバーに何も送信せずにブラウザ上でローカルに生成できます。

