WordPress REST-API nonce-sense.

Working with the WordPress REST-API is HELL. There. I said it. It is powerful, it is secure, it is everything a developer needs, but for the love of [fill in your favorite deity here], WordPress, be consistent!

Using the REST-API requires authentication. Well, that’s not a problem. Just create a route to log-in and one to log-out. WordPress has functions to do that.

wp_signon()
and
wp_logout()

The first hurdle is getting the WordPress REST API to function. Oh, wait, you need a nonce ?! Well, thank you WordPress for this ‘security’-measure. For everything else in WordPress the authentication cookies you get when logging in to /wp-admin are enough, but for REST-API you need a nonce … the F why !?

Sorry, but this is just NONCE-SENSE! Pun intented. If only it were funny.

To get nonces to actually work, you need something like this:
(over simplified, please make sure you do this un-hackable ^^)

Somewhere in PHP

<?php add_action('wp_head', function(){
print '<script>window.nonce = '. json_encode(wp_create_nonce( 'wp_rest' )) .'</script>';
}); ?>

and in your application.js

$(document).ajaxSend(function (event, xhr, settings) {
xhr.setRequestHeader('X-WP-Nonce', window.nonce);
});

Now you can send the nonce to WordPress. Great. But then it works once you are already logged in or not. It stops working on a change of log-in status. After log-in, the nonce is no longer valid and a new nonce is needed. Same after log-out.

Simple solution: in your REST response, send a new nonce, and update the nonce-variable in javascript accordingly.

Somewhere in PHP

<?php add_filter( 'rest_post_dispatch', function( WP_REST_Response $response) {
$response->header('X-WP-Nonce', wp_create_nonce( 'wp_rest' )); return $response;
}, PHP_INT_MAX);?>

and in your application.js

$(document).ajaxComplete(function (event, xhr, settings) {
window.nonce = xhr.getResponseHeader('X-WP-Nonce');
});

But wait; just generating a new nonce will not work!

Why not?

on wp_signon(), WordPress sets new cookies. but WordPress only uses set_cookie() to do that. In other words; the new cookies are valid only on consequent page loads.
wp_create_nonce() uses the cookie-values in memory (PHP $_COOKIE) which … is NOT refreshed after log-in.
wp_logout() has the same issue. WordPress sends empty cookies, but does not update the local memory, again, requiring a page reload.

Solution:

Do what WordPress should do, but doesn’t.

<?php
add_action('set_logged_in_cookie', function($cookie_value){
$_COOKIE[ LOGGED_IN_COOKIE ] = $cookie_value;
}, PHP_INT_MAX); add_action('clear_auth_cookie', function(){
$_COOKIE[ LOGGED_IN_COOKIE ] = ' ';
});

With this, the used $_COOKIE variable is now identical to the cookie.

But this does not work… yet…

Why?

Well, wp_create_nonce() depends on more than just the cookie. It depends on the current-user also. The global variable $current_user holds the current user. So it would be safe to assume that on wp_signon(), that variable is updated, right? Well that assumption would be wrong!

wp_signon() does NOT set the global user object to the new user. And, failing consistently, wp_logout() does NOT invalidate the global user object.

Again, more hooks just to make WordPress do what it should do out of the box;

<?php
add_action('wp_login', function($login, $user){
wp_set_current_user( $user->ID );
}, PHP_INT_MAX, 2); add_action('wp_logout', function(){
wp_set_current_user( 0 );
}, PHP_INT_MAX);

The full code for your enjoyment:

P.s. I repeat: this code is perhaps not secure, perhaps over-simplified, but at least, with this, the REST-API is usable. The alternative is reverting to 1990s AJaX calls through wp-admin/admin-ajax.php.

Author: Remon Pel

WebDeveloper though not WebDesigner

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.