Одноразовый номер, полученный из REST API, недопустим и отличается от одноразового номера, сгенерированного в wp_localize_script

10

Для тех, кто прибывает из Google: вы, вероятно, не должны получать одноразовые номера от REST API , если вы действительно не знаете, что делаете. Cookie проверки подлинности на основе с REST API только предназначены для плагинов и тем. Для одностраничного приложения вы, вероятно, должны использовать OAuth .

Этот вопрос существует, потому что в документации нет / неясно, как вам следует выполнять аутентификацию при создании одностраничных приложений, JWT не подходят для веб-приложений, а OAuth сложнее реализовать, чем аутентификация на основе файлов cookie.


В справочнике есть пример того, как клиент Backbone JavaScript обрабатывает одноразовые номера, и если я последую этому примеру, я получу одноразовый номер, который принимает встроенные конечные точки, такие как / wp / v2 / posts.

\wp_localize_script("client-js", "theme", [
  'nonce' => wp_create_nonce('wp_rest'),
  'user' => get_current_user_id(),

]);

Тем не менее, использование Backbone исключено, как и темы, поэтому я написал следующий плагин:

<?php
/*
Plugin Name: Nonce Endpoint
*/

add_action('rest_api_init', function () {
  $user = get_current_user_id();
  register_rest_route('nonce/v1', 'get', [
    'methods' => 'GET',
    'callback' => function () use ($user) {
      return [
        'nonce' => wp_create_nonce('wp_rest'),
        'user' => $user,
      ];
    },
  ]);

  register_rest_route('nonce/v1', 'verify', [
    'methods' => 'GET',
    'callback' => function () use ($user) {
      $nonce = !empty($_GET['nonce']) ? $_GET['nonce'] : false;
      return [
        'valid' => (bool) wp_verify_nonce($nonce, 'wp_rest'),
        'user' => $user,
      ];
    },
  ]);
});

Я немного повозился в консоли JavaScript и написал следующее:

var main = async () => { // var because it can be redefined
  const nonceReq = await fetch('/wp-json/nonce/v1/get', { credentials: 'include' })
  const nonceResp = await nonceReq.json()
  const nonceValidReq = await fetch(`/wp-json/nonce/v1/verify?nonce=${nonceResp.nonce}`, { credentials: 'include' })
  const nonceValidResp = await nonceValidReq.json()
  const addPost = (nonce) => fetch('/wp-json/wp/v2/posts', {
    method: 'POST',
    credentials: 'include',
    body: JSON.stringify({
      title: `Test ${Date.now()}`,
      content: 'Test',
    }),
    headers: {
      'X-WP-Nonce': nonce,
      'content-type': 'application/json'
    },
  }).then(r => r.json()).then(console.log)

  console.log(nonceResp.nonce, nonceResp.user, nonceValidResp)
  console.log(theme.nonce, theme.user)
  addPost(nonceResp.nonce)
  addPost(theme.nonce)
}

main()

Ожидаемый результат - два новых сообщения, но я получаю Cookie nonce is invalidпервое, а второе создает сообщение успешно. Это, вероятно, потому что одноразовые номера разные, но почему? Я вошел как один и тот же пользователь в обоих запросах.

введите описание изображения здесь

Если мой подход неверен, как мне получить одноразовый номер?

Редактировать :

Я попытался возиться с глобалами без особой удачи . Немного повезло с использованием действия wp_loaded:

<?php
/*
Plugin Name: Nonce Endpoint
*/

$nonce = 'invalid';
add_action('wp_loaded', function () {
  global $nonce;
  $nonce = wp_create_nonce('wp_rest');
});

add_action('rest_api_init', function () {
  $user = get_current_user_id();
  register_rest_route('nonce/v1', 'get', [
    'methods' => 'GET',
    'callback' => function () use ($user) {
      return [
        'nonce' => $GLOBALS['nonce'],
        'user' => $user,
      ];
    },
  ]);

  register_rest_route('nonce/v1', 'verify', [
    'methods' => 'GET',
    'callback' => function () use ($user) {
      $nonce = !empty($_GET['nonce']) ? $_GET['nonce'] : false;
      error_log("verify $nonce $user");
      return [
        'valid' => (bool) wp_verify_nonce($nonce, 'wp_rest'),
        'user' => $user,
      ];
    },
  ]);
});

Теперь, когда я запускаю JavaScript выше, создаются два сообщения, но проверка конечной точки завершается неудачно!

введите описание изображения здесь

Я пошел, чтобы отладить wp_verify_nonce:

function wp_verify_nonce( $nonce, $action = -1 ) {
  $nonce = (string) $nonce;
  $user = wp_get_current_user();
  $uid = (int) $user->ID; // This is 0, even though the verify endpoint says I'm logged in as user 2!

Я добавил некоторые записи

// Nonce generated 0-12 hours ago
$expected = substr( wp_hash( $i . '|' . $action . '|' . $uid . '|' . $token, 'nonce'), -12, 10 );
error_log("expected 1 $expected received $nonce uid $uid action $action");
if ( hash_equals( $expected, $nonce ) ) {
  return 1;
}

// Nonce generated 12-24 hours ago
$expected = substr( wp_hash( ( $i - 1 ) . '|' . $action . '|' . $uid . '|' . $token, 'nonce' ), -12, 10 );
error_log("expected 2 $expected received $nonce uid $uid action $action");
if ( hash_equals( $expected, $nonce ) ) {
  return 2;
}

и код JavaScript теперь приводит к следующим записям. Как вы можете видеть, когда вызывается конечная точка проверки, uid равен 0.

[01-Mar-2018 11:41:57 UTC] verify 716087f772 2
[01-Mar-2018 11:41:57 UTC] expected 1 b35fa18521 received 716087f772 uid 0 action wp_rest
[01-Mar-2018 11:41:57 UTC] expected 2 dd35d95cbd received 716087f772 uid 0 action wp_rest
[01-Mar-2018 11:41:58 UTC] expected 1 716087f772 received 716087f772 uid 2 action wp_rest
[01-Mar-2018 11:41:58 UTC] expected 1 716087f772 received 716087f772 uid 2 action wp_rest
Кристиан
источник

Ответы:

3

Присмотритесь к function rest_cookie_check_errors().

Когда вы получаете одноразовый номер /wp-json/nonce/v1/get, вы не отправляете одноразовый номер в первую очередь. Таким образом, эта функция аннулирует вашу аутентификацию с помощью этого кода:

if ( null === $nonce ) {
    // No nonce at all, so act as if it's an unauthenticated request.
    wp_set_current_user( 0 );
    return true;
}

Вот почему вы получаете одноразовый номер от вашего вызова REST от получения из темы. Вызов REST преднамеренно не распознает ваши учетные данные для входа в систему (в данном случае с помощью cookie-аутентификации), поскольку вы не отправили действительный одноразовый номер в запросе get.

Теперь причина, по которой ваш код wp_loaded работал, заключается в том, что вы получили одноразовый номер и сохранили его в глобальном коде до того, как этот код отдыха аннулирует ваш логин. Проверка не удалась, потому что остальной код аннулирует ваш логин перед проверкой.

эфирное масло
источник
Я даже не смотрел на эту функцию, но это, вероятно, имеет смысл. Дело в том, почему я должен включать допустимый одноразовый номер для запроса GET? (Я понял это сейчас, но это далеко не очевидно). Весь смысл конечной точки / verify заключается в том, что я могу проверить, является ли одноразовый номер еще действительным, и если он устарел или недействителен, получить новый одноразовый номер.
Кристиан
Основываясь на источнике rest_cookie_check_errors, я должен изменить свою конечную точку, чтобы она не проверяла $_GET['nonce'], а только заголовок или $_GET['_wpnonce']параметр nonce . Правильный?
Кристиан
1

Хотя это решение работает, оно не рекомендуется . OAuth является предпочтительным выбором.


Я думаю, что понял.

Я думаю, что wp_verify_nonce не работает, так как wp_get_current_user не может получить нужный объект пользователя.

Это не так, как иллюстрирует Отто.

К счастью, у него есть фильтр: $uid = apply_filters( 'nonce_user_logged_out', $uid, $action );

Используя этот фильтр, я смог написать следующее, и код JavaScript выполняется так, как должен:

введите описание изображения здесь

<?php
/*
Plugin Name: Nonce Endpoint
*/

$nonce = 'invalid';
add_action('wp_loaded', function () {
  global $nonce;
  $nonce = wp_create_nonce('wp_rest');
});

add_action('rest_api_init', function () {
  $user = get_current_user_id();
  register_rest_route('nonce/v1', 'get', [
    'methods' => 'GET',
    'callback' => function () use ($user) {
      return [
        'nonce' => $GLOBALS['nonce'],
        'user' => $user,
      ];
    },
  ]);

  register_rest_route('nonce/v1', 'verify', [
    'methods' => 'GET',
    'callback' => function () use ($user) {
      $nonce = !empty($_GET['nonce']) ? $_GET['nonce'] : false;
      add_filter("nonce_user_logged_out", function ($uid, $action) use ($user) {
        if ($uid === 0 && $action === 'wp_rest') {
          return $user;
        }

        return $uid;
      }, 10, 2);

      return [
        'status' => wp_verify_nonce($nonce, 'wp_rest'),
        'user' => $user,
      ];
    },
  ]);
});

Если вы обнаружите проблему безопасности с исправлением, пожалуйста, напишите мне, прямо сейчас я не вижу ничего плохого в этом, кроме глобальных.

Кристиан
источник
0

Глядя на весь этот код, кажется, что ваша проблема заключается в использовании замыканий. На initэтапе вы должны только установить хуки и не оценивать данные, так как не все ядро ​​завершило загрузку и инициализировалось.

В

add_action('rest_api_init', function () {
  $user = get_current_user_id();
  register_rest_route('nonce/v1', 'get', [
    'methods' => 'GET',
    'callback' => function () use ($user) {
      return [
        'nonce' => $GLOBALS['nonce'],
        'user' => $user,
      ];
    },
  ]);

$userнеизбежно рано использоваться в закрытии, но никто не обещает вам , что куки были уже обработаны и аутентификации пользователя на их основе. Лучший код будет

add_action('rest_api_init', function () {
  register_rest_route('nonce/v1', 'get', [
    'methods' => 'GET',
    'callback' => function () {
    $user = get_current_user_id();
      return [
        'nonce' => $GLOBALS['nonce'],
        'user' => $user,
      ];
    },
  ]);

Как всегда с любым хуком в WordPress, используйте новейший хук и никогда не пытайтесь пересчитать то, что вам не нужно.

Марк Каплун
источник
Я использовал раздел действий и ловушек Query Monitors, чтобы выяснить, что выполняется и в каком порядке set_current_user запускается до init & after_setup_theme, не должно быть проблем с определением $ user снаружи и перед замыканиями.
Кристиан
@Christian, и все они могут быть неуместны в контексте json API. Я был бы очень удивлен, если бы монитор запросов работал в этом контексте
Марк Каплун