Проверка nonce в REST API
Я хотел бы разобраться в лучших практиках проверки nonce в REST API.
Я вижу, что многие говорят о nonce wp_rest
для REST-запросов. Но при изучении исходного кода WordPress я обнаружил, что wp_rest
- это просто nonce для проверки статуса авторизованного пользователя, а если его нет, запрос выполняется от имени гостя.
Учитывая это, должен ли я отправлять два nonce при выполнении POST-запроса к REST API? Один для аутентификации wp_rest
и другой для действия foo_action
?
Если да, то как мне отправить nonce wp_rest
и foo_action
в JavaScript, и в каком месте PHP-кода правильно проверять эти nonce? (Имею в виду validate_callback для аргумента или permission_callback?)

Вам следует передать специальный одноразовый код wp_rest
(nonce) как часть запроса. Без него объект global $current_user
не будет доступен в вашем REST-классе. Вы можете передать его несколькими способами: через $_GET, $_POST или заголовки.
Действующий одноразовый код (action nonce) является опциональным. Если вы его добавите, то не сможете использовать REST-эндпоинт с внешнего сервера — только из запросов, отправленных из самого WordPress. Пользователь может аутентифицироваться с помощью Basic Auth, OAuth2 или JWT с внешнего сервера даже без одноразового кода wp_rest
, но если вы добавите и action nonce, это не сработает.
Таким образом, action nonce является опциональным. Добавляйте его, только если хотите, чтобы эндпоинт работал локально.
Пример:
/**
* Первый шаг: регистрация, локализация и подключение JavaScript
*/
wp_register_script( 'main-js', get_template_directory_uri() . '/js/main.js', [ 'jquery' ] );
wp_localize_script( 'main-js', 'data', [
'rest' => [
'endpoints' => [
'my_endpoint' => esc_url_raw( rest_url( 'my_plugin/v1/my_endpoint' ) ),
],
'timeout' => (int) apply_filters( "my_plugin_rest_timeout", 60 ),
'nonce' => wp_create_nonce( 'wp_rest' ),
//'action_nonce' => wp_create_nonce( 'action_nonce' ),
],
] );
wp_enqueue_script( 'main-js' );
/**
* Второй шаг: запрос в JavaScript-файле
*/
jQuery(document).on('click', '#some_element', function () {
let ajax_data = {
'some_value': jQuery( ".some_value" ).val(),
//'action_nonce': data.rest.action_nonce
};
jQuery.ajax({
url: data.rest.endpoints.my_endpoint,
method: "GET",
dataType: "json",
timeout: data.rest.timeout,
data: ajax_data,
beforeSend: function (xhr) {
xhr.setRequestHeader('X-WP-Nonce', data.rest.nonce);
}
}).done(function (results) {
console.log(results);
alert("Успешно!");
}).fail(function (xhr) {
console.log(results);
alert("Ошибка!");
});
});
/**
* Третий шаг: сам REST-эндпоинт
*/
class My_Endpoint {
public function registerRoutes() {
register_rest_route( 'my_plugin', 'v1/my_endpoint', [
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'get_something' ],
'args' => [
'some_value' => [
'required' => true,
],
],
'permission_callback' => function ( WP_REST_Request $request ) {
return true;
},
] );
}
/**
* @return WP_REST_Response
*/
private function get_something( WP_REST_Request $request ) {
//if ( ! wp_verify_nonce( $request['nonce'], 'action_nonce' ) ) {
// return false;
//}
$some_value = $request['some_value'];
if ( strlen( $some_value ) < 5 ) {
return new WP_REST_Response( 'Извините, Some Value должен содержать не менее 5 символов.', 400 );
}
// Поскольку мы передаем заголовок "X-WP-Nonce", это сработает:
$user = wp_get_current_user();
if ( $user instanceof WP_User ) {
return new WP_REST_Response( 'Извините, не удалось получить имя.', 400 );
} else {
return new WP_REST_Response( 'Ваше имя пользователя: ' . $user->display_name, 200 );
}
}
}

Важно также добавить 'required' => true
к аргументам foo_nonce
, иначе можно будет отправлять запросы к конечной точке без nonce.

Откуда берется data.rest.nonce
? Я не вижу его определения в вашем коде.

@Dylan Хорошая идея сделать его обязательным, но если вы не отправите nonce действия, запрос завершится ошибкой при проверке wp_verify_nonce, которая выполняется перед запуском действия

Развивая то, что написал @lucas-bustamante (это очень помогло!), после настройки заголовка X-WP-Nonce в ваших пользовательских маршрутах, вы можете сделать следующее:
register_rest_route('v1', '/my_post', [
'methods' => WP_REST_Server::CREATABLE,
'callback' => [$this, 'create_post'],
'args' => [
'post_title' => [
'required' => true,
],
'post_excerpt' => [
'required' => true,
]
],
'permission_callback' => function ( ) {
return current_user_can( 'publish_posts' );
},
]);
Обратите внимание, что permission_callback
находится на корневом уровне, а не под args
(документация здесь), и я убрал дополнительную проверку nonce
из args
, так как проверка разрешения сама по себе завершится ошибкой, если nonce недействителен или не предоставлен (я тщательно протестировал это и могу подтвердить, что получаю ошибку, когда nonce не передан или он недействителен).

Важно отметить, что в ответе @Lucas Bustamante описан процесс проверки на основе пользователя. Это означает, что если у вас есть анонимная конечная точка API, которая не требует пользователя, то просто не передавая заголовок X-WP-NONCE
, вы пройдёте описанную проверку nonce. Однако, если передать некорректный nonce, всё равно будет вызвана ошибка.
Причина в том, что функция rest_cookie_check_errors
, которая выполняет проверку, просто устанавливает current_user
в пустое значение, если nonce не предоставлен. Это работает корректно, когда пользователь требуется, но не в обратном случае. (см.: https://developer.wordpress.org/reference/functions/rest_cookie_check_errors/)
Если вы хотите расширить ответ Lucas, чтобы включить анонимные конечные точки, вы можете добавить ручную проверку nonce в начало вашей конечной точки, например так:
if ( !$_SERVER['HTTP_X_WP_NONCE'] || !wp_verify_nonce( $_SERVER['HTTP_X_WP_NONCE'], 'wp_rest' ) ) {
header('HTTP/1.0 403 Forbidden');
exit;
}
