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


10

Googleから届いたものの場合:何をしているのか本当に理解していない限り、REST APIからナンスを取得すべきではありません。REST APIを使用したCookieベースの認証は、プラグインとテーマのみ対象としています。シングルページアプリケーションの場合は、おそらくOAuthを使用する必要があります。

この質問が存在するのは、単一ページのアプリを構築するときに実際に認証する方法がドキュメントで明確になっていない、または明確ではなかったため、JWTは実際にはウェブアプリに適しておらず、OAuthはCookieベースの認証よりも実装が難しいためです。


このハンドブックには、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番目の投稿は正常に投稿を作成します。それはおそらくナンスが異なるためですが、なぜですか?両方のリクエストで同じユーザーとしてログインしています。

ここに画像の説明を入力してください

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

編集

運が悪いのにグローバルいじってみました。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つの投稿が作成されますが、検証エンドポイントが失敗します。

ここに画像の説明を入力してください

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

回答:


3

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

を介してnonceを取得する場合/wp-json/nonce/v1/get、そもそもnonceを送信するわけではありません。したがって、この関数は次のコードで認証を無効にします。

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コードが機能した理由は、この残りのコードがログインを無効にする前にナンスを取得してグローバルに保存したためです。検証が行われる前に残りのコードがログインを無効にするため、検証は失敗します。


私はその機能を見たこともありませんが、おそらくそれは理にかなっています。問題は、GETリクエストに有効なナンスを含める必要があるのはなぜですか?(私は今それを手に入れましたが、明白ではありません)/ verifyエンドポイントの重要な点は、ナンスがまだ有効であるかどうかをチェックでき、それが古くなっているか無効である場合は、新しいナンスを取得することです。
クリスチャン

rest_cookie_check_errorsのソースに基づいて、チェックしないようにエンドポイントを変更する必要があります$_GET['nonce']が、ノンスヘッダーまたは$_GET['_wpnonce']パラメーターです。正しい?
クリスチャン

1

このソリューションは機能しますが、お勧めできません。OAuthをお勧めします。


わかったと思います。

wp_get_current_userが適切なユーザーオブジェクトを取得できないため、wp_verify_nonceが壊れていると思います。

Ottoが示すように、そうではありません。

幸いにもフィルターがあります: $uid = apply_filters( 'nonce_user_logged_out', $uid, $action );

このフィルターを使用すると、次のように記述でき、JavaScriptコードは本来のように実行されます。

ここに画像の説明を入力してください

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

修正によるセキュリティの問題を見つけた場合は、一言お願いします。現時点では、グローバル以外の問題は確認できません。


0

このすべてのコードを見ると、問題はクロージャーの使用にあるようです。で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は早期にクロージャで使用されるようにバインドされていますが、Cookieがすでに処理され、ユーザーがそれらに基づいて認証されたことを誰もあなたに約束しません。より良いコードは

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

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


Query Monitorsのアクションとフックのセクションを使用して、何がどの順序で実行されるかを確認しました。set_current_userはinit&after_setup_themeの前に実行されます。$ userがクロージャーの外と前に定義されていることで問題は発生しません。
クリスチャン

@Christian、およびそれらのすべては、json APIのコンテキストに関連しない場合があります。クエリモニタは、そのコンテキストで動作する場合、私は非常に驚かれることでしょう
マークKaplunを
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.