O nonce recuperado da API REST é inválido e diferente do nonce gerado em wp_localize_script

10

Para aqueles que chegam do Google: você provavelmente não deve obter os nonces da API REST , a menos que saiba realmente o que está fazendo. Autenticação baseada em cookie com o API REST é apenas significou para plugins e temas. Para um aplicativo de página única, você provavelmente deve usar o OAuth .

Essa pergunta existe porque a documentação não é clara ou não é clara sobre como você deve se autenticar ao criar aplicativos de página única, os JWTs não são adequados para aplicativos da Web e o OAuth é mais difícil de implementar do que a autenticação baseada em cookies.


O manual tem um exemplo de como o cliente Backbone JavaScript lida com nonces e, se eu seguir o exemplo, recebo um nonce que os pontos de extremidade internos como / wp / v2 / posts aceitam.

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

]);

No entanto, o uso do Backbone está fora de questão, assim como os temas, então escrevi o seguinte plugin:

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

Eu consertei um pouco o console do JavaScript e escrevi o seguinte:

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

O resultado esperado são duas novas postagens, mas recebo Cookie nonce is invalidda primeira e a segunda cria a postagem com êxito. Provavelmente porque os nonces são diferentes, mas por quê? Estou logado como o mesmo usuário nos dois pedidos.

insira a descrição da imagem aqui

Se minha abordagem estiver errada, como devo obter o nonce?

Editar :

Eu tentei mexer com globals sem muita sorte . Foi um pouco mais sortudo utilizando a ação 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,
      ];
    },
  ]);
});

Agora, quando executo o JavaScript acima, duas postagens são criadas, mas o ponto de extremidade de verificação falha!

insira a descrição da imagem aqui

Fui para 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!

Eu adicionei alguns logs

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

e o código JavaScript agora resulta nas seguintes entradas. Como você pode ver, quando o ponto de extremidade de verificação é chamado, 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
cristão
fonte

Respostas:

3

Dê uma olhada no function rest_cookie_check_errors().

Quando você obtém o nonce via /wp-json/nonce/v1/get, você não está enviando um nonce em primeiro lugar. Portanto, essa função anula sua autenticação, com 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;
}

É por isso que você está recebendo um nonce diferente da sua chamada REST e obtendo-o do tema. A chamada REST não intencionalmente reconhece suas credenciais de login (neste caso, através da autenticação de cookie) porque você não enviou um nonce válido na solicitação de obtenção.

Agora, o motivo pelo qual o código wp_loaded funcionou foi porque você obteve o nonce e o salvou em um global antes que esse código restante anulasse seu login. A verificação falha porque o código restante anula seu logon antes que a verificação ocorra.

Otto
fonte
Eu nem olhei para essa função, mas isso provavelmente faz sentido. O problema é: por que devo incluir um nonce válido para a solicitação GET? (Eu entendo agora, mas está longe de ser óbvio) O ponto principal do terminal / verificar é que posso verificar se o nonce ainda é válido e se está ficando obsoleto ou inválido, obter um novo nonce.
Christian
Com base na origem de rest_cookie_check_errors, devo alterar meu nó de extremidade para que não verifique $_GET['nonce'], mas o cabeçalho ou $_GET['_wpnonce']parâmetro nonce . Corrigir?
Christian
1

Embora essa solução funcione, ela não é recomendada . OAuth é a escolha preferida.


Eu acho que entendi.

Eu acho que wp_verify_nonce está quebrado, como wp_get_current_user falha ao obter o objeto de usuário adequado.

Não é, como ilustrado por Otto.

Felizmente, ele tem um filtro: $uid = apply_filters( 'nonce_user_logged_out', $uid, $action );

Usando esse filtro, consegui escrever o seguinte e o código JavaScript é executado da seguinte maneira:

insira a descrição da imagem aqui

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

Se você encontrar um problema de segurança com a correção, dê-me um grito, agora não vejo nada de errado com ela, além de globais.

cristão
fonte
0

Olhando para todo esse código, parece que seu problema é o uso de fechamentos. No initestágio, você deve definir apenas ganchos e não avaliar os dados, pois nem todo o núcleo terminou o carregamento e a inicialização.

No

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

o $userlimite é o uso antecipado no fechamento, mas ninguém promete a você que o cookie já foi tratado e que um usuário foi autenticado com base neles. Um código melhor 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 sempre, com qualquer gancho no wordpress, use o gancho mais recente possível e nunca tente pré-calcular o que você não precisa.

Mark Kaplun
fonte
Usei a seção de ações e ganchos do Query Monitors para descobrir o que é executado e em qual ordem, o set_current_user é executado antes do init & after_setup_theme, não deve haver um problema com o $ user sendo definido fora e antes dos fechamentos.
Christian
@Christian, e todos eles podem não ser relevantes no contexto da API json. Eu ficaria muito surpreso se o monitor consulta funciona nesse contexto
Mark Kaplun