web-dev-qa-db-ja.com

から取得したノンス REST APIが無効であり、wp_localize_scriptで生成されたnonceとは異なります

Googleからやってくる人たちのために:あなた はおそらくREST AP​​Iからナンスを取得すべきではありません あなたが本当にあなたがしていることを知らない限り。 REST AP​​Iを使用したCookieベースの認証は、プラグインとテーマに対してのみを意味します。単一ページのアプリケーションの場合は、おそらく OAuth を使用する必要があります。

この質問が存在するのは、シングルページアプリケーションを構築する際にどのように実際に認証するべきかについてのドキュメントが明確ではなかったため、JWTがWebアプリケーションにはあまり適していないためです。


ハンドブック には、Backbone JavaScriptクライアントがノンスを処理する方法の例があり、この例に従うと、次のような組み込みエンドポイントのノンスが得られます。/wp/v2/postsは受け入れます。

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

]);

しかし、Backboneを使用することは問題外であり、テーマもそうであるので、私は次のプラグインを書きました:

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

私はJavaScriptコンソールをちょっとひねり、次のように書きました:

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

期待される結果は2つの新しい投稿ですが、私は最初の投稿からCookie nonce is invalidを取得し、2番目の投稿は正常に投稿を作成します。それはおそらく、ナンスが違うからです。両方のリクエストで同じユーザーとしてログインしています。

enter image description here

私のアプローチが間違っている場合、どのように私はノンスを取得する必要がありますか?

編集

グローバルをめちゃくちゃにした 運が悪い。 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,
      ];
    },
  ]);
});

上記のJavaScriptを実行すると、2つの投稿が作成されますが、検証エンドポイントは失敗します。

enter image description here

私は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!

ロギングを追加しました

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

javaScriptコードは次のようになります。ご覧のとおり、検証エンドポイントが呼び出されると、uidは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

function rest_cookie_check_errors()をよく見てください。

/wp-json/nonce/v1/getを介してナンスを取得した場合、最初からノンスを送信していません。そのため、この関数はこのコードであなたの認証を無効にします。

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

それがあなたがあなたのREST呼び出しとは異なるナンスを得ているのに対し、テーマからそれを得ている理由です。 getリクエストで有効なnonceを送信しなかったため、REST呼び出しは意図的にあなたのログイン認証情報を認識していません(この場合はcookie authを介して)。

さて、あなたのwp_loadedコードがうまくいった理由は、この休止コードがあなたのログインを無効にする前にあなたがnonceを得てそれをグローバルに保存したからです。検証が行われる前に残りのコードがログインを無効にするため、検証は失敗します。

3
Otto

この解決策は機能しますが、 お勧めできません 。 OAuthが好ましい選択です。


私はそれを手に入れたと思います。

I と思う wp_get_current_userが適切なユーザーオブジェクトを取得できなかったので、wp_verify_nonceは壊れています。

オットーが示すように、そうではありません。

幸いなことに、それはフィルタを持っています:$uid = apply_filters( 'nonce_user_logged_out', $uid, $action );

このフィルタを使用して私は次のように書くことができました、そしてJavaScriptコードはそれがあるべきように実行されます:

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

あなたがこの修正でセキュリティ上の問題を発見した場合は、私に一言お願いします。今、私はそれ以外に、グローバル以外の何も問題がないと考えることはできません。

1
Christian

このコードをすべて見ると、あなたの問題はクロージャの使用にあるようです。 init段階では、すべてのコアがロードおよび初期化を完了していないので、フックを設定し、データを評価しないでください。

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

$userはクロージャで使用されることが早い時期に制限されていますが、クッキーが既に処理されており、それに基づいてユーザーが認証されていることを保証する人は誰もいません。より良いコードは

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

ワードプレスのフックはいつもそうであるように、可能な限り最新のフックを使用し、あなたがする必要がないものは何も事前計算しないでください。

0
Mark Kaplun