Skip to main content

Headless WordPress: JWT Auth Done Right

· 8 min read
Nicu Micle
Creator of Simple JWT Login

Headless WordPress has gone mainstream. Teams reach for it when they want WordPress's content management experience paired with a modern front-end - React, Next.js, Vue, or a mobile app. The REST API makes that possible, but it leaves one critical piece unresolved: authentication.

WordPress's built-in auth is cookie-based and browser-centric. It doesn't translate cleanly to API-first architectures. That's the gap Simple JWT Login fills, and in this article I'll walk through a complete, realistic setup.

What We're Building​

A headless WordPress backend with:

  • JWT-based login (email + password → token)
  • Token refresh before expiry
  • Protected REST endpoints that require a valid JWT
  • User registration via API
  • Password reset flow
  • Auto-login links for email campaigns

All of this is handled by Simple JWT Login with zero custom PHP beyond optional hooks.


Installation and Initial Configuration​

Install Simple JWT Login from the WordPress plugin repository, then navigate to Simple JWT Login in your WordPress admin sidebar.

The first setting to configure is the JWT Decryption Key - this is the secret used to sign and verify tokens. Treat it like a database password: long, random, and stored in a secrets manager rather than hardcoded.

Settings > Simple JWT Login > General > JWT Decryption Key

Next, select your algorithm. For most setups HS256 (HMAC SHA-256) is the right default. If you need asymmetric signing - for example, to let a third-party service verify tokens without knowing the secret - switch to RS256 and configure your public/private key pair.

Set a reasonable JWT expiration time. Sixty minutes is a sensible starting point for most web apps; mobile apps often use longer windows paired with token refresh.


Generating a Token​

Once the plugin is active, your WordPress site immediately has an authentication endpoint:

curl -X POST "https://example.com/wp-json/simple-jwt-login/v1/auth" \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "their_password"
}'

A successful response looks like this:

{
"success": true,
"data": {
"jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"ID": 42,
"user_email": "user@example.com",
"display_name": "Jane Doe"
}
}
}

Store this token client-side (memory or an HttpOnly cookie - avoid localStorage for sensitive apps) and attach it to subsequent requests.

The plugin also supports username and a combined login field (email or username) for the initial auth call, configurable under General > Login by.


Making Authenticated Requests​

Once you have a token, include it in the Authorization header on every protected request:

curl "https://example.com/wp-json/wp/v2/users/me" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

The plugin also accepts the token as a query parameter (?JWT=<token>) or in the request body ({"JWT": "<token>"}), which is handy for scenarios where setting custom headers is awkward - like certain webhook consumers.


Refreshing Tokens​

Short-lived tokens are more secure, but they require your client to handle expiry gracefully. Simple JWT Login provides a dedicated refresh endpoint:

curl -X POST "https://example.com/wp-json/simple-jwt-login/v1/auth/refresh" \
-H "Content-Type: application/json" \
-d '{"JWT": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}'

The response is a new token with a fresh expiry. A typical front-end pattern: check the token's exp claim before each request, and if it's within a few minutes of expiry, refresh proactively rather than reacting to a 401.


Protecting Your REST Endpoints​

A headless WordPress site often exposes more REST routes than intended. Simple JWT Login's Endpoint Protection feature lets you require a valid JWT for any route.

The most secure posture for a private API: enable "Protect all endpoints" and then whitelist only the routes that must be public.

Settings > Simple JWT Login > Protect Endpoints
Mode: Protect all WordPress Endpoints

For a typical blog-style API you might whitelist:

  • GET /wp-json/wp/v2/posts - public post listing
  • GET /wp-json/wp/v2/categories - taxonomy data
  • POST /wp-json/simple-jwt-login/v1/auth - login itself

Every other route - user data, media uploads, post creation - requires a valid JWT.

You can also protect routes by HTTP method. If your front-end needs GET on posts to be public but POST (creating posts) to be authenticated, that's a single checkbox per method. No custom middleware needed.


User Registration via API​

If your application handles its own onboarding flow, you can create WordPress users directly through the API. First, enable registration under:

Settings > Simple JWT Login > Register User

For security, always pair registration with an Auth Code - an API key that must accompany registration requests. This prevents anyone with your API URL from creating arbitrary accounts.

curl -X POST "https://example.com/wp-json/simple-jwt-login/v1/users" \
-H "Content-Type: application/json" \
-d '{
"email": "newuser@example.com",
"password": "initial_password",
"first_name": "Jane",
"last_name": "Doe",
"auth_code": "YOUR_REGISTRATION_AUTH_CODE"
}'

You can also configure the plugin to generate a random password and return it in the response - useful if you want to issue a temporary password and immediately prompt the user to change it.

IP address restrictions and email domain allowlists add further layers of control over who can register.


Password Reset Flow​

The plugin ships a full password reset flow accessible via API, which is often missing from DIY JWT implementations.

Step 1 - Request a reset code:

curl -X POST "https://example.com/wp-json/simple-jwt-login/v1/users/reset_password" \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com"}'

Step 2 - Submit the new password with the code:

curl -X PUT "https://example.com/wp-json/simple-jwt-login/v1/users/reset_password" \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"code": "RESET_CODE_FROM_EMAIL",
"new_password": "new_secure_password"
}'

Three reset modes are available:

  • Silent: The reset code is returned directly in the API response (for custom email delivery).
  • Default WordPress email: Uses WordPress's built-in email template.
  • Custom email template: You define the subject and body using variables like {{CODE}}, {{NAME}}, and {{EMAIL}}.

The custom template mode is ideal for headless apps that have their own transactional email design system and don't want WordPress's default styling.


One of the most underrated features: generate a URL that logs a user in automatically when clicked.

Enable auto-login under:

Settings > Simple JWT Login > Auto Login

Then generate a JWT for the target user and construct the URL:

https://example.com/?JWT=<user_jwt>&redirectUrl={{site_url}}/account/dashboard

When the user clicks that link, Simple JWT Login authenticates them silently and redirects them to their dashboard - already logged in.

The redirectUrl parameter supports dynamic variables ({{user_id}}, {{user_email}}, {{user_first_name}}, and more), so each link can route the user to a personalized destination. Pair this with the MailPoet add-on and you have one-click autologin directly inside email campaigns without a single line of custom code.


Enriching the JWT with Custom Data​

Out of the box, the JWT payload contains standard claims (sub, iat, exp). For most front-ends you'll want to include additional user data to avoid extra API calls after login.

Use the simple_jwt_login_jwt_payload filter:

add_filter('simple_jwt_login_jwt_payload', function($payload, $user) {
$payload['display_name'] = $user->display_name;
$payload['roles'] = $user->roles;
$payload['avatar'] = get_avatar_url($user->ID);
return $payload;
}, 10, 2);

Your front-end can now decode the JWT and immediately render the user's name and avatar without an additional /users/me request.


Revoking Tokens​

When a user logs out or changes their password, you'll want to invalidate their existing tokens. Simple JWT Login provides a revoke endpoint:

curl -X DELETE "https://example.com/wp-json/simple-jwt-login/v1/auth/revoke" \
-H "Authorization: Bearer <token>"

Revoked tokens are blacklisted server-side and will be rejected on subsequent requests, even if they haven't technically expired yet.


A Note on CORS​

If your front-end is served from a different domain than WordPress - which is almost always the case in headless setups - you'll need CORS headers. Simple JWT Login can add Access-Control-Allow-Origin: * automatically:

Settings > Simple JWT Login > General > Allow CORS

For production, you'll typically want to restrict this to your front-end domain via your web server config (Apache or Nginx), using the plugin's CORS setting as a development convenience only.


Conclusion​

Simple JWT Login takes what would otherwise be hundreds of lines of custom authentication code and turns it into a configuration exercise. Token generation, refresh, revocation, endpoint protection, user registration, password resets, and auto-login are all covered out of the box.

For teams building headless WordPress - whether the front-end is React, Next.js, Vue, or a native mobile app - it's one of the highest-leverage plugins available. The documentation is thorough, the plugin is actively maintained, and the hooks system means you're never painted into a corner when requirements get complex.

Install it, spend an hour on the settings, and your WordPress REST API will have authentication that actually fits modern development patterns.