Technote

Security Practical

Series The REST API and going headless Part 4 of 8

Choosing authentication: cookies, application passwords, tokens

These three differ in threat model, not convenience. Whether the caller is a browser, your own server, or a third party decides the answer for you.

Once a route exists you have to decide who may call it. WordPress gives you three practical options, and they differ not in convenience but in where the secret lives.

Three methods — who the caller is decides which one applies

Cookies and nonces — when a browser calls

This is for a logged-in user’s browser calling the same site’s API. The cookie authenticates; the nonce is not authentication at all but CSRF protection. Conflating the two leads to bad decisions — a nonce is not a secret, and it is perfectly fine printed in the page source.

wp_enqueue_script( 'wper-app', $url, [], $ver, true );

wp_localize_script(
	'wper-app',
	'wperApi',
	[
		'root'  => esc_url_raw( rest_url( 'wper/v1/' ) ),
		'nonce' => wp_create_nonce( 'wp_rest' ),
	]
);

// Client: fetch( wperApi.root + 'orders/12', {
//   credentials: 'same-origin',
//   headers: { 'X-WP-Nonce': wperApi.nonce }
// } )

The trap here is that nonces expire. Press save in a tab left open overnight and you get a 403 with rest_cookie_invalid_nonce. To the user that reads as “it worked a moment ago”, so the client has to catch that error and prompt a reload, or refresh the nonce through the heartbeat.

The threat model is clear: the secret is the browser session, so a successful XSS is the end of the story. Output escaping matters most precisely on the screens that use this method.

Application passwords — when a server calls

Another server or a script has no cookie to send. Core application passwords exist for this. You issue one per application from the user profile, send it over HTTP Basic, and revoke it individually without touching anything else.

curl -u 'ci-bot:abcd EFGH ijkl MNOP qrst UVWX' 
     https://example.com/wp-json/wp/v2/posts?status=draft

Three properties matter. First, it cannot exceed the user’s own capabilities — so create a dedicated automation user with the smallest role that works. Second, it is separate from the account password, so a leak costs you one credential rather than the account. Third, HTTPS is a precondition: Basic auth puts the credential straight into a header.

The threat model is “a long-lived credential sitting in a config file”. Keeping it out of the repository and deciding the rotation procedure in advance is the homework this method sets.

Tokens — when a third party issues them

Tokens belong where a mobile app or a separate front end calls on behalf of its own users, or where you genuinely need short-lived credentials. This is not core, so a plugin or your own implementation fills the gap — and at that moment you begin operating an issuer and a refresh flow.

Worth being honest here: tokens are not safer because they are more modern. Short expiry narrows the window of a leak, but it buys you refresh-token storage, revocation lists and key rotation — new operational load. If one server posts articles on a nightly batch, an application password is the right answer and a token is overkill.

Pick by caller — the last two lines need a different answer

The wider view on hardening lives in the Security archive, and if you would like authentication and permissions reviewed alongside everything else, a diagnostic is part of our optimization program.

Next part

The fourth line of that list is the next part. A webhook from an external service carries no cookie and no nonce, so a nonce check fails one hundred per cent of the time. We will look at what to verify instead.

More on this topic

All technotes

Security Practical

The first hour after you discover a compromise

The most common mistake in this moment is deleting things in a hurry. Here is what to stop first, what to preserve, and in what order to recover…

Founders 8 min read

₩270,000 · Join the program