Le nonce récupéré à partir de l'API REST n'est pas valide et différent du nonce généré dans wp_localize_script

10

Pour ceux qui arrivent de Google: vous ne devriez probablement pas obtenir les nonces de l'API REST , sauf si vous savez vraiment ce que vous faites. L'authentification basée sur les cookies avec l'API REST est uniquement destinée aux plugins et aux 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 façon dont vous devez réellement vous authentifier 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 à mettre en œuvre que l'authentification basée sur les cookies.


Le manuel contient un exemple sur la façon dont le client JavaScript Backbone gère les nonces, et si je suis l'exemple, j'obtiens un nonce que les points de terminaison 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 bricolé un peu dans la console JavaScript et j'ai é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 escompté est deux nouveaux messages, mais je reçois Cookie nonce is invaliddu 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.

entrez la description de l'image ici

Si mon approche est fausse, comment dois-je obtenir le nonce?

Modifier :

J'ai essayé de jouer avec les mondiaux sans trop de chance . A obtenu 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 JavaScript ci-dessus, deux publications sont créées, mais le point de terminaison de vérification échoue!

entrez la description de l'image ici

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 entraîne désormais les entrées suivantes. Comme vous pouvez le voir, lorsque le point de terminaison de vérification est appelé, uid est 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
Christian
la source

Réponses:

3

Regardez de plus près le 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. Cette fonction annule donc 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 par rapport au thème. L'appel REST ne reconnaît pas intentionnellement vos informations de connexion (dans ce cas via l'authentification par cookie) car vous n'avez pas envoyé de nonce valide dans la demande 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é dans un global avant que ce code de repos n'annule votre connexion. La vérification échoue car le code de repos annule votre connexion avant la vérification.

Otto
la source
Je n'ai même pas examiné cette fonction, mais cela a probablement du sens. La chose est, pourquoi devrais-je inclure un nonce valide pour la demande GET? (Je comprends maintenant, mais c'est loin d'être évident) Le point final du point de terminaison / verify est que je peux vérifier si le nonce est toujours valide, et s'il devient périmé ou n'est pas valide, obtenir un nouveau nonce.
Christian
En fonction de la source de rest_cookie_check_errors, je devrais changer mon point de terminaison pour qu'il ne vérifie pas $_GET['nonce'], mais l'en-tête ou le $_GET['_wpnonce']paramètre nonce . Correct?
Christian
1

Bien que cette solution fonctionne, elle n'est pas recommandée . OAuth est le choix préféré.


Je crois que j'ai compris.

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

Ce n'est pas le cas, 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:

entrez la description de l'image ici

<?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 détectez un problème de sécurité avec le correctif, veuillez me donner un cri, pour le moment je ne vois rien de mal à cela, à part les globaux.

Christian
la source
0

En regardant tout ce code, il semble que votre problème soit l'utilisation de fermetures. À l' initétape, vous ne devez définir que des hooks et ne pas évaluer les données car tout le noyau n'a pas terminé le chargement et l'initialisation.

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 $userest destiné à ê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 la base de ceux-ci. 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 n'importe quel crochet dans wordpress, utilisez le dernier crochet possible et n'essayez jamais de précalculer tout ce que vous n'avez pas à faire.

Mark Kaplun
la source
J'ai utilisé la section Actions et hooks de Query Monitors afin de comprendre ce qui s'exécute et dans quel ordre, set_current_user s'exécute avant init & after_setup_theme, il ne devrait pas y avoir de problème avec $ user étant défini à l'extérieur et avant les fermetures.
Christian
@Christian, et tous peuvent ne pas être pertinents dans le contexte de l'API json. Je serais très surpris si le moniteur de requêtes fonctionne dans ce contexte
Mark Kaplun