Nonce yang diambil dari REST API tidak valid dan berbeda dari nonce yang dihasilkan di wp_localize_script

10

Bagi mereka yang datang dari Google: Anda mungkin tidak seharusnya mendapatkan nonces dari REST API , kecuali jika Anda benar - benar tahu apa yang Anda lakukan. Otentikasi berbasis cookie dengan REST API hanya dimaksudkan untuk plugin dan tema. Untuk aplikasi satu halaman, Anda mungkin harus menggunakan OAuth .

Pertanyaan ini ada karena dokumentasi tidak / tidak jelas tentang bagaimana seharusnya Anda benar-benar mengotentikasi saat membuat aplikasi satu halaman, JWT tidak benar-benar cocok untuk aplikasi web, dan OAuth lebih sulit untuk diimplementasikan daripada autentikasi berbasis cookie.


Buku pegangan ini memiliki contoh tentang bagaimana klien Backbone JavaScript menangani masalah nonses, dan jika saya mengikuti contoh tersebut, saya mendapatkan nilai negatif yang diterima oleh titik akhir bawaan seperti / wp / v2 / posts.

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

]);

Namun, menggunakan Backbone keluar dari pertanyaan, dan begitu pula tema, jadi saya menulis plugin berikut:

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

Saya bermain-main di konsol JavaScript sedikit, dan menulis yang berikut:

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

Hasil yang diharapkan adalah dua posting baru, tapi saya dapatkan Cookie nonce is invaliddari yang pertama, dan yang kedua membuat posting dengan sukses. Itu mungkin karena nonce berbeda, tetapi mengapa? Saya masuk sebagai pengguna yang sama di kedua permintaan.

masukkan deskripsi gambar di sini

Jika pendekatan saya salah, bagaimana saya harus mendapatkan kesempatan?

Edit :

Saya mencoba main-main dengan global tanpa banyak keberuntungan . Menjadi sedikit lebih beruntung dengan memanfaatkan tindakan 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,
      ];
    },
  ]);
});

Sekarang ketika saya menjalankan JavaScript di atas, dua posting dibuat, tetapi titik akhir verifikasi gagal!

masukkan deskripsi gambar di sini

Saya pergi ke debug 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!

Saya menambahkan beberapa logging

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

dan kode JavaScript sekarang menghasilkan entri berikut. Seperti yang Anda lihat, ketika titik akhir verifikasi dipanggil, uid adalah 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
Kristen
sumber

Jawaban:

3

Lihatlah lebih dekat pada function rest_cookie_check_errors().

Ketika Anda mendapatkan notce melalui /wp-json/nonce/v1/get, Anda tidak mengirim notce di tempat pertama. Jadi fungsi ini membatalkan otentikasi Anda, dengan kode ini:

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

Itu sebabnya Anda mendapatkan perbedaan dari panggilan REST Anda vs mendapatkannya dari tema. Panggilan REST sengaja tidak mengenali kredensial login Anda (dalam hal ini melalui cookie auth) karena Anda tidak mengirim data yang valid dalam permintaan dapatkan.

Sekarang, alasan kode wp_loaded Anda berfungsi adalah karena Anda mendapatkan nonce dan menyimpannya ke global sebelum kode sisanya ini membatalkan login Anda. Verifikasi gagal karena kode lainnya membatalkan login Anda sebelum verifikasi dilakukan.

Otto
sumber
Saya bahkan belum melihat fungsi itu, tapi itu mungkin masuk akal. Masalahnya, mengapa saya harus menyertakan notce yang valid untuk permintaan GET? (Saya mengerti sekarang, tetapi masih jauh dari jelas) Inti dari titik akhir / verifikasi adalah bahwa saya dapat memeriksa apakah angka tetap masih valid, dan jika basi atau tidak valid, dapatkan nilai baru.
Christian
Berdasarkan sumber rest_cookie_check_errors, saya harus mengubah titik akhir saya sehingga tidak memeriksa $_GET['nonce'], tetapi header atau $_GET['_wpnonce']parameter nonce . Benar?
Christian
1

Meskipun solusi ini berfungsi, tidak disarankan . OAuth adalah pilihan yang lebih disukai.


Saya rasa saya mengerti.

Saya pikir wp_verify_nonce rusak, karena wp_get_current_user gagal mendapatkan objek pengguna yang tepat.

Tidak seperti yang diilustrasikan oleh Otto.

Untungnya memiliki filter: $uid = apply_filters( 'nonce_user_logged_out', $uid, $action );

Dengan menggunakan filter ini, saya dapat menulis yang berikut ini, dan kode JavaScript dijalankan seperti seharusnya:

masukkan deskripsi gambar di sini

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

Jika Anda menemukan masalah keamanan dengan perbaikannya, tolong beri saya teriakan, saat ini saya tidak dapat melihat ada yang salah dengan perbaikannya, selain global.

Kristen
sumber
0

Melihat semua kode ini, sepertinya masalah Anda adalah penggunaan penutupan. Pada inittahap Anda hanya perlu mengatur kait dan tidak mengevaluasi data karena tidak semua inti telah selesai memuat dan diinisialisasi.

Di

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

yang $userterikat awal untuk digunakan dalam penutupan, tapi tidak ada janji-janji kepada Anda bahwa cookie sudah ditangani dan pengguna telah dikonfirmasi didasarkan pada mereka. Kode yang lebih baik adalah

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

Seperti biasa dengan kait apa pun di wordpress, gunakan kait terbaru yang mungkin dan jangan pernah mencoba untuk menghitung ulang apa pun yang tidak harus Anda lakukan.

Mark Kaplun
sumber
Saya menggunakan bagian Tindakan & kait Monitor Query untuk mengetahui apa yang berjalan dan dalam urutan mana, set_current_user berjalan sebelum init & after_setup_theme, seharusnya tidak ada masalah dengan $ user didefinisikan di luar & sebelum penutupan.
Christian
@Christian, dan semuanya mungkin tidak relevan dalam konteks json API. Saya akan sangat terkejut jika monitor permintaan berfungsi dalam konteks itu
Mark Kaplun