Von der REST-API abgerufene Nonce ist ungültig und unterscheidet sich von Nonce, das in wp_localize_script generiert wurde

10

Für diejenigen, die von Google kommen: Sie sollten die Nonces wahrscheinlich nicht von der REST-API erhalten , es sei denn, Sie wissen wirklich , was Sie tun. Die Cookie-basierte Authentifizierung mit der REST-API ist nur für Plugins und Themes gedacht . Für eine Anwendung mit nur einer Seite sollten Sie wahrscheinlich OAuth verwenden .

Diese Frage besteht, weil in der Dokumentation nicht klar ist, wie Sie sich beim Erstellen von Apps für einzelne Seiten tatsächlich authentifizieren sollten, JWTs nicht wirklich für Web-Apps geeignet sind und OAuth schwieriger zu implementieren ist als die Cookie-basierte Authentifizierung.


Das Handbuch enthält ein Beispiel dafür, wie der Backbone-JavaScript-Client mit Nonces umgeht. Wenn ich dem Beispiel folge, erhalte ich eine Nonce, die von den integrierten Endpunkten wie / wp / v2 / posts akzeptiert wird.

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

]);

Die Verwendung von Backbone kommt jedoch nicht in Frage, ebenso wie Themen. Deshalb habe ich das folgende Plugin geschrieben:

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

Ich habe ein bisschen an der JavaScript-Konsole herumgebastelt und folgendes geschrieben:

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

Das erwartete Ergebnis sind zwei neue Beiträge, aber ich bekomme Cookie nonce is invalidvom ersten und der zweite erstellt den Beitrag erfolgreich. Das liegt wahrscheinlich daran, dass die Nonces unterschiedlich sind, aber warum? Ich bin in beiden Anfragen als derselbe Benutzer angemeldet.

Geben Sie hier die Bildbeschreibung ein

Wenn mein Ansatz falsch ist, wie soll ich die Nonce bekommen?

Bearbeiten :

Ich habe versucht, ohne viel Glück mit Globals zu spielen . Ich habe ein bisschen mehr Glück mit der Aktion 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,
      ];
    },
  ]);
});

Wenn ich jetzt das obige JavaScript ausführe, werden zwei Beiträge erstellt, aber der Endpunkt zur Überprüfung schlägt fehl!

Geben Sie hier die Bildbeschreibung ein

Ich ging zum Debuggen von 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!

Ich habe etwas Protokollierung hinzugefügt

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

und der JavaScript-Code führt nun zu den folgenden Einträgen. Wie Sie sehen können, ist uid beim Aufruf des Verifizierungsendpunkts 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
quelle

Antworten:

3

Schauen Sie sich das genauer an function rest_cookie_check_errors().

Wenn Sie die Nonce über erhalten /wp-json/nonce/v1/get, senden Sie überhaupt keine Nonce. Diese Funktion macht Ihre Authentifizierung mit diesem Code ungültig:

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

Aus diesem Grund erhalten Sie von Ihrem REST-Aufruf eine andere Nonce als vom Thema. Der REST-Aufruf erkennt Ihre Anmeldeinformationen absichtlich nicht (in diesem Fall über die Cookie-Authentifizierung), da Sie in der Abrufanforderung keine gültige Nonce gesendet haben.

Der Grund, warum Ihr wp_loaded-Code funktioniert hat, war, dass Sie die Nonce erhalten und in einem globalen Code gespeichert haben, bevor dieser Restcode Ihre Anmeldung ungültig machte. Die Überprüfung schlägt fehl, da der Restcode Ihre Anmeldung ungültig macht, bevor die Überprüfung stattfindet.

Otto
quelle
Ich habe mir diese Funktion noch nicht einmal angesehen, aber das macht wahrscheinlich Sinn. Die Sache ist, warum sollte ich eine gültige Nonce für die GET-Anfrage einfügen? (Ich verstehe es jetzt, aber es ist alles andere als offensichtlich.) Der springende Punkt des / verify-Endpunkts ist, dass ich überprüfen kann, ob das Nonce noch gültig ist, und ob es veraltet ist oder nicht, ein neues Nonce.
Christian
Basierend auf der Quelle von rest_cookie_check_errors sollte ich meinen Endpunkt so ändern, dass er nicht überprüft $_GET['nonce'], sondern den Nonce-Header oder $_GET['_wpnonce']-Parameter. Richtig?
Christian
1

Diese Lösung funktioniert zwar, wird jedoch nicht empfohlen . OAuth ist die bevorzugte Wahl.


Ich denke ich habe es.

Ich denke, dass wp_verify_nonce kaputt ist, da wp_get_current_user nicht das richtige Benutzerobjekt erhält.

Es ist nicht so, wie Otto es illustriert.

Zum Glück hat es einen Filter: $uid = apply_filters( 'nonce_user_logged_out', $uid, $action );

Mit diesem Filter konnte ich Folgendes schreiben, und der JavaScript-Code wird so ausgeführt, wie er sollte:

Geben Sie hier die Bildbeschreibung ein

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

Wenn Sie ein Sicherheitsproblem mit dem Fix feststellen, rufen Sie mich bitte an. Im Moment kann ich nichts anderes als globale Probleme feststellen.

Christian
quelle
0

Wenn Sie sich diesen ganzen Code ansehen, scheint Ihr Problem die Verwendung von Verschlüssen zu sein. In der initPhase sollten Sie nur Hooks setzen und keine Daten auswerten, da nicht der gesamte Kern vollständig geladen und initialisiert wurde.

Im

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

Das $userwird voraussichtlich frühzeitig für die Schließung verwendet, aber niemand verspricht Ihnen, dass das Cookie bereits verarbeitet wurde und ein Benutzer anhand dieser Informationen authentifiziert wurde. Ein besserer Code wird sein

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

Verwenden Sie wie immer bei allen Hooks in WordPress den neuesten Hook und versuchen Sie niemals, alles vorab zu berechnen, was Sie nicht müssen.

Mark Kaplun
quelle
Ich habe den Abschnitt Aktionen und Hooks für Abfragemonitore verwendet, um herauszufinden, was ausgeführt wird und in welcher Reihenfolge set_current_user vor init und after_setup_theme ausgeführt wird. Es sollte kein Problem geben, wenn $ user außerhalb und vor Schließungen definiert wird.
Christian
@Christian, und alle von ihnen sind möglicherweise im Kontext der JSON-API nicht relevant. Ich wäre sehr überrascht, wenn der Abfragemonitor in diesem Zusammenhang funktioniert
Mark Kaplun