Nonce recuperado de la API REST no es válido y es diferente de nonce generado en wp_localize_script

10

Para aquellos que llegan de Google: Probablemente no debería obtener el nonces de la API REST , a menos que realmente sepa lo que está haciendo. La autenticación basada en cookies con la API REST solo está destinada a complementos y temas. Para una sola aplicación página, probablemente debería usar OAuth .

Esta pregunta existe porque la documentación no está / no estaba clara sobre cómo debería autenticarse realmente al crear aplicaciones de una sola página, los JWT no son realmente aptos para aplicaciones web y OAuth es más difícil de implementar que la autenticación basada en cookies.


El manual tiene un ejemplo de cómo el cliente Backbone JavaScript maneja nonces, y si sigo el ejemplo, obtengo un nonce que aceptan los puntos finales integrados como / wp / v2 / posts.

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

]);

Sin embargo, el uso de Backbone está fuera de discusión, y también los temas, así que escribí el siguiente complemento:

<?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,
      ];
    },
  ]);
});

Jugué un poco en la consola de JavaScript y escribí lo siguiente:

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()

El resultado esperado son dos publicaciones nuevas, pero obtengo Cookie nonce is invalidde la primera, y la segunda crea la publicación con éxito. Probablemente sea porque los nonces son diferentes, pero ¿por qué? He iniciado sesión como el mismo usuario en ambas solicitudes.

ingrese la descripción de la imagen aquí

Si mi enfoque es incorrecto, ¿cómo debo obtener el nonce?

Editar :

Traté de jugar con los globales sin mucha suerte . Obtuve un poco más de suerte al utilizar la acción 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,
      ];
    },
  ]);
});

Ahora cuando ejecuto el JavaScript anterior, se crean dos publicaciones, ¡pero el punto final de verificación falla!

ingrese la descripción de la imagen aquí

Fui a depurar 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!

Agregué algunos registros

// 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;
}

y el código JavaScript ahora da como resultado las siguientes entradas. Como puede ver, cuando se llama al punto final de verificación, uid es 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
cristiano
fuente

Respuestas:

3

Eche un vistazo más de cerca a la function rest_cookie_check_errors().

Cuando obtienes el nonce vía /wp-json/nonce/v1/get, no estás enviando un nonce en primer lugar. Entonces, esta función anula su autenticación, con este código:

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

Es por eso que obtienes un nonce diferente de tu llamada REST frente a obtenerlo del tema. La llamada REST intencionalmente no reconoce sus credenciales de inicio de sesión (en este caso a través de la autenticación de cookies) porque no envió un nonce válido en la solicitud de obtención.

Ahora, la razón por la que su código wp_loaded funcionó fue porque obtuvo el nonce y lo guardó en un global antes de que este código de descanso anulara su inicio de sesión. La verificación falla porque el código de descanso anula su inicio de sesión antes de que tenga lugar la verificación.

Otón
fuente
Ni siquiera he visto esa función, pero eso probablemente tiene sentido. La cuestión es, ¿por qué debería incluir un nonce válido para la solicitud GET? (Lo entiendo ahora, pero está lejos de ser obvio). El punto final del punto de verificación / verify es que puedo verificar si el nonce todavía es válido, y si está obsoleto o no es válido, obtengo un nuevo nonce.
Christian
Según la fuente de rest_cookie_check_errors, debería cambiar mi punto final para que no se verifique $_GET['nonce'], sino el encabezado o $_GET['_wpnonce']parámetro nonce . ¿Correcto?
Christian
1

Si bien esta solución funciona, no se recomienda . OAuth es la opción preferida.


Creo que lo tengo.

Yo creo que wp_verify_nonce se rompe, como wp_get_current_user no puede obtener el objeto de usuario adecuada.

No lo es, como lo ilustra Otto.

Afortunadamente tiene un filtro: $uid = apply_filters( 'nonce_user_logged_out', $uid, $action );

Usando este filtro pude escribir lo siguiente, y el código JavaScript se ejecuta como debería:

ingrese la descripción de la imagen aquí

<?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,
      ];
    },
  ]);
});

Si detecta un problema de seguridad con la solución, avíseme, en este momento no puedo ver nada malo, aparte de los globales.

cristiano
fuente
0

Mirando todo este código, parece que su problema es el uso de cierres. En la initetapa, solo debe establecer ganchos y no evaluar los datos, ya que no todo el núcleo había terminado de cargarse e inicializarse.

En

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,
      ];
    },
  ]);

el $userestá obligado pronto para ser utilizado en el cierre, pero nadie promesas a usted que la cookie ya estaban manipulados y un usuario se ha autenticado en base a ellos. Un mejor código será

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,
      ];
    },
  ]);

Como siempre con cualquier gancho en WordPress, use el último gancho posible y nunca intente calcular previamente nada que no sea necesario.

Mark Kaplun
fuente
Utilicé la sección de acciones y enlaces de Monitores de consultas para averiguar qué se ejecuta y en qué orden, set_current_user se ejecuta antes de init & after_setup_theme, no debería haber un problema con $ user que se define fuera y antes de los cierres.
Christian
@Christian, y todos ellos podrían no ser relevantes en el contexto de json API. Me sorprendería mucho si el monitor de consultas funciona en ese contexto
Mark Kaplun