Why wp_verify_nonce() Fails in WordPress REST API Endpoints

So you’ve created a custom form on WordPress which you’ve protected with a wp_nonce_field()

?>
<form
	action="<?php echo esc_url( rest_url( REST_API_NAMESPACE_V1 . '/subscribe' ) ); ?>"
	method="post"
>
	<label for="new-subscriber-email">Subscribe to our newsletter:</label>
	<input id="new-subscriber-email" name="new_subscriber_email" type="email" />
	<?php wp_nonce_field( 'new_subscriber', 'nonce', true, true ); ?>
	<input name="new_subscriber_submit" type="submit" value="Subscribe" />
</form>
<?php

On the backend, you then call wp_verify_nonce() to validate the request to your custom WordPress REST API endpoint…

<?php
register_rest_route(
	REST_API_NAMESPACE_V1,
	'/subscribe',
	array(
		array(
			'methods'             => 'POST',
			'callback'            => array( __CLASS__, 'handle_post_subscribe' ),
			'permission_callback' => '__return_true',
			'args'                => array(
				'nonce' => array(
					'type'              => 'string',
					'required'          => true,
					'sanitize_callback' => 'sanitize_text_field',
					'validate_callback' => function( $value, $request, $param ) {
						return in_array(
							wp_verify_nonce( $value, 'new_subscriber' ),
							array( 1, 2 ),
							true
						);
					},
				),
			),
		),
	)
);

Perfect! When you go to test the form submission, however, you sometimes get an HTTP 400 response. That’s odd. Why does this happen?

Well, it’s actually a very simple problem that even the most experienced WordPress developers can get puzzled by! All that’s missing is an important single line of code. But, first, let me quickly explain how WordPress nonces work in the first place and how that relates to the WordPress REST API.

How WordPress Generates Nonce Values

If you review the source code of wp_create_nonce(), you’ll see that WordPress hashes various values together to create the nonce. In particular, the current user’s ID from wp_get_current_user() and the current user’s session token from wp_get_session_token() are part of the hash.

This means that nonces are tied to the current user’s identity.

How WordPress Authenticates REST API Requests

You may be surprised to find that WordPress does not recognize the logged_in cookie by default when processing REST API requests. The Authentication section of WordPress’s REST API Handbook states:

For developers making manual Ajax requests, the nonce will need to be passed with each request. The API uses nonces with the action set to wp_rest. These 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. If no nonce is provided the API will set the current user to 0, turning the request into an unauthenticated request, even if you’re logged into WordPress. […] It is important to keep in mind that this authentication method relies on WordPress cookies. As a result this method is only applicable when the REST API is used inside of WordPress and the current user is logged in.

This means the WordPress REST API will not recognize the logged in user unless the request includes the wp_rest nonce value as the _wpnonce data parameter or the X-WP-Nonce header value.

When no user is logged into WordPress, wp_get_current_user() returns a WP_User with ID 0 (zero) and no capabilities. This expresses an anonymous, public user, which is what the WordPress REST API uses by default.

How wp_verify_nonce() Fails in Custom WordPress REST API Endpoints

Okay, so how does all of this tie together? Well, when you display wp_nonce_field() in your theme template file, shortcode output, or otherwise hook into WordPress’s display of the current page, the logged-in WordPress user is automatically recognized. This means that the nonce field is calculated with that user’s ID and logged_in cookie token.

When that nonce value is then passed to the WordPress REST API backend, wp_verify_nonce() is then validating the value for the default, anonymous user with ID 0 (zero) and an empty logged_in cookie token.

This means the ingredients for the nonce are different, thus validation fails and the WordPress REST API returns an HTTP 400 response code.

The Solution

If you’re going to perform identity-dependent actions within WordPress REST API endpoints, remember to properly authenticate the requests! This may be as simple as including the following nonce field in your form, depending on your processing:

wp_nonce_field( 'wp_rest', '_wpnonce', true, true );

Remember, though, that this nonce only helps determine the current user’s identity within WordPress REST API endpoints. I recommend that you still additionally output and verify your own custom nonce to ensure the user wanted to perform the specified action.

Michelle Blanchette

Michelle Blanchette

Full-stack web developer, specialized in WordPress and API integrations. Currently focused on building Completionist, the Asana integration WordPress plugin.