WordPress REST API Auth nonces in POST requests

I have a strange feeling I’ve been here before.

Let’s say you (who me?) are injecting JavaScript into a WordPress admin page that wants to do stuff with the REST API.

You can’t be bothered to set up application passwords as if you’re logged in you already have cookies that authenticate you! Application passwords are for use outside WordPress itself, and you know that you are already inside WordPress.

However, if you are using cookie-based authentication for WordPress REST API requests you need both the cookies for authentication and a nonce for actions (POST requests).

The authentication docs say this:

[the nonce] can then be passed to the API via the _wpnonce data parameter (either POST data or in the query for GET requests), or via the X-WP-Nonce header

And it turns out that the POST data approach does not work!

Testing requests in the browser

I was testing requests in the browser using the fetch API.

First you’ll need the nonce, which you can get by opening up the dev tools console once you’re logged into WordPress and type in this:

wpApiSettings.nonce

You can then use the nonce, and a known post ID (we will update the title, so make sure you’re working on production!) to so something like this:

let r = await fetch(
  '/wp-json/wp/v2/posts/12345',
  {
    body: JSON.stringify({
      title: 'Hello Moon'
    }),
    method: 'POST',
  }
);

This will fail with a “401 Unauthorized” message. If you type this in the console it will give you the full message/details:

await r.json()

It will probably say:

code: "rest_cannot_edit"
data: { status: 401 }
message: "Sorry, you are not allowed to edit this post."

That’s because you need to provide the nonce that we got at the start.

According to the docs, we can pass this in POST data, so this should work:

let r = await fetch(
  '/wp-json/wp/v2/posts/12345',
  {
    body: JSON.stringify({
      title: 'Hello Moon',
      _wpnonce: 12345678
    }),
    method: 'POST',
  }
);

But it doesn’t! We get the same result!! Grrrr.

The trick is to put the _wpnonce in the URL parameters:

let r = await fetch(
  '/wp-json/wp/v2/posts/12345?_wpnonce=12345678',
  {
    body: JSON.stringify({
      title: 'Hello Moon'
    }),
    method: 'POST',
  }
);

THAT works.

You can also use the X-WP-Nonce header, and perhaps that is just more reliable? That looks like this:

let r = await fetch(
  '/wp-json/wp/v2/posts/12345',
  {
    body: JSON.stringify({
      title: 'Hello Moon'
    }),
    method: 'POST',
    headers: {
      'X-WP-Nonce': '12345678'
    }
  }
);

I hope this helps you, and future me!