WordPress Logo WordPress REST API のアクセス制御

rest_authentication_errors や rest_pre_dispatch などのフィルターの使い方から、特定のプラグイン・ドメインのみを許可する方法、カスタムヘッダーやCORS制限の活用、APIキーベースのシンプルな認証方法まで、実運用で役立つ知識を解説しています。

更新日:2025年05月11日

作成日:2025年05月10日

アクセスを制御する

WordPress REST API のアクセスを制御するには、rest_authentication_errors フィルターや rest_pre_dispatch フィルターなどを利用することができます。

例えば、WordPress REST API を無効にする場合、無条件にすべて無効化してしまうと、REST API に依存する管理画面の機能(例:Gutenberg エディター)などが動作しなくなってしまいます。

そのため、WordPress REST API を無効にして匿名の外部アクセスを防止するには、ユーザーがログインしていない場合にのみ無効化するようにします(REST API Handbook)。

以下は rest_authentication_errors フィルターフックを使って、ログインしていない場合にのみ WordPress REST API を無効にする例です。

add_filter( 'rest_authentication_errors', function( $result ) {
  // 以前の認証チェックが適用されている場合は、その結果を変更せずに渡す
  if ( true === $result || is_wp_error( $result ) ) {
      return $result;
  }

  // ログインしていない場合はエラーを返す(REST API へのアクセスを止める)
  if ( ! is_user_logged_in() ) {
    return new WP_Error(
      'rest_not_logged_in',
      __('You are not currently logged in.', 'text-domain'),
      array( 'status' => 401 )  // 401 Unauthorized(認証失敗)
    );
  }

  // ログイン済みのリクエストはそのまま返す(許可)
  return $result;
}); 

以下は rest_pre_dispatch フィルターフックを使って、ログインしていない場合にのみ WordPress REST API を無効にする例です。

add_filter('rest_pre_dispatch', function($result, $server, $request) {
// ログインしていない場合はエラーを返す(REST API へのアクセスを止める)
if ( ! is_user_logged_in() ) {
  return new WP_Error(
    'rest_disabled',
    __('The REST API on this site has been disabled.', 'text-domain'),
    array( 'status' => 403 ) // 403 Forbidden(処理を拒否)
  );
}

return $result;
}, 10, 3);

このように、rest_authentication_errors フィルターや rest_pre_dispatch フィルターを使って WordPress REST API のアクセスを制御することができます。

基本的には、認証(ログイン・APIキーなど)に関する制御をしたい場合は rest_authentication_errors を使い、リクエスト内容に応じて処理を止めたい場合は rest_pre_dispatch を使います。

また、上記のコードの場合、どちらの場合も、ログインしていなければブラウザで /wp-json/ にアクセスできませんが、rest_authentication_errors のコードでは、ログインしていれば、/wp-json/ にアクセスできますが、rest_pre_dispatch のコードではログインしていても、/wp-json/ にアクセスできないなどの実行されるタイミングによる違いが発生する場合があります。

rest_authentication_errors と rest_pre_dispatch の違い

以下は2つのフィルターの違いを簡単にまとめたものです。

比較項目 rest_authentication_errors rest_pre_dispatch
タイミング 認証段階 リクエスト処理実行直前
使い道 認証・ログインチェック中心 実行制御・レスポンス制御
エラー返すと? 認証失敗(401など) 実行失敗(403や独自ステータス)

rest_authentication_errors は「誰に許すか」を決め、認証(authentication)エラーとして扱うのに対して、rest_pre_dispatch は「このリクエスト自体を許すか」を決め、リクエスト制御(dispatch前)エラーとして扱うという違いがありますが、どちらもフィルターなので、要件に応じて両方使うことができます(両方使うとより柔軟なアクセス制御ができます)。

フローチャート的に表すと以下のようになります。

RESTリクエストが来た
  ↓
認証段階(ログイン済みか? APIキー正しいか?)
  ↓
→ 認証エラーなら rest_authentication_errors でエラー返す
  ↓
→ 認証成功したら次へ
  ↓
リクエスト処理直前(実行する?止める?レスポンス上書きする?)
  ↓
→ rest_pre_dispatch で制御
  ↓
→ 最後、正式にAPI実行

リクエスト処理の流れ

以下は WordPress REST API リクエスト時のフィルター・アクションなど処理の順番です。

  1. リクエスト
    • ブラウザ・クライアントから /wp-json/... にリクエストが来る
    • 通常リクエストとRESTリクエストの判別がここで始まる。
  2. rest_api_loaded
    • REST API リクエストと判断された時点で最初に発火。
    • 通常ルーティングから REST モードに切り替える。
    • ここではルーティング処理はまだ行われない。
    • 初期化、認証関連の準備、ロギングに使う。
  3. rest_authentication_errors(認証チェック)
    • ユーザー認証の処理。ログインしているか、認証ヘッダーがあるかなどの判定。
    • ここでエラー (WP_Error) を返すと、それ以降の処理をすべて止めてレスポンスを返す(終了)。
    • この時点では $request オブジェクトは無いので、$wp->query_vars などを使うしかない。
  4. ルーティング処理(内部処理)
    • リクエスト URL や HTTP メソッドに応じて、どのエンドポイントに対応するかを決定。
    • この時点で $request が作られるので、$request->get_route() などが使えるようになる。
  5. rest_pre_dispatch(エンドポイント処理直前)
    • 認証も権限も通った後、エンドポイント処理を開始する直前
    • ここでリクエストを差し替えたり、エラーに変えたりできる
    • WP_Error または WP_REST_Response を返すと処理が止まる
    • ここで WP_Error を返すと、403系エラーやカスタムエラーを発生させられる
    • $request オブジェクトが使える。
  6. permission_callback(エンドポイントの権限チェック)
    • 各エンドポイント登録時に設定されている permission_callback が実行される。
    • permission_callback で NG なら(false を返すと)403 Forbidden エラーになる。
  7. rest_request_before_callbacks
    • permission_callback が通った直後、実際のエンドポイント callback を呼び出す直前に実行。
    • ここで WP_Error を返せば、コールバックは実行されない。
    • $request がフルで使える。
  8. エンドポイント callback 実行
    • APIエンドポイントのメインの処理(例:投稿データを取得、作成、更新、削除など)が行われる
  9. rest_post_dispatch(レスポンス加工)
    • エンドポイント callback が終わった後、レスポンスデータを加工できる。
    • 例えば、APIレスポンスに共通ヘッダーや追加フィールドを付与するなど。
    • $response と $request の両方にアクセスできる。
  10. rest_send_response
    • WP_REST_Response を最終的な HTTP レスポンスに変換する直前。
    • ヘッダーや HTTP ステータスコードの最終調整に使える。
    • ここが実質最後のフックポイント。
  11. ブラウザ・クライアントへレスポンス
    • 完成した HTTP レスポンスが送信される。

rest_authentication_errors

rest_authentication_errors は WordPress の REST API リクエストを受けたとき、そのリクエストがすでに認証できているかやエラーがあるかをチェックするフィルターで、ここで WP_Error を返すと、即エラー終了になります(以降のルーティングや permission_callback は実行されません)。

REST API リクエストの最初の段階(認証フェーズ)でコアが必ずこの rest_authentication_errors フィルターを呼び出します。

そのため、Cookie 認証や Application Passwords 認証、OAuth 認証 などの前後に、ここで独自に認証ルールを追加することができます。

例えば、このフィルターを使って以下のようなことが可能です。

  • ログインしてないと REST API 禁止(前述の例)
  • 特定のエンドポイントだけ例外で許可
  • IPアドレスやオリジン(HTTP_ORIGIN)で制限
  • APIキーなど独自ヘッダーによる認証

コールバック関数

以下は、コールバック関数の基本形です。

add_filter('rest_authentication_errors', function($result) {
  // ここで認証チェック・制御処理をする

  return $result;
});

コールバック関数の引数 $result には、それまでの認証フィルターがどう判断したか(前段階の認証処理)の結果が入っています。

$result の値 意味
null 認証チェック未実行、まだ判断されてない
true すでに認証がOKと判断されている(例:プラグインがOK返した)
false 認証に失敗したことを示している(「失敗」とだけ分かるが、詳細理由なし)
WP_Error インスタンス すでに認証エラーが発生している(エラーコード・メッセージ付き)

コールバック関数が返す値(戻り値)で次の流れが決まります。

戻り値 意味
null 問題なしとみなして、次の処理へ進む
true 認証成功とみなして、リクエスト続行
WP_Error インスタンス 即座にエラー終了(401 Unauthorized など)
$result をそのまま返す 変更せず次のフィルターに渡す(自分のフィルターでは特に何もしない)

エラー終了させるには、WP_Error インスタンスを返し、自分のフィルターでは特に何もしない場合は $result をそのまま返します。

また、基本的には、以前の認証チェックが適用されている場合は、その結果を変更せずに返すように最初に以下を記述します。

if (true === $result || is_wp_error($result)) {
  return $result; // すでに決まってるならそのまま!
}

以下は先述のログインしていない場合にのみ WordPress REST API を無効にするコードの再掲載です。

add_filter( 'rest_authentication_errors', function( $result ) {
  // 以前の認証チェックが適用されている場合は、その結果を変更せずに渡す
  if ( true === $result || is_wp_error( $result ) ) {
      return $result;
  }

  // ログインしていない場合はエラーを返す
  if ( ! is_user_logged_in() ) {
    return new WP_Error(
      'rest_not_logged_in',
      __('You are not currently logged in.', 'text-domain'),
      array( 'status' => 401 )  // 401 Unauthorized(認証失敗)
    );
  }

  // ログイン済みのリクエストはそのまま返す(許可)
  return $result;
}); 

上記はログインしていないなら即エラーを返し、ログインしているなら最後にまとめて許可していますが、以下のようにログインしているなら即許可し、ログインしていなければ最後にエラーを返すこともできます。

add_filter('rest_authentication_errors', function ($result) {
  if (true === $result || is_wp_error($result)) {
    return $result;
  }

  // ログイン済みユーザーは許可
  if (is_user_logged_in()) {
    return $result;
  }

  // ログインしていない場合はエラーを返す
  return new WP_Error(
    'rest_not_logged_in',
    __('You are not currently logged in.', 'text-domain'),
    array('status' => 401)
  );
});

また、無名関数ではなく、名前付きのコールバック関数にして以下のように記述することもできます。

以下では成功パターンをまとめて無駄な if をなくしています。

add_filter('rest_authentication_errors', 'restrict_rest_api_to_logged_in_users');

function restrict_rest_api_to_logged_in_users($result) {
  if (true === $result || is_wp_error($result) || is_user_logged_in()) {
    return $result;
  }

  return new WP_Error(
    'rest_not_logged_in',
    __('You are not currently logged in.', 'text-domain'),
    array('status' => 401)
  );
}

rest_pre_dispatch

rest_pre_dispatch フィルターは、WordPress REST APIのリクエスト処理が「実際に実行される直前」に呼び出されるフィルターです。

つまり、認証(rest_authentication_errors)とパーミッションチェック(permission_callback)が終わった後で、まだリクエストが実際に実行される前のタイミングでフィルタリングできます。

rest_pre_dispatch フィルターを使うと、以下のようなことが可能です。

  • リクエストを強制終了(キャンセル)
  • 特定条件でレスポンスを差し替え
  • ログを取る

以下のような場合に使用できます。

  • 特定のルートや API だけ拒否・制御したいとき
  • 全リクエストを監視・ログに記録したいとき
  • リクエストを条件によって早期終了させたいとき
  • レスポンスを強制的に置き換えたいとき(キャッシュレスポンス返すなど)

コールバック関数

以下は、コールバック関数の基本形です。

add_filter('rest_pre_dispatch', function($result, $server, $request) {
  // ここで何らかの処理をする
  return $result;
}, 10, 3);
引数 内容
$result mixed すでに途中で何かの値がセットされていればここに入る。何か特別なレスポンスを返したい場合はここにセット。普通は最初は null。
$server WP_REST_Server REST API のサーバーインスタンス(WP_REST_Server オブジェクト)。必要ならリクエストメソッドや内部情報を取れる。
$request WP_REST_Request 現在処理中のリクエストオブジェクト。エンドポイント、メソッド、パラメータなど、リクエストに関するあらゆる情報が入ってる。

コールバック関数の戻り値

戻り値 意味
$result をそのまま返す リクエストは続行
WP_Error インスタンス リクエストをエラーで終了(403 Forbidden など)
WP_REST_Response 独自レスポンスにすり替える
null 問題なしとみなして、通常処理続行

以下は先述のログインしていない場合にのみ WordPress REST API を無効にするコードの再掲載です。

add_filter('rest_pre_dispatch', function($result, $server, $request) {
  // ログインしていない場合はエラーを返す
  if ( ! is_user_logged_in() ) {
    return new WP_Error(
      'rest_disabled',
      __('The REST API on this site has been disabled.', 'text-domain'),
      array( 'status' => 403 ) // 403 Forbidden(処理を拒否)
    );
  }

  return $result;
}, 10, 3);

以下のようにログインしているなら即許可し、ログインしていなければ最後にエラーを返すこともできます。

add_filter('rest_pre_dispatch', function($result, $server, $request) {
  // ログイン済みユーザーは許可
  if ( is_user_logged_in() ) {
    return $result;
  }

  return new WP_Error(
    'rest_disabled',
    __('The REST API on this site has been disabled.', 'text-domain'),
    array( 'status' => 403 )
  );
}, 10, 3);

無名関数ではなく、名前付きのコールバック関数にして以下のように記述することもできます。

add_filter('rest_pre_dispatch', 'restrict_rest_api_to_logged_in_users', 10, 3);

function restrict_rest_api_to_logged_in_users($result, $server, $request) {
  if (is_user_logged_in()) {
    return $result;
  }

  return new WP_Error(
    'rest_disabled',
    __('The REST API on this site has been disabled.', 'text-domain'),
    array('status' => 403)
  );
}

特定のプラグインだけ許可

先述の「ログインしていない場合にのみ WordPress REST API を無効にする」例の場合、Contact Form 7 などの「ログインしていないユーザー」も対象にして REST API 通信しているプラグインなどは正常動作できなくなってしまいます。

そのため、プラグインが使う特定のエンドポイントだけを許可する処理を追加する必要があります。

rest_authentication_errors を利用

以下は rest_authentication_errors フィルターを使って、Embed(oEmbed)や Contact Form 7、 Akismet プラグイン、及びカスタムエンドポイントを許可する例です。

また、管理画面が正常に機能するように、編集権限があるユーザーは許可するようにしています。

許可するルート($allowed_routes)には、REST API のエンドポイントの /wp-json/ 以降のパス(namespace)にスラッシュを付けた形で指定します(例:/contact-form-7/v1 など)。

add_filter('rest_authentication_errors', function ($result) {
  if (true === $result || is_wp_error($result)) {
    return $result;
  }

  // 編集権限があるユーザーは許可(管理画面の機能を使えるように)
  if (current_user_can('edit_posts') || current_user_can('edit_pages')) {
    return $result;
  }

  // REST API リクエストルートを取得
  global $wp;
  // /wp-json/ 以降のパスが取れる(例: /contact-form-7/v1 や /wp/v2/posts など)
  $rest_route = isset($wp->query_vars['rest_route']) ? $wp->query_vars['rest_route'] : '';

  // 許可するルート
  $allowed_routes = [
    '/oembed/1.0',                 // Embed 用
    '/contact-form-7/v1',          // Contact Form 7 用
    '/akismet/v1',                 // Akismet 用
    '/my-theme-api/v1',            // カスタムエンドポイント 用
  ];

  // 対象のルートは許可
  foreach ($allowed_routes as $allowed_route) {
    if (strpos($rest_route, $allowed_route) === 0) {
      return $result; // 許可対象ならOK
    }
  }

  // それ以外はエラーを返す
  return new WP_Error(
    'rest_not_logged_in',
    __('You are not currently logged in.', 'text-domain'),
    array('status' => 401)
  );
});
query_vars['rest_route']

$wp->query_vars は、現在のリクエストに関するパラメータ(URLの解析結果)をまとめた配列です。

REST API へのアクセス時には、rest_route キーにリクエストされたAPIパスが格納され、 $wp->query_vars['rest_route'] を使って、「REST APIへのアクセスかどうか」や「どのエンドポイントにアクセスしているか」を判定できます。

例えば、ブラウザで https://example.com/wp-json/wp/v2/posts にアクセスした場合、$wp->query_vars['rest_route'] は /wp/v2/postsになります。

この値を見ることで、リクエストが「どのAPIに対して行われたものか」を判定できるので、特定のエンドポイントだけを許可したい場合に役立ちます。

rest_authentication_errors フィルターは、リクエスト全体に対してかかるため、リクエスト先(ルート)を $wp->query_vars['rest_route'] を使って判別し、「Contact Form 7」や「Akismet」などのリクエスト(/contact-form-7/v1 や /akismet/v1 など)は特別に許可するようにしています。

strpos() は、ある文字列の中に、特定の文字列が「どこに出てくるか」を探す関数です。

ここでは $rest_route(リクエストされたAPIのパス)に、$allowed_route(許可したいパス)が先頭(位置0)から一致しているかをチェックしています(先頭一致だけを許可して、それ以外はNG)。

rest_authentication_errors の時点では $request オブジェクトはまだ無いので、ルートの取得では $request->get_route() は使えないため、$wp->query_vars などを使います。

動作確認時の注意点

このページの例のコードでは、基本的にユーザー(または編集権限のあるユーザー)としてログインしている場合は、REST API のアクセスをすべて許可しています。

ログインしているかどうかで結果が変わる場合があるので、コードの動作を確認する際は注意が必要です。

例えば、アクセスできないはずがアクセスできてしまう場合は、ログインしているのが理由という可能性があります。これを理解していないと、わけがわからなくなってしまうかも知れません(自分がそうでした)。

プラグインの namespace

REST API を利用しているプラグインの名前空間(namespace)は「サイトのURL/wp-json/」でアクセスして表示されるデータの namespaces で確認できます。

必要に応じて許可するプラグインを、許可するルート $allowed_routes(上記17-22行目部分)に(先頭にスラッシュを付けて)追加します。また、不要なものは削除します。

rest_pre_dispatch を利用

以下は、rest_pre_dispatch フィルターを使って、引数 $request の get_route() メソッドをで、ルート判定して特定 API(プラグイン)だけ例外許可する例です。

前述の rest_authentication_errors を使ったコード同様、特定のプラグイン(oEmbed, Contact Form 7, Akismet)だけ許可し、あとは制限(エラー)します。許可の基準は namespace になるので先頭のスラッシュなしで指定します。

// 編集権限のあるユーザー及び指定したプラグインに REST API を許可(それ以外は無効にする)
add_filter('rest_pre_dispatch', function ($result, $server, $request) {
  // 念のため、リクエストオブジェクトの型チェック ( WP_REST_Request 以外なら処理しない )※省略可能
  if (! ($request instanceof WP_REST_Request)) {
    return $result;
  }

  // 投稿・固定ページの編集権限があるユーザーなら許可(例: Gutenberg使用者)
  if (current_user_can('edit_posts') || current_user_can('edit_pages')) {
    return $result;
  }

  // ルート取得(先頭スラッシュを除去)
  $route = ltrim($request->get_route(), '/');

  // ルートが空の場合は REST API を無効化
  if (empty($route)) {
    return new WP_Error(
      'rest_disabled',
      __('The REST API on this site has been disabled.', 'text-domain'),
      array('status' => 403)
    );
  }

  // スラッシュでルートを分割して namespace を抽出(例: "contact-form-7/v1")
  $parts = explode('/', $route);
  $namespace = isset($parts[0]) && isset($parts[1]) ? $parts[0] . '/' . $parts[1] : '';

  // 許可する namespace 一覧(oEmbed、Contact Form 7、Akismet)
  $permitted_namespaces = [
    'oembed/1.0',          // Embed 用
    'contact-form-7/v1',   // Contact Form 7 用
    'akismet/v1',          // Akismet 用
    'my-theme-api/v1',     // カスタムエンドポイント 用
  ];

  // 許可リストに含まれていれば通過
  if (in_array($namespace, $permitted_namespaces, true)) {
    return $result;
  }

  // それ以外は REST API を無効化(ステータス 403)
  return new WP_Error(
    'rest_disabled',
    __('The REST API on this site has been disabled.', 'text-domain'),
    array('status' => 403)
  );
}, 10, 3);
$request->get_route() とネームスペース判定

$request は、REST API リクエストの詳細情報を持ったオブジェクト(WP_REST_Request)です。

$request の get_route() メソッドを使うと、リクエストされたエンドポイントのパスを取得できます。(例:/contact-form-7/v1/forms/123など)

取得したパスから、先頭の部分(たとえばcontact-form-7/v1)を抜き出して、「これは許可していいネームスペースか?」を判定しています。

具体的な処理の流れ

  1. リクエストのルート(パス)を取得する
    → $request->get_route() (例:/contact-form-7/v1/forms/123)
  2. 先頭スラッシュを除去して、スラッシュ / で分割する
    → 最初の2つ (contact-form-7 と v1) をくっつけて、contact-form-7/v1 という「ネームスペース」を作る。
  3. 許可リストにこのネームスペースが含まれているか確認する
    → 含まれていれば許可、含まれていなければ403エラー。

つまり、$request->get_route() でルートを取得し、先頭部分(ネームスペース)を抜き出して、許可リストと照らし合わせてアクセスを制御しています。

ltrim($route, '/')

get_route() で取得できるルートは、たとえば /contact-form-7/v1/forms/123 のように、先頭に必ずスラッシュ / がついています。

このままだと後続の explode() で、配列の最初に空文字('')が入ってしまい、意図通りネームスペースを取り出せなくなるので、ltrim() で先頭のスラッシュを取り除き、正しく「contact-form-7」「v1」などが取得できるようにしています。

explode('/', $route)

explode() を使ってスラッシュで分割すると、「ネームスペース」「バージョン」「リソース」「ID」…という形で、パスの構造が要素ごとに分かれます。

例えば、explode('/', 'contact-form-7/v1/forms/123'); の結果は ['contact-form-7', 'v1', 'forms', '123'] のようになります。このうち最初のふたつ ['contact-form-7', 'v1'] を結合して contact-form-7/v1 にすることで、「どのネームスペースか(どのプラグインか)」を判定できます。

REST API は通常「ネームスペース/エンドポイント」という構成なので、最初の2階層だけ見れば大まかな判定ができます。

特定のドメインのみ許可

以下は、Embed、Contact Form 7、Akismet はドメインに関係なく許可し、/my-theme-api/v1 や /wp/v2 のルートは特定のドメインのみ許可する例です(編集権限のあるユーザーも許可しています)。

特定ドメインのみ許可するようにするには、$_SERVER['HTTP_ORIGIN'] を使って、リクエストヘッダーの Origin を確認し、そこに「許可するドメイン」が含まれているかチェックします。

Embed、Contact Form 7、Akismet のルートは Origin に関係なく許可し、/my-theme-api/v1 や /wp/v2 のルートは 特定の Origin のみ許可するので、ルートを分けて定義しています。

add_filter('rest_authentication_errors', function ($result) {
  if (true === $result || is_wp_error($result)) {
    return $result;
  }

  // 投稿・固定ページの編集権限があるユーザーなら許可
  if (current_user_can('edit_posts') || current_user_can('edit_pages')) {
    return $result;
  }

  global $wp;
  // REST API リクエストルートを取得
  $rest_route = isset($wp->query_vars['rest_route']) ? $wp->query_vars['rest_route'] : '';

  // それぞれの目的に応じたルートを分けて定義
  // 常に許可されるルート(Embed、Contact Form 7、Akismet)
  $always_allowed_routes = [
    '/oembed/1.0',
    '/contact-form-7/v1',
    '/akismet/v1',
  ];

  // 制限するルート(カスタムエンドポイントと標準のエンドポイント)
  $restricted_routes = [
    '/my-theme-api/v1',
    '/wp/v2',
  ];

  // 許可するドメイン
  $allowed_origins = [
    'https://example.com',
    'https://subdomain.example.com',
    'http://example.localhost:5501',
  ];

  // リクエストヘッダーの Origin を確認(クロスオリジンの時に送られてくる)
  $origin = isset($_SERVER['HTTP_ORIGIN']) ? $_SERVER['HTTP_ORIGIN'] : '';

  // まずは常に許可されるルートのチェック
  foreach ($always_allowed_routes as $allowed_route) {
    if (strpos($rest_route, $allowed_route) === 0) {
      return $result; // Origin に関係なく許可
    }
  }

  // 次に Origin 制限があるルートのチェック
  foreach ($restricted_routes as $restricted_route) {
    if (strpos($rest_route, $restricted_route) === 0) {
      // $origin が空でなく、かつ許可リストに含まれていれば許可
      if (!empty($origin) && in_array($origin, $allowed_origins, true)) {
        return $result; // 特定の Origin のみ許可
      } else {
        return new WP_Error(
          'rest_forbidden_origin',
          __('Origin not allowed for this endpoint.', 'text-domain'),
          array('status' => 403)
        );
      }
    }
  }

  // それ以外は拒否
  return new WP_Error(
    'rest_not_logged_in',
    __('You are not currently logged in.', 'text-domain'),
    array('status' => 401)
  );
});

HTTP_ORIGIN が空になるケース

通常、クロスオリジン(別ドメイン)リクエスト の場合は、Origin ヘッダーありますが、同一オリジンの普通のリクエストの場合、Origin ヘッダーがないので、HTTP_ORIGIN が空になります。

以下は HTTP_ORIGIN が空になる代表的なケースです。

ケース 説明
通常のブラウザ内アクセス(ページ表示) GET リクエストなど単純なナビゲーションでは、基本 Origin ヘッダーは付かないことが多い。
サーバーサイドからのリクエスト 例: wp_remote_post()、curl コマンド、PHPの file_get_contents() などは普通 Origin を付けない。
JavaScript の同一オリジンからのリクエスト 同じドメインからの fetch() や XMLHttpRequest() で特別なヘッダーを付けなければ、Origin が省略される場合がある。
一部古いブラウザや特殊なツール 古いブラウザや開発ツール、Botなどでは Origin が無いこともある。

同一ドメインからの fetch()

同一オリジン(Same-Origin) からの fetch() リクエストでは通常、HTTP_ORIGIN ヘッダーは 送信されないため、サーバー側で $_SERVER['HTTP_ORIGIN'] をチェックしていると、空の値('')になるので、不許可になるという問題が起こります。

同一ドメインからの fetch() を許可するには、前述のコードの50行目を以下の4行目ように $origin が空、つまり同一オリジンとみなせる場合も「許可」とするように変更します。

foreach ($restricted_routes as $restricted_route) {
  if (strpos($rest_route, $restricted_route) === 0) {
    // Origin が空(same-origin)または許可された Origin の場合は許可
    if (empty($origin) || in_array($origin, $allowed_origins, true)) {
      return $result; // 特定の Origin と同一オリジンのみ許可
    } else {
      return new WP_Error(
        'rest_forbidden_origin',
        __('Origin not allowed for this endpoint.', 'text-domain'),
        array('status' => 403)
      );
    }
  }
}
リファラ(Referer)チェックを追加

Origin が空のときの補助的チェックとして、リファラチェックを追加することもできます。

但し、Referer はユーザーがブロックできるのと、簡単に偽装できるので、信頼性の高いセキュリティチェックにはなりません。

以下は $current_domain に自サイトのドメインを設定して、リファラと比較して判定する例です。

add_filter('rest_authentication_errors', function ($result) {
  if (true === $result || is_wp_error($result)) {
    return $result;
  }

  if (current_user_can('edit_posts') || current_user_can('edit_pages')) {
    return $result;
  }

  global $wp;
  $rest_route = isset($wp->query_vars['rest_route']) ? $wp->query_vars['rest_route'] : '';

  // それぞれの目的に応じたルートを分けて定義
  $always_allowed_routes = [
    '/oembed/1.0',
    '/contact-form-7/v1',
    '/akismet/v1',
  ];

  $restricted_routes = [
    '/my-theme-api/v1',
    '/wp/v2',
  ];

  $allowed_origins = [
    'https://example.com',
    'https://subdomain.example.com',
    'http://example.localhost:5501',
  ];

  $origin = isset($_SERVER['HTTP_ORIGIN']) ? $_SERVER['HTTP_ORIGIN'] : '';

  // まずは常に許可されるルートのチェック
  foreach ($always_allowed_routes as $allowed_route) {
    if (strpos($rest_route, $allowed_route) === 0) {
      return $result; // Origin に関係なく許可
    }
  }

  // リファラ(Referer)を取得
  $referer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '';
  $referer_origin = '';
  $is_same_domain = false;
  // 自サイトのドメイン
  $current_domain = 'https://www.mydomain.com';

  // Referer のドメイン抽出(ドメイン部分だけを取り出す処理)と比較
  if (!empty($referer)) {
    $parsed_url = parse_url($referer);
    if (isset($parsed_url['scheme'], $parsed_url['host'])) {
      $referer_origin = $parsed_url['scheme'] . '://' . $parsed_url['host'];
      if ($referer_origin === $current_domain) {
        // リファラとドメインの値が一致した場合
        $is_same_domain = true;
      }
    }
  }

  // 制限付きのルート判定
  foreach ($restricted_routes as $restricted_route) {
    if (strpos($rest_route, $restricted_route) === 0) {
      // 許可されたドメイン(ORIGIN)
      $origin_allowed = !empty($origin) && in_array($origin, $allowed_origins, true);
      // 同一ドメイン
      $same_origin_allowed = empty($origin) && $is_same_domain;
      // 許可されたドメインまたは同一ドメインの場合は許可
      if ($origin_allowed || $same_origin_allowed) {
        return $result;
      } else {
        return new WP_Error(
          'rest_forbidden_origin',
          __('Origin not allowed for this endpoint.', 'text-domain'),
          array('status' => 403)
        );
      }
    }
  }

  // それ以外は拒否
  return new WP_Error(
    'rest_not_logged_in',
    __('You are not currently logged in.', 'text-domain'),
    array('status' => 401)
  );
});

カスタムヘッダー(X-API-Key)で制御

以下は特定の WordPress REST API エンドポイントに対して、カスタムヘッダー(X-API-Key)を使ってアクセス制限をかける例です。X-API-Key ヘッダーに指定された API キーの値でアクセスを制御します。

どのフィルターを使うかやその組み合わせにより、以下のようないくつかの方法があります。

  • rest_authentication_errors と rest_pre_dispatch を使う
  • rest_authentication_errors と rest_request_before_callbacks フィルターを使う
  • rest_pre_dispatch のみで実装
  • rest_authentication_errors のみで実装

最初の例は rest_authentication_errors と rest_pre_dispatch を使う方法です。

この方法は、認証エラーを最初に処理し、その後のリクエスト処理で追加の制限をかけることができるので、精度の高いアクセス制御が可能です。また、複数のエンドポイントやネームスペースごとに異なるアクセス制限を設けることができるため、後々の拡張やメンテナンスがしやすいです。

まず、rest_authentication_errors を使ってサイトが正しく機能するようにログイン中で編集権限があるユーザーと必要なルートを許可します。このとき、 API キーをチェックするルートも許可しておきます。

そして rest_pre_dispatch を使って特定のルートに対して API キーのチェックを行います。

以下は rest_authentication_errors フィルターの部分です。内容的には「特定のプラグインだけ許可」と同じです。

add_filter('rest_authentication_errors', function ($result) {
  if (true === $result || is_wp_error($result)) {
    return $result;
  }

  // 編集権限があるユーザーは許可(管理画面の機能を使えるように)
  if (current_user_can('edit_posts') || current_user_can('edit_pages')) {
    return $result;
  }

  // REST API リクエストルートを取得
  global $wp;
  // /wp-json/ 以降のパスが取れる(例: /contact-form-7/v1 や /wp/v2/posts など)
  $rest_route = isset($wp->query_vars['rest_route']) ? $wp->query_vars['rest_route'] : '';

  // 許可するルート
  $allowed_routes = [
    '/oembed/1.0',                 // Embed 用
    '/contact-form-7/v1',          // Contact Form 7 用
    '/akismet/v1',                 // Akismet 用
    '/my-theme-api/v1',            // カスタムエンドポイント用(別途 API キーの値で制御)
    '/wp/v2',  // WordPress のエンドポイント用(別途API キーの値で制御)
  ];

  // 対象のルートは許可
  foreach ($allowed_routes as $allowed_route) {
    if (strpos($rest_route, $allowed_route) === 0) {
      return $result; // 許可対象ならOK
    }
  }

  // それ以外はエラーを返す
  return new WP_Error(
    'rest_not_logged_in',
    __('You are not currently logged in.', 'text-domain'),
    array('status' => 401)
  );
});

rest_pre_dispatch フィルターでは、特定のルート(正確には namespace)のリクエストヘッダーで API キーをチェックして、値が正しければ許可します。

リクエストヘッダーの取得では、まず getallheaders() が使えれば(環境によっては存在しないので)それを使って取得し、定義されていない場合は $_SERVER から取得します。

また、セキュリティ上 API キーを直接コードにハードコーディングするのは避けたいので、別途 wp-config.php に定義しておき、defined() で定義されているかチェックしてから constant() で取り出しています。

add_filter('rest_pre_dispatch', function ($result, $server, $request) {
  // ルート取得(先頭スラッシュを除去)
  $route = ltrim($request->get_route(), '/');

  // スラッシュでルートを分割して namespace を抽出
  $parts = explode('/', $route);
  $namespace = isset($parts[0]) && isset($parts[1]) ? $parts[0] . '/' . $parts[1] : '';

  // API キーを確認する namespace 一覧
  $permitted_namespaces = [
    'my-theme-api/v1',     // カスタムエンドポイント
    'wp/v2',  // WordPress のエンドポイント
  ];

  // リストに含まれていればヘッダーから API キーを取得してチェック
  if (in_array($namespace, $permitted_namespaces, true)) {
    // getallheaders が使えれば(存在すれば)取得して $headers に代入し、使えなければ空の配列を代入しておく
    $headers = function_exists('getallheaders') ? getallheaders() : [];

    $api_key = ''; // API キーの値を初期化

    // まず getallheaders() で取得
    if (isset($headers['X-API-Key'])) {
      $api_key = $headers['X-API-Key'];
    }
    // getallheaders() で取れなかったら $_SERVER 経由で取得(環境によってはこちらしか使えないことも)
    elseif (isset($_SERVER['HTTP_X_API_KEY'])) {
      $api_key = $_SERVER['HTTP_X_API_KEY'];
    }

    // wp-config.php に定義した API キーを取得
    $my_api_key = defined('MY_API_KEY') ? constant('MY_API_KEY') : '';

    // API キーをチェック
    if ($api_key !== $my_api_key) {
      // API キーが正しくなければエラーにする
      return new WP_Error(
        'rest_forbidden',
        __('Invalid or missing API key.', 'text-domain'),
        array('status' => 403)
      );
    }
  }

  return $result;
}, 10, 3);

以下は wp-config.php に API キーを定義する例です。

/* カスタム値は、この行と「編集が必要なのはここまでです」の行の間に追加してください。 */

define('MY_API_KEY', 'abc123def456');  // API キー(実際には複雑な値を設定)

/* 編集が必要なのはここまでです ! WordPress でのパブリッシングをお楽しみください。 */

rest_request_before_callbacks フィルターを使う

前述の rest_pre_dispatch フィルターのコードの代わりに、rest_request_before_callbacks フィルターを使って記述することもできます。

rest_authentication_errors のコードも別途必要です。

rest_request_before_callbacks は、WordPress REST API の実行前(ルートが確定した後)にリクエスト情報をフックして処理することができるフィルターです。

add_filter('rest_request_before_callbacks', function ($response, $handler, $request) {
  // ルートを取得し、先頭スラッシュを除去
  $route = ltrim($request->get_route(), '/');

  // ルートを分割して namespace を取得
  $parts = explode('/', $route);
  $namespace = isset($parts[0], $parts[1]) ? $parts[0] . '/' . $parts[1] : '';

  // 許可する namespace 一覧
  $allowed_namespaces = [
    'wp/v2',
    'my-theme-api/v1',
  ];

  $my_api_key = defined('MY_API_KEY') ? constant('MY_API_KEY') : '';

  // namespace が許可リストに含まれる場合、APIキー認証を実施
  if (in_array($namespace, $allowed_namespaces, true)) {
    $headers = function_exists('getallheaders') ? getallheaders() : [];
    $api_key = $headers['X-API-Key'] ?? ($_SERVER['HTTP_X_API_KEY'] ?? '');

    if ($api_key !== $my_api_key) {
      return new WP_Error(
        'rest_forbidden',
        __('Invalid or missing API key.', 'text-domain'),
        array('status' => 403)
      );
    }
  }

  return $response;
}, 10, 3);

以下の API キー取得部分のコードは

 $api_key = $headers['X-API-Key'] ?? ($_SERVER['HTTP_X_API_KEY'] ?? '');

以下と同じことです。上記では、??(ヌル合体演算子)を使って、$headers['X-API-Key'] が存在すればそれを使い、無ければ $_SERVER['HTTP_X_API_KEY'] を使い、それも無ければ空文字 '' を代入します。

$api_key = '';

// まず getallheaders() で取得
if (isset($headers['X-API-Key'])) {
  $api_key = $headers['X-API-Key'];
}
// getallheaders() で取れなかったら $_SERVER 経由で取得(環境によってはこちらしか使えないことも)
elseif (isset($_SERVER['HTTP_X_API_KEY'])) {
  $api_key = $_SERVER['HTTP_X_API_KEY'];
}

rest_pre_dispatch のみで実装

以下は rest_pre_dispatch フィルターのみを使う例です。

この例のようにシンプルな場合は、一箇所で完結できるのでアクセス権限・ルーティング・ヘッダー認証すべてが集約され、可読性が高くなります。

但し、rest_pre_dispatch はリクエストがディスパッチされる前に検証を行うため、エラー処理が早期に終わる反面、条件によっては処理が行われる前にフィルターで排除されることがあります。

また、他のフィルターに比べて、細かな認証やアクセス制御がやや難しくなる場合があります(例: 特定のユーザー権限を確認するなどの処理)。

add_filter('rest_pre_dispatch', function ($result, $server, $request) {

  // 投稿・固定ページの編集権限があるユーザーなら許可(例: Gutenberg使用者)
  if (current_user_can('edit_posts') || current_user_can('edit_pages')) {
    return $result;
  }

  // ルート取得(先頭スラッシュを除去)
  $route = ltrim($request->get_route(), '/');

  // ルートが空の場合は REST API を無効化
  if (empty($route)) {
    return new WP_Error(
      'rest_disabled',
      __('The REST API on this site has been disabled.', 'text-domain'),
      array('status' => 403)
    );
  }

  // スラッシュでルートを分割して namespace を抽出(例: "contact-form-7/v1")
  $parts = explode('/', $route);
  $namespace = isset($parts[0]) && isset($parts[1]) ? $parts[0] . '/' . $parts[1] : '';

  // 許可する namespace 一覧(oEmbed、Contact Form 7、Akismet)
  $permitted_namespaces = [
    'oembed/1.0',          // Embed 用
    'contact-form-7/v1',   // Contact Form 7 用
    'akismet/v1',          // Akismet 用
  ];

  // 許可リストに含まれていれば通過
  if (in_array($namespace, $permitted_namespaces, true)) {
    return $result;
  }

  // API キーを確認するルートの namespace
  $header_required_namespaces = [
    'my-theme-api/v1',     // カスタムエンドポイント 用
    'wp/v2',  // WordPress のエンドポイント用
  ];

  // リストに含まれていればヘッダーから API キーを取得してチェック
  if (in_array($namespace, $header_required_namespaces, true)) {
    // getallheaders が使えれば(存在すれば)
    $headers = function_exists('getallheaders') ? getallheaders() : [];

    $api_key = $headers['X-API-Key'] ?? ($_SERVER['HTTP_X_API_KEY'] ?? '');

    // wp-config.php に定義した API キーを取得
    $my_api_key = defined('MY_API_KEY') ? constant('MY_API_KEY') : '';

    // API キーをチェック
    if ($api_key === $my_api_key) {
      // API キーが正しければ許可
      return $result;
    }
  }

  // それ以外は REST API を無効化(ステータス 403)
  return new WP_Error(
    'rest_disabled',
    __('The REST API on this site has been disabled.', 'text-domain'),
    array('status' => 403)
  );
}, 10, 3);

rest_authentication_errors のみで実装

以下は rest_authentication_errors フィルターのみを使う例です。

前述の rest_pre_dispatch フィルターのみを使う例と同様、API キーやユーザー権限を含むすべてのアクセス制限を1つのフィルター内で管理できるので、コードが簡潔になります。

また、rest_authentication_errors フィルターのみでアクセス制御を行うので、シンプルでパフォーマンス的にも軽い実装です。

但し、特定のエンドポイントに対して、より詳細な制御を行いたい場合には不十分で、基本的な制限しか設定できないのと、認証エラーの時点で処理が終了するため、rest_pre_dispatch のようにリクエストが進んでから細かい制御を行うことができません。

add_filter('rest_authentication_errors', function ($result) {
  if (true === $result || is_wp_error($result)) {
    return $result;
  }

  // 編集権限があるユーザーは許可(管理画面の機能を使えるように)
  if (current_user_can('edit_posts') || current_user_can('edit_pages')) {
    return $result;
  }

  // REST API リクエストルートを取得
  global $wp;
  // /wp-json/ 以降のパスが取れる(例: /contact-form-7/v1 や /wp/v2/posts など)
  $rest_route = isset($wp->query_vars['rest_route']) ? $wp->query_vars['rest_route'] : '';

  // 許可するルート
  $allowed_routes = [
    '/oembed/1.0',                 // Embed 用
    '/contact-form-7/v1',          // Contact Form 7 用
    '/akismet/v1',                 // Akismet 用
  ];

  // 対象のルートは許可
  foreach ($allowed_routes as $allowed_route) {
    if (strpos($rest_route, $allowed_route) === 0) {
      return $result; // 許可対象ならOK
    }
  }

  // API キーを確認するルート
  $header_required_routes = [
    '/my-theme-api/v1',   // カスタムエンドポイント
    '/wp/v2',             // WordPress のエンドポイント
  ];

  // リストに含まれていればヘッダーから API キーを取得してチェック
  if (in_array($rest_route, $header_required_routes, true)) {
    // getallheaders が使えれば(存在すれば)
    $headers = function_exists('getallheaders') ? getallheaders() : [];

    $api_key = $headers['X-API-Key'] ?? ($_SERVER['HTTP_X_API_KEY'] ?? '');

    // wp-config.php に定義した API キーを取得
    $my_api_key = defined('MY_API_KEY') ? constant('MY_API_KEY') : '';

    // API キーをチェック
    if ($api_key === $my_api_key) {
      // API キーが正しければ許可
      return $result;
    }
  }

  // それ以外はエラーを返す
  return new WP_Error(
    'rest_not_logged_in',
    __('You are not currently logged in.', 'text-domain'),
    array('status' => 401)
  );
});

以下はそれぞれの方法の比較です。

比較対象 長所 短所
rest_authentication_errors と rest_pre_dispatch 高度なアクセス制御が可能。エラー処理の2段階チェック。 実装が複雑。
rest_authentication_errors と rest_request_before_callbacks 処理前にリクエストを早期排除。パフォーマンス向上。 柔軟性が低い。デバッグが難しい。
rest_pre_dispatch のみ シンプルで直感的。実装が簡単。 認証チェックのタイミングが難しい。アクセス制御が粗い。
rest_authentication_errors のみ シンプルで軽量。1箇所で制御できる。 柔軟性に欠け、特定の条件で制御が難しい。

どの方法を使う?

以下の理由から最初の例の rest_authentication_errors と rest_pre_dispatch を使う方法が良いかと。

  1. 柔軟性
    • rest_authentication_errors で認証エラーを早期に処理し、rest_pre_dispatch でリクエストが処理される直前にアクセス制限をかけることができ、より精密な制御が可能。
    • 特定のエンドポイントやネームスペースごとに異なるアクセス制御を行うことができ柔軟。
  2. 拡張性
    • 特定のAPIルートやネームスペースに対するアクセス制限を細かく制御できるので、将来的に新しいエンドポイントを追加した場合にも、簡単に拡張が可能。
  3. セキュリティ
    • 2つのフィルターを組み合わせることで、認証エラーの段階で早期にリクエストを拒否し、その後のリクエスト処理に入る前にさらにセキュリティを強化することが可能。これにより、リクエストが意図しない方法で処理されるリスクを減少させます。
  4. パフォーマンス
    • フィルターフックが適切に使われていれば、パフォーマンスへの影響は最小限に抑えることが可能。最初に rest_authentication_errors でエラーを返すことで、不正なリクエストを早期に排除し、その後の無駄な処理を減らせます。

但し、シンプルなアクセス制限が求められる場合や、プロジェクトが小規模である場合には、rest_authentication_errors のみ(または rest_pre_dispatch のみ)で実装して簡単に制御する方法でも十分だと思います。これにより、実装がシンプルで管理が容易になります。

また、パフォーマンスを最優先にする場合は、リクエスト処理の前にエラーハンドリングを行い、不要な処理を防ぐことができる rest_request_before_callbacks の使用も選択肢です。ただし、柔軟性は少し犠牲になります。

Authorization ヘッダー版

以下は X-API-Key の代わりに Authorization ヘッダーを使用するように rest_pre_dispatch フィルターを書き換えたコードです。但し、認証機能などは設定していないので、本番環境用ではありません。

この例では Authorization: ApiKey {APIキー} のようなカスタムスキーム形式で送られてくることを前提にしています。そして preg_match('/ApiKey\s(.+)/', ...) で {APIキー} の部分のみを抽出しています。

add_filter('rest_pre_dispatch', function ($result, $server, $request) {
  // ルート取得(先頭スラッシュを除去)
  $route = ltrim($request->get_route(), '/');

  // スラッシュでルートを分割して namespace を抽出
  $parts = explode('/', $route);
  $namespace = isset($parts[0]) && isset($parts[1]) ? $parts[0] . '/' . $parts[1] : '';

  // API キーを確認する namespace 一覧
  $permitted_namespaces = [
    'my-theme-api/v1',     // カスタムエンドポイント
    'wp/v2',               // WordPress のエンドポイント
  ];

  // リストに含まれていれば Authorization ヘッダーを確認
  if (in_array($namespace, $permitted_namespaces, true)) {
    // getallheaders が使えれば(存在すれば)
    $headers = function_exists('getallheaders') ? getallheaders() : [];

    $auth_header = ''; // Authorization ヘッダーの値を初期化

    // まず getallheaders() で取得
    if (isset($headers['Authorization'])) {
      $auth_header = $headers['Authorization'];
    }
    // getallheaders() が使えない場合は $_SERVER 経由で取得(Apache/Nginx の場合)
    elseif (isset($_SERVER['HTTP_AUTHORIZATION'])) {
      $auth_header = $_SERVER['HTTP_AUTHORIZATION'];
    }

    // Authorization ヘッダー形式: "ApiKey {APIキー}" と想定
    $token = '';  // Authorization ヘッダーに指定された API キーの値(トークン)を初期化
    if (preg_match('/ApiKey\s(.+)/', $auth_header, $matches)) {
      $token = trim($matches[1]);
    }

    // wp-config.php に定義した API キー
    $my_api_key = defined('MY_API_KEY') ? constant('MY_API_KEY') : '';

    // トークンと API キーが一致するか
    if ($token !== $my_api_key) {
      return new WP_Error(
        'rest_forbidden',
        __('Invalid or missing Authorization token.', 'text-domain'),
        array('status' => 403)
      );
    }
  }

  return $result;
}, 10, 3);

X-API-Key と Authorization

X-API-Key と Authorization はどちらも HTTP ヘッダーに API キーなどの認証情報を載せる手段ですが、それぞれに意味と慣習上の違いがあります。

項目 X-API-Key Authorization
標準性 非標準(カスタムヘッダー) 標準ヘッダー。HTTP仕様で定義(RFC 7235)
目的 主にAPIキーのような簡易的認証用 トークン、Basic認証、Bearer認証など広範な認証(HTTP 認証)
構文 自由に決められる(例: X-API-Key: abc123) 規定の形式がある(例: Authorization: Bearer abc123)
プロキシやCDNとの互換性 時に無視されることがある(CDNやWAFでブロック対象になることも) より広く対応されている(OAuth2などで一般的)
複数の認証スキームの管理 難しい(単一の用途に固定) 複数の認証方式を切り替えられる(Basic, Bearer, Digestなど)
慣習 シンプルなAPIキー制御でよく使われる トークンベースの安全なAPIアクセス(JWTやOAuthなど)で一般的

使い分けの目安

  • シンプルなAPIキー制御だけが必要な場合: X-API-Key が手軽。
  • より標準的・拡張可能な設計をしたい(将来的にOAuthやJWT導入もありえる)場合: Authorization が望ましい。
  • セキュリティ対策やキャッシュ制御などにおいて信頼性が必要な場合: Authorization の方がCDNやリバースプロキシに正しく処理される可能性が高い。

セキュリティ面の補足

両者とも HTTPS を使わないと盗聴されるリスクがあります。

API キーを指定するフロントエンドサンプル

以下は投稿のエンドポイントに API キーを指定してアクセスするフロントエンドのサンプルです。

API キーを露出しないように、中継用の PHP ファイル(プロキシ)を作成して、fetch() からはプロキシ経由でエンドポイントにアクセスします。

以下は中継用の PHP ファイル(fetch-posts.php)です。

// config/api-config.php から API キー(定数 API_KEY)を読み込む
require_once __DIR__ . '/config/api-config.php';

// フロントエンドから送信された URL パラメータを取得(GET方式)
$url = $_GET['url'] ?? '';

// URL パラメータが未指定または空の場合は 400 Bad Request を返して終了
if (empty($url)) {
  http_response_code(400);
  header('Content-Type: application/json');
  echo json_encode(['error' => 'Missing URL']);
  exit;
}

// URL の妥当性をチェック
if (!filter_var($url, FILTER_VALIDATE_URL)) {
  http_response_code(400);
  header('Content-Type: application/json');
  echo json_encode(['error' => 'Invalid URL']);
  exit;
}

// cURL を使用して外部 API にリクエストを送信
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // レスポンスを文字列として取得
curl_setopt($ch, CURLOPT_HTTPHEADER, [
  'X-API-Key: ' . API_KEY,              // API キーをカスタムヘッダーに含める
  'Content-Type: application/json',
]);

$response = curl_exec($ch);                        // 実行
$curlError = curl_error($ch);                      // cURL エラー内容(失敗時)
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); // HTTPステータスコード取得
curl_close($ch);                                   // cURLセッションを終了

// JSON としてレスポンスを返す
header('Content-Type: application/json');
// HTTP ステータスをレスポンスに反映
http_response_code($httpCode ?: 500);

// エラーハンドリングとレスポンス出力
if ($response === false) {
  echo json_encode([
    'error' => 'cURL エラー',
    '詳細' => $curlError,
  ]);
} else {
  echo $response; // API からの生レスポンスをそのまま返す(必要に応じて整形可能)
}

API キーは以下の PHP ファイルで定義し、.htaccess でこのファイルへの直接アクセスを防ぎます。

<?php
define('API_KEY', 'abc123def456');

以下は JavaScript です。

中継用の PHP ファイル(fetch-posts.php)にアクセスして、パラメータとしてエンドポイントの URL とクエリパラメータを渡して結果を受け取ります。

// WordPress REST API カスタムエンドポイントのベース URL
const apiUrl = "https://example.com/wp/wp-json/wp/v2/posts";
// クエリパラメータを構築
const params = new URLSearchParams({
  _fields: "title,link", // 必要なフィールドのみ取得
  per_page: 5, // 1ページあたりの投稿数
}).toString();

// PHP 中継ファイルのエンドポイントに、URLパラメータを渡す
const proxyUrl = `api/fetch-posts.php?url=${ encodeURIComponent(`${apiUrl}?${params}`) }`;

// fetch() で中継ファイルにリクエストを送信
fetch(proxyUrl, {
  method: "GET",
})
  .then(async (response) => {
    // レスポンスヘッダーから Content-Type を取得
    const contentType = response.headers.get("Content-Type") || "";
    let errorData = null;
    // レスポンスが失敗している(ステータスコードが 200 番台以外)
    if (!response.ok) {
      if (contentType.includes("application/json")) {
        errorData = await response.json(); // JSON形式のエラーデータを取得
      } else {
        const text = await response.text(); // テキスト形式の場合
        errorData = { error: text };
      }
      // エラー内容を例外として投げる(catch で受ける)
      throw new Error(errorData.error || `HTTPエラー: ${response.status}`);
    }
    // 成功時は JSON データをパースして返す
    return response.json();
  })
  .then((posts) => {
    // 投稿データをコンソールに出力(実際の処理に応じて変更可能)
    console.log(posts);
  })
  .catch((error) => {
    console.warn("投稿の取得エラー:", error);
  });

詳細:fetch() で API キーを安全に扱う方法

CORS(クロスオリジン)制限

WordPress のデフォルト設定では、カスタムヘッダー付きの外部リクエストに対して CORS ヘッダーを返さないため、ブラウザによってブロックされる可能性があります。

外部から fetch() などで header を指定して WordPress REST API にリクエストを送ること自体は可能ですが、サーバー側(WordPress 側)が CORS(Cross-Origin Resource Sharing)を許可していなければできません。

特にカスタムヘッダー(例 X-API-KEY)や Authorization ヘッダーを送る場合は、事前に OPTIONS リクエスト(プリフライトリクエスト)が自動的に送られます。

そのとき、サーバーが適切に CORS ヘッダーを返さないと、ブラウザ側でエラー(Blocked by CORS policy)になって通信できないという仕組みです。

そのため、外部から fetch() で header を指定して WordPress REST API にリクエストを送れるようにするには、サーバー側(WordPress)で対応する必要があります。

基本的な仕組み

  • ブラウザには同一オリジンポリシー(Same-Origin Policy)というセキュリティ制限があり、異なるドメイン間の通信には厳しい制約が課されています。
  • 外部ドメインに対してカスタムヘッダー(例:X-API-Key や Authorization)を含めてリクエストを送ると、ブラウザは自動的にプリフライトリクエスト(OPTIONSメソッド)を送信します。

WordPress のデフォルトの挙動

  • WordPress は初期状態では CORS ヘッダーを返しません。
  • そのため、外部ドメインから fetch() でリクエストを送っても、プリフライトリクエストに対して適切なレスポンスが返されず、ブラウザが通信をブロックします(例:Blocked by CORS policy)。

影響を受けるケース

以下のようなヘッダーを含めたリクエストを行う場合は、CORS 対応が必須です。

  • Authorization(例:Bearer トークン)
  • X-API-Key などのカスタムヘッダー
  • Content-Type: application/json(POST や PUT)

サーバー側(WordPress)での対応方法

  • Access-Control-Allow-Origin でフロントエンド側ドメインを許可する
  • Access-Control-Allow-Methods で使用する HTTP メソッドを列挙する
  • Access-Control-Allow-Headers で使用するヘッダー名を列挙する
  • OPTIONS リクエストに正しくレスポンスする

特定のドメインを許可する

特定のドメインから fetch() などで header を指定して WordPress REST API にリクエストできるようにするには、以下を functions.php に記述します。許可するオリジン(ドメイン)の部分は適宜変更します。

add_filter('rest_pre_serve_request', function ($served, $result, $request, $server) {
  // 許可するオリジンのリスト(複数対応可)
  $allowed_origins = [
    'http://example.com',
    'https://example.com',
    'http://example.localhost:5501',
  ];
  // リクエストヘッダーから Origin を取得
  $origin = $_SERVER['HTTP_ORIGIN'] ?? '';
  // リクエスト元の Origin が許可リストに含まれているか確認
  $is_allowed = in_array($origin, $allowed_origins, true);

  // プリフライトリクエスト対応(OPTIONS の場合は、先に CORS ヘッダーを設定して即応答)
  if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    if ($is_allowed) {
      header("Access-Control-Allow-Origin: {$origin}"); // 許可するオリジン(ドメイン)
      header("Access-Control-Allow-Methods: GET, POST, OPTIONS"); // 許可する HTTPメソッド
      header("Access-Control-Allow-Headers: Authorization, X-API-KEY, Content-Type"); // 含めてもよいヘッダー
      header("Access-Control-Allow-Credentials: true");// Cookie や認証情報を送信可能にする(必要に応じて)
      header("Access-Control-Max-Age: 86400"); // プリフライト結果のキャッシュ期間(秒)最大1日キャッシュ
      header("Vary: Origin"); // キャッシュの誤適用を防止
    }
    // 許可されていないオリジンからのOPTIONSリクエストにはCORSヘッダーを付けずに空レスポンスを返す
    header('Content-Length: 0');
    header('Content-Type: text/plain');  // 省略可能
    return true; // WordPressに「レスポンスは処理済み」と知らせる(true を返す)
  }

  // 通常のCORS付きリクエスト(GET/POSTなど)に対するヘッダー設定
  if ($is_allowed) {
    header("Access-Control-Allow-Origin: {$origin}");
    header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
    header("Access-Control-Allow-Headers: Authorization, X-API-KEY, Content-Type");
    header("Access-Control-Allow-Credentials: true");
    header("Access-Control-Max-Age: 86400");
    header("Vary: Origin");
  }

  return $served; // 通常のレスポンスは WordPress に任せる
}, 10, 4);

rest_pre_serve_request は、実際にREST APIのデータ(JSONレスポンス)がブラウザに送られる直前に呼ばれるフックです。

Access-Control-Allow-Origin MDN
  • 外部からアクセスを許可するオリジン(ドメイン)を指定します
  • ワイルドカード * を使用すると、すべてのオリジンからのリクエストを許可しますが、セキュリティ上の注意が必要です。
  • このヘッダーが適切に設定されていないと、ブラウザはクロスオリジンのリクエストをブロックします。
Access-Control-Allow-Methods MDN
  • 外部から許可する HTTPメソッド(GET、POSTなど)を指定します。
  • ここに書かれていないメソッドでリクエストすると、ブラウザ側で拒否されます。
Access-Control-Allow-Headers MDN
  • リクエストに含めてもよいヘッダーを指定します。
  • ブラウザがデフォルトで許可していないヘッダーは、ここで許可しないと送れません。
Access-Control-Allow-Credentials MDN
  • クッキーや認証情報を使う場合は true を指定し、クライアント側では credentials: 'include' を指定します。true を指定すると、このレスポンスは認証情報(Cookie、Authorization ヘッダーなど)付きのリクエストにも対応することを意味します。
  • クライアント(fetch() や XMLHttpRequest)が credentials: 'include' を明示しない限り、ブラウザは Cookie(認証情報)を送信しないので効果はありません。
Access-Control-Max-Age MDN
  • プリフライトリクエスト(OPTIONS)の結果をブラウザにどれくらいキャッシュさせるかを秒数で指定します。
  • これにより、同じリクエストに対してプリフライトリクエストを繰り返す必要がなくなり、パフォーマンスが向上します。
Vary MDN
  • Vary ヘッダーは、キャッシュの挙動を制御するためのヘッダーです。
  • Vary: Origin を指定することで、Origin ごとにキャッシュを分けて管理してくれるようになります(間違ったキャッシュが使われるのを防止できます)。
独自に認証処理をしている場合

rest_authentication_errors などを使って独自に認証処理をしている場合は、前述の方法では CORS プリフライトブロックを回避できません。

独自に認証処理をしているケースでの CORS プリフライトブロック回避策としては、rest_api_init フックを使う方法があります。

rest_api_init は、WordPress REST API へのリクエストが行われたときだけ実行されるフックです。具体的には、URL が /wp-json/ から始まる API リクエストのときに、WordPress が REST API の初期化処理を行う段階で呼び出されます。

主な用途としては以下のようなものがあります。

以下は、rest_api_init の早い段階で OPTIONS リクエストを処理して終了することで、CORS ブロックを防ぐ例です。

REST API へのリクエスト時、HTTPメソッドが OPTIONS の場合、それは多くの場合、ブラウザによるプリフライトリクエストです。この場合、必要な CORS ヘッダー(Access-Control-Allow-Origin や Allow-Methods など)を返した上で、即座に exit することで、プリフライトチェックに正しく応答できます。

これにより、実際の API リクエストがブラウザによってブロックされるのを防げます。

OPTIONS リクエストに対して早期に exit することで、他の rest_api_init コールバックや一部の REST フックがスキップされる可能性はありますが、OPTIONS メソッドのみに限定して exit を使っているため、通常の API 実行(GET, POST など)には影響しないので、大きな問題が生じることはほぼありません。

// プリフライトリクエスト対応(OPTIONS)
add_action('rest_api_init', function () {
  if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    // 許可するオリジンのリスト(複数対応可)
    $allowed_origins = [
      'http://example.com',
      'https://example.com',
      'http://example.localhost:5501',
    ];

    // リクエストヘッダーから Origin を取得
    $origin = $_SERVER['HTTP_ORIGIN'] ?? '';

    if (in_array($origin, $allowed_origins, true)) {
      header("Access-Control-Allow-Origin: {$origin}"); // 許可するオリジン(ドメイン)
      header("Access-Control-Allow-Methods: GET, POST, OPTIONS"); // 許可する HTTPメソッド
      header("Access-Control-Allow-Headers: Authorization, X-API-KEY, Content-Type"); // 含めてもよいヘッダー
      header("Access-Control-Allow-Credentials: true"); // Cookie や認証情報を送信可能にする(必要に応じて)
      header("Access-Control-Max-Age: 86400"); // プリフライト結果のキャッシュ期間(秒)最大1日キャッシュ
      header("Vary: Origin"); // キャッシュの誤適用を防止
      exit;
    }
  }
});

CORS の制限を受けないケース

CORS はブラウザ側のセキュリティ機能であり、「あるオリジン(ドメイン)から別のオリジンにリクエストが送られたときに、レスポンスを JavaScript で参照できるかどうか」を制御します。

しかし、サーバー間通信(例:PHP の cURL や file_get_contents など)は、CORS ポリシーを無視してリクエストを送信・受信できます。

そのため、PHP の cURLfile_get_contents() では、問題なく Authorization ヘッダー付きの API にリクエストを送ったり、JSON を取得したりできます。

リクエスト方法 CORS の影響 説明
ブラウザの fetch/XHR 受ける WordPress が Access-Control-Allow-Origin を返さないと、JS 側でブロックされる
PHP の cURL/file_get_contents 受けない ヘッダーがなくてもレスポンスを受信可能