【WordPress】Ajax通信で認証を行う

## 概要
WordPress の Ajax 通信において、nonce（ノンス）と referer（リファラー）を用いた簡単な認証方法をご紹介します。

## 環境
WordPress： 4.9.19
PHP：7.0.27
jQuery：2.2.4

## nonce（ノンス）とは
nonce（ノンス）は任意の文字列や現在時刻、現在のユーザーIDによって
作られるハッシュ値です。
nonce 値は予めサーバー側で上記の要素を元に生成したうえでフロントに渡し、Ajax リクエストを送る際にリクエストパラメーターに含めます。
そしてリクエストを受け取った際に再度同じ要素から nonce 値を生成し、送られてきた値と照合します。

ログインしているユーザーに対してはユーザーごとに一意の値が作成される一方で、非ログインユーザーに対しては渡した文字列に応じて常に同じ値が作成されます。

nonce の有効期限はデフォルトでは12~24時間ですが任意の値を設定することも可能です。
nonce の有効期限が切れていた場合は、同じ文字列から生成した値を照らし合わせても無効となります。
なお nonce の有効期限の考え方はやや複雑なので、[こちらのドキュメント](https://wpdocs.osdn.jp/WordPress_Nonce#nonce_.E3.81.AE.E6.9C.89.E5.8A.B9.E6.9C.9F.E9.96.93.E3.81.AE.E5.A4.89.E6.9B.B4)を参考にしていただければと思います。

nonce は WordPress においては CSRF 対策として用いられることが多いようです。
しかし有効期限内であれば何度でも同じ値を使えること、非ログインユーザーには同じ値しか生成されないことなどから安全性が高いわけではないのでご注意ください。

## referer（リファラー）とは
referer（リファラー）とは参照元を意味する用語であり、Ajax 通信においては Ajax リクエストの送信元を指します。
これを使った認証を行うことで外部からのリクエストを見分けたり、想定していないページから間違ってリクエストを送信することを防いだりすることができます。
具体的にはリクエストヘッダーの Referer フィールドの値（HTTP リファラー）とリクエストボディに含まれる「_wp_http_referer」パラメータのうちいずれか片方を参照します。

なお、「参照元」という意味の英単語の正しい綴りは「referrer」です。
WordPress においては HTTP ヘッダーの名前に合わせるためなのか、敢えて間違った綴りを採用しているようです。

## 実装内容
### コールバック関数の登録
ここからは実装を進めていきます。
まずは準備として、Ajax を使うためのフックにコールバック関数を登録します。

``` functions.php
function sample_ajax () {
  // 後程記述
}

add_action('wp_ajax_sample_action', 'sample_ajax');
add_action('wp_ajax_nopriv_sample_action', 'sample_ajax');
```
今回は非ログインユーザーも対象とするため、```wp_ajax_sample_action``` と ```wp_ajax_nopriv_sample_action``` 両方のフックに関数を登録します。
両フックの「sample_action」の部分は action と呼ばれるもので、ここに記述した文字列は後述の Ajax のリクエストにも含める必要があります。


### パラメータの出力
次にリクエストに必要な値を function.php のコードでフロントに出力します。

``` functions.php
function enque_scripts() {
    if (is_page('sample')) {
        $handle = 'js_handle_sample';
        wp_register_script($handle, get_template_directory_uri(). '/js/sample.js', false, null, true);
        $nonce = wp_create_nonce('abcdefg');

        $params = [
            'ajax_url' => admin_url( 'admin-ajax.php'),
            'action' => 'sample_action',
            'nonce' => $nonce
        ];
        wp_localize_script($handle, 'params', $params);

        wp_enqueue_script($handle);
    }
}

add_action('wp_enqueue_scripts', 'enque_scripts');
```

今回は Ajax リクエストのエンドポイント URL、Ajax のコールバック関数登録時に使用した action の名前、nonce の3つを出力します。

nonce は ```wp_create_nonce()``` によって、引数に渡した文字列から作成します。

出力の手順としては、まず ```wp_register_script()``` で Ajax 送信専用の JavaScript のファイルを登録します。
ファイル名は第二引数で指定します。

次に3つのパラメータを ```$params```という名前の配列に入れて ```wp_localize_script()``` の第三引数に渡します。
渡した配列は HTML の ```script``` タグにオブジェクト形式で出力され、 登録したファイルから参照できるようになります。
この出力したデータは第二引数で指定した名前の変数に格納されます。
そのため、例えば出力したデータから JavaScript のコードでnonce を取得したい場合は ```params.nonce``` で取得できます。


最後にこれらの内容を ```wp_enqueue_script()``` によって WordPress のスクリプト出力用のキューに登録します。

なお、3つの関数の第一引数に使われている ```$handle``` はキューへの登録に必要なハンドル名であり、3つの関数で共通のものを使わなければなりません。

以上により出力される内容はこちらになります。

``` page-sample.php
<script type="text/javascript" id="js_handle_sample-js-extra">
/* <![CDATA[ */
var params = {"ajax_url":"https:\/\/test.com\/wp-admin\/admin-ajax.php","action":"sample_action","nonce":"7d1a54127b"};
/* ]]> */
</script>
<script type="text/javascript" src="https://test.com/wp-content/themes/sample_template/js/sample.js" id="js_handle_sample-js"></script>
```

### referer の出力
Ajax リクエストを送信するテンプレートファイルの任意の個所に以下のコードを追加することで、出力するページのスラッグ以下の部分を値に持つ ```input``` タグを出力することができます。（厳密には ```$_SERVER[ 'REQUEST_URI' ]``` で取得できる値）

``` page-sample.php
<?php wp_referer_field(); ?>
```

出力されるコード：
```
<input type="hidden" name="_wp_http_referer" value="/sample/">
```
上記の value 属性の値を「_wp_http_referer」というパラメータに含めて送信することで、サーバーサイドで ```wp_get_referer()``` 関数を使って取得することができます。

ちなみにフロントに referer を出力せずに、```wp_get_referer()``` 関数によって HTTP ヘッダーからリファラー を取得することもできます。
ただし、ブラウザの設定によっては HTTP リファラーを送信しないようにできること、ツールを使えば簡単に改ざんできることから今回は上記の関数を使ってフロントに出力する方法を使いたいと思います。

### 送信処理

``` sample.js
$(function(){
	$('.button').on('click', function() {
		const referer = $('input[name="_wp_http_referer"]').val();
		
		$.ajax({
        type: "post",
        url: params.ajax_url,
        data: {
            action: params.action,
            nonce: params.nonce,
            _wp_http_referer: referer,
            data: 'テスト送信'
        }
    }).done(function (data) {
        console.log(data);
        if(data.includes('error fatal')) {
            console.log('認証失敗');
        } else {
            console.log("ajax成功");
        }
    }).fail(function (XMLHttpRequest, status, e) {
        console.log("ajax失敗: " + status + XMLHttpRequest);
        console.log(e);
    });
	});
});
```
送信するデータは以下の通りです。
action：PHP のコールバック関数をフックに登録する際に使用した action の文字列
nonce：サーバーサイドから出力した nonce の値
_wp_http_referer：リファラー。HTML に出力した ```input``` タグから取得
data：送信するデータ本体

### 受信&認証処理

``` functions.php
function sample_ajax () {
    // nonceをチェック
    if (!check_ajax_referer('abcdefg', 'nonce', false)) {
        echo 'error fatal';
        wp_die();
        return;
    }
    
    // POSTパラメータのrefererをチエック
    $referer = wp_get_referer();
   if ($referer !== '/sample/') {
      echo 'error fatal';
      wp_die();
      return;
	}
    
    // 認証成功時の処理
    echo $_POST['data'];
}

add_action('wp_ajax_sample_action', 'sample_ajax');
add_action('wp_ajax_nopriv_sample_action', 'sample_ajax');
```

まずは送られてきた nonce 値を ```check_ajax_referer()``` でチェックします。
この関数は送られてきた値と、第一引数に渡した文字列から生成した nonce 値を照合して同じ値であればtrue、 そうでなければ false を返却します。
ここでは 照合の結果が false であれば「error fatal」という文字列を出力し、受信処理を終了しています。

第二引数では nonce 値が含まれているリクエストのパラメータ名、第三引数では nonce 値が無効だった場合に即時に受信処理を終了（die）するかを指定します。
今回は無効時に「error fatal」を ```echo``` を使ってフロントへ出力したいので false にしています。

nonce の次は referer を確認します。
referer は ```wp_get_referer()``` で取得しています。
この関数はリクエストパラメータに ```_wp_http_referer``` フィールドが含まれていればその値を、なければ HTTP ヘッダーの Referer フィールドを取得します。
今回は ```wp_referer_field()``` でフロントに出力した値を ```_wp_http_referer``` フィールドに含めて送信しているため、「/sample/」が取得できます。

なお、```wp_referer_field()``` の性質上、ページの URL に GET パラメータを付与してページにアクセスしていた場合は GET パラメータも含めて ```input``` タグに出力されます。
GET パラメータも含めて判定する場合は ```strpos()```などの関数を使うとよいと思います。

nonce と referer を両方チェックした後は成功時の処理を書いていくとよいでしょう。

## まとめ
今回は nonce と referer を使った認証方法をご紹介しました。
これで最低限のセキュリティは確保できると思います。
しかし、実装が簡単なだけにセキュリティとしては不十分な点が多いことも事実です。
本格的な認証を導入したい場合は、完全に一度しか使えない値を生成するなど、別の方法を検討した方が良いと思います。

## 参考
https://tadtadya.com/secure-communication-with-wordpress-ajax/
[AJAX \| Plugin Developer Handbook | WordPress Developer Resources](https://developer.wordpress.org/plugins/javascript/ajax/)
[HTTPリファラ - Wikipedia](https://ja.wikipedia.org/wiki/HTTP%E3%83%AA%E3%83%95%E3%82%A1%E3%83%A9)