web-dev-qa-db-fra.com

Nonce récupéré à partir du REST L'API n'est pas valide et différente de la nonce générée dans wp_localize_script

Pour ceux qui arrivent de Google: Vous ne devriez probablement pas obtenir les nonces de la REST API , à moins que vous vraiment sachiez ce que vous faites. L'authentification basée sur les cookies avec l'API REST n'est que destiné pour les plugins et les thèmes. Pour une application d'une seule page, vous devriez probablement utiliser OAuth .

Cette question existe parce que la documentation n’est pas/n’était pas claire sur la manière de s’authentifier réellement lors de la création d’applications à page unique, les JWT ne sont pas vraiment adaptés aux applications Web et OAuth est plus difficile à implémenter que l’authentification basée sur des cookies.


Le manuel contient un exemple sur la façon dont le client JavaScript Backbone traite les nonces. Si je suis cet exemple, j'obtiens un nonce que les noeuds finaux intégrés, tels que/wp/v2/posts, acceptent.

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

]);

Cependant, l'utilisation de Backbone est hors de question, tout comme les thèmes. J'ai donc écrit le plugin suivant:

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

J'ai un peu bricolé dans la console JavaScript et écrit ce qui suit:

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

Le résultat attendu est deux nouveaux messages, mais je reçois Cookie nonce is invalid du premier et le second crée le message avec succès. C'est probablement parce que les nonces sont différents, mais pourquoi? Je suis connecté en tant que même utilisateur dans les deux demandes.

 enter image description here 

Si mon approche est mauvaise, comment devrais-je obtenir le nonce?

Modifier :

J'ai essayé de jouer avec les globals sans beaucoup de chance. Vous avez un peu plus de chance en utilisant l'action 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,
      ];
    },
  ]);
});

Maintenant, lorsque j'exécute le code JavaScript ci-dessus, deux publications sont créées, mais le noeud final de vérification échoue!

 enter image description here 

Je suis allé à déboguer 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!

J'ai ajouté un peu de journalisation

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

et le code JavaScript se traduit maintenant par les entrées suivantes. Comme vous pouvez le constater, lorsque le noeud final de vérification est appelé, l'ID utilisateur est égal à 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
9
Christian

Examinez de plus près la function rest_cookie_check_errors().

Lorsque vous obtenez le nonce via /wp-json/nonce/v1/get, vous n'envoyez pas de nonce en premier lieu. Donc, cette fonction annule votre authentification, avec ce code:

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

C'est pourquoi vous obtenez un nonce différent de votre appel REST et ne le récupérez pas à partir du thème. L'appel REST ne reconnaît pas intentionnellement vos informations d'identification de connexion (dans ce cas, via l'authentification du cookie), car vous n'avez pas envoyé de nonce valide dans la requête get.

Maintenant, la raison pour laquelle votre code wp_loaded a fonctionné est que vous avez obtenu le nonce et que vous l'avez enregistré en tant que global avant que ce code restant annule votre connexion. La vérification échoue car le code de reste annule votre connexion avant que la vérification ait lieu.

3
Otto

Bien que cette solution fonctionne, ce n’est pas recommandé . OAuth est le choix préféré.


Je crois que j'ai compris.

I think que wp_verify_nonce est cassé, car wp_get_current_user ne parvient pas à obtenir le bon objet utilisateur.

Ce n'est pas, comme illustré par Otto.

Heureusement, il a un filtre: $uid = apply_filters( 'nonce_user_logged_out', $uid, $action );

En utilisant ce filtre, j'ai pu écrire ce qui suit et le code JavaScript s'exécute comme il se doit:

 enter image description here 

<?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 vous remarquez un problème de sécurité avec le correctif, donnez-moi un cri, pour le moment, je ne vois rien de mal à cela, mis à part les globales.

1
Christian

En regardant tout ce code, il semble que votre problème soit l'utilisation de fermetures. À l'étape init, vous ne devez définir que les points d'ancrage et ne pas évaluer les données, car tous les éléments centraux n'ont pas encore été chargés et initialisés.

Dans

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

le $user doit obligatoirement être utilisé tôt dans la fermeture, mais personne ne vous promet que le cookie a déjà été traité et qu'un utilisateur a été authentifié sur cette base. Un meilleur code sera

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

Comme toujours avec wordpress, utilisez le dernier crochet possible et n’essayez jamais de pré-calculer tout ce que vous n’avez pas à faire.

0
Mark Kaplun