Kod jednorazowy pobrany z interfejsu API REST jest niepoprawny i różni się od kodu jednorazowego wygenerowanego w skrypcie wp_localize_script

10

Dla tych, którzy przybywają z Google: Prawdopodobnie nie powinieneś otrzymywać noncesów z interfejsu API REST , chyba że naprawdę wiesz, co robisz. Uwierzytelnianie oparte na ciasteczka z API reszta jest tylko przeznaczona dla wtyczek i tematów. W przypadku aplikacji na jedną stronę prawdopodobnie powinieneś użyć OAuth .

To pytanie istnieje, ponieważ dokumentacja nie jest / nie była jasna, w jaki sposób należy uwierzytelniać podczas tworzenia aplikacji jednostronicowych, JWT tak naprawdę nie nadają się do aplikacji internetowych, a OAuth jest trudniejszy do wdrożenia niż uwierzytelnianie oparte na plikach cookie.


Podręcznik zawiera przykład, w jaki sposób klient JavaScript Backbone obsługuje jednostki nonces, a jeśli podążę za tym przykładem, otrzymam komunikat, który akceptuje wbudowane punkty końcowe, takie jak / wp / v2 / posts.

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

]);

Jednak użycie Backbone jest wykluczone, podobnie jak motywy, więc napisałem następującą wtyczkę:

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

Trochę majstrowałem w konsoli JavaScript i napisałem:

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

Oczekiwanym rezultatem są dwa nowe posty, ale otrzymuję Cookie nonce is invalidod pierwszego, a drugi z powodzeniem tworzy post. Prawdopodobnie dlatego, że jednostki są różne, ale dlaczego? Jestem zalogowany jako ten sam użytkownik w obu żądaniach.

wprowadź opis zdjęcia tutaj

Jeśli moje podejście jest błędne, jak powinienem otrzymać ten nonce?

Edytuj :

Próbowałem zadzierać z globalnymi bez większego szczęścia . Masz trochę więcej szczęścia, korzystając z akcji 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,
      ];
    },
  ]);
});

Teraz, gdy uruchamiam JavaScript powyżej, tworzone są dwa posty, ale weryfikacja punktu końcowego kończy się niepowodzeniem!

wprowadź opis zdjęcia tutaj

Poszedłem do debugowania 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!

Dodałem trochę logowania

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

a kod JavaScript skutkuje teraz następującymi wpisami. Jak widać, po wywołaniu punktu końcowego weryfikacji uid wynosi 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
chrześcijanin
źródło

Odpowiedzi:

3

Przyjrzyj się bliżej function rest_cookie_check_errors().

Kiedy dostajesz nonce za pośrednictwem /wp-json/nonce/v1/get, nie wysyłasz nonce w pierwszej kolejności. Tak więc ta funkcja unieważnia twoje uwierzytelnianie za pomocą tego kodu:

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

Dlatego otrzymujesz inny nonce od połączenia REST w porównaniu do pobierania go z motywu. Wywołanie REST celowo nie rozpoznaje poświadczeń logowania (w tym przypadku za pomocą uwierzytelniania plików cookie), ponieważ nie wysłano prawidłowej wartości jednorazowej w żądaniu pobrania.

Powodem, dla którego działał Twój kod wp_load, było to, że otrzymałeś kod jednorazowy i zapisałeś go w globalnym, zanim ten kod spoczynku unieważnił twoje logowanie. Weryfikacja kończy się niepowodzeniem, ponieważ kod resztowy unieważnia Twój login przed weryfikacją.

Otto
źródło
Nawet nie spojrzałem na tę funkcję, ale to chyba ma sens. Chodzi o to, dlaczego powinienem dołączyć poprawną wartość jednorazową dla żądania GET? (Rozumiem teraz, ale nie jest to oczywiste) Cały punkt końcowy / Verify polega na tym, że mogę sprawdzić, czy wartość jednorazowa jest nadal ważna, a jeśli staje się nieaktualna lub nieważna, uzyskaj nową wartość jednorazową.
Christian
Na podstawie źródła rest_cookie_check_errors powinienem zmienić punkt końcowy, aby nie sprawdzał $_GET['nonce'], ale nagłówek lub $_GET['_wpnonce']parametr nonce . Poprawny?
Christian
1

Chociaż to rozwiązanie działa, nie jest zalecane . OAuth jest preferowanym wyborem.


Myślę, że rozumiem.

Myślę , że wp_verify_nonce jest uszkodzony, ponieważ wp_get_current_user nie może uzyskać odpowiedniego obiektu użytkownika.

Tak nie jest, jak ilustruje Otto.

Na szczęście ma filtr: $uid = apply_filters( 'nonce_user_logged_out', $uid, $action );

Za pomocą tego filtra udało mi się napisać, a kod JavaScript działa tak, jak powinien:

wprowadź opis zdjęcia tutaj

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

Jeśli zauważysz problem związany z poprawką, daj mi krzyk, teraz nie widzę w tym nic złego oprócz globali.

chrześcijanin
źródło
0

Patrząc na cały ten kod, wydaje się, że twoim problemem jest użycie zamknięć. Na initetapie należy jedynie ustawiać haki i nie oceniać danych, ponieważ nie wszystkie rdzenie zakończyły ładowanie i zostały zainicjowane.

W

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

$userjest związany wcześnie, aby być stosowany w zamknięciu, ale nikt nie obiecuje wam, że ciasteczka były już obsługiwane, a użytkownik został uwierzytelniony na ich podstawie. Lepszy będzie kod

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

Jak zawsze z dowolnym hakiem w Wordpress, użyj najnowszego możliwego haka i nigdy nie próbuj wstępnie obliczać niczego, czego nie musisz.

Mark Kaplun
źródło
Użyłem sekcji Działania i przechwytywania Monitory zapytań, aby dowiedzieć się, jakie przebiega i w jakiej kolejności set_current_user działa przed init i after_setup_theme, nie powinno być problemu z definiowaniem $ user na zewnątrz i przed zamknięciami.
Christian
@Christian i wszystkie z nich mogą nie być odpowiednie w kontekście json API. Byłbym bardzo zaskoczony, gdyby monitor zapytań
działał