Configuring JWT authentication for the WordPress REST API to power headless React apps

How to configure JWT authentication for the WordPress REST API

To configure JWT authentication for the WordPress REST API you need to install a JWT plugin, add a secret key to wp-config.php, adjust your server’s rewrite rules, generate a token, and then include that token in the Authorization header of every request your head‑less React app makes.

Prerequisites

  • WordPress 6.4 or later, running on Apache or Nginx.
  • Access to the site’s file system and the ability to edit .htaccess (Apache) or the server block (Nginx).
  • PHP 7.4 or later.
  • A React project that will call the WordPress REST endpoints.

Step 1 – Install the JWT Authentication plugin

  1. From the WordPress admin dashboard go to Plugins → Add New.
  2. Search for “JWT Authentication for WP‑REST API”. The current stable version is 2.5.0.
  3. Click Install Now and then Activate.

The plugin registers two new routes:

  • /wp-json/jwt-auth/v1/token – POST request to obtain a token.
  • /wp-json/jwt-auth/v1/token/validate – GET request to validate an existing token.

Step 2 – Add a secret key to wp-config.php

Edit the root wp-config.php file and add a unique phrase. It must be a long, random string; you can generate one with openssl rand -base64 32 on the command line.

// wp-config.php
define('JWT_AUTH_SECRET_KEY', 'your‑random‑base64‑string‑here');
define('JWT_AUTH_CORS_ENABLE', true); // optional, enables CORS for the token route

Do not expose this key in version control. Keep it private on the server.

Step 3 – Configure the web server

Apache

If you are using Apache, the plugin requires the Authorization header to be passed to PHP. Add the following lines to the site’s .htaccess file, directly above the WordPress rewrite block.

# .htaccess – pass Authorization header to PHP
RewriteEngine On
RewriteCond %{HTTP:Authorization} ^(.*)
RewriteRule ^(.*)$ - [E=HTTP_AUTHORIZATION:%1]

Nginx

For Nginx, add the header forwarding directive inside the location / block.

# nginx.conf – forward Authorization header
location / {
    try_files $uri $uri/ /index.php?$args;
    proxy_set_header Authorization $http_authorization;
}

Step 4 – Test token generation

Use curl or a tool like Postman to request a token. Replace example.com with your domain and admin/password with a valid user’s credentials.

curl -X POST http://example.com/wp-json/jwt-auth/v1/token \
     -d "username=admin" \
     -d "password=password"

A successful response looks like:

{
  "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
  "user_email": "admin@example.com",
  "user_nicename": "admin",
  "user_display_name": "admin"
}

Copy the token value; it will be sent with every API request.

Step 5 – Use the token in a React fetch call

Below is a minimal example using the native fetch API. The token is stored in localStorage after the login request.

// src/api.js – React helper
export async function login(username, password) {
  const response = await fetch('https://example.com/wp-json/jwt-auth/v1/token', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ username, password })
  });
  const data = await response.json();
  if (response.ok) {
    localStorage.setItem('jwt', data.token);
    return data;
  }
  throw new Error(data.message || 'Login failed');
}

export async function fetchProtected(endpoint) {
  const token = localStorage.getItem('jwt');
  const response = await fetch(`https://example.com/wp-json${endpoint}`, {
    method: 'GET',
    headers: {
      'Authorization': `Bearer ${token}`
    }
  });
  if (response.status === 401) {
    // token invalid or expired – handle re‑login
    throw new Error('Unauthorised – token may have expired');
  }
  return response.json();
}

Call fetchProtected('/wp/v2/posts') to retrieve posts that require authentication.

Step 6 – Handling token expiry

The JWT plugin does not provide a refresh endpoint out of the box. The usual approach is to store the token’s expiry timestamp (available in the token payload) and, when the current time exceeds it, force the user to log in again. You can decode the payload client‑side without verification:

function getTokenExpiry(token) {
  const payload = JSON.parse(atob(token.split('.')[1]));
  return payload.exp; // Unix timestamp
}

If Date.now() / 1000 > getTokenExpiry(token) you should clear localStorage and redirect to the login page.

Step 7 – Common pitfalls and how to avoid them

  • Missing Authorization header on Apache. Without the rewrite rule in .htaccess the header never reaches PHP, resulting in “Invalid token” errors.
  • CORS errors. The plugin disables CORS by default. Setting JWT_AUTH_CORS_ENABLE to true in wp-config.php allows cross‑origin requests from your React app. If you need tighter control, configure the Access-Control-Allow-Origin header in your server block.
  • Using a non‑HTTPS endpoint. JWT tokens are signed but not encrypted; transmitting them over HTTP exposes them to sniffing. Always serve the token endpoint and the REST API over HTTPS.
  • Incorrect user role. By default the plugin allows any user with the read capability to obtain a token. If you want to restrict token issuance to administrators, add a filter in functions.php:
// functions.php – restrict token generation
add_filter('jwt_auth_token_before_dispatch', function($data, $user) {
    if (!user_can($user, 'manage_options')) {
        return new WP_Error('jwt_auth_invalid_user', 'User does not have permission', array('status' => 403));
    }
    return $data;
}, 10, 2);

Step 8 – Securing the API further

If you need fine‑grained control, combine JWT authentication with WordPress capabilities. In your custom REST route callback you can check the token’s user ID and verify capabilities with current_user_can(). This ensures that even a valid token cannot access resources the user is not authorised for.

Testing the full flow

  1. Run the login function in your React app and confirm a token is stored.
  2. Open the browser’s developer tools, go to the Network tab, and verify that the Authorization: Bearer … header is present on a request to /wp/v2/posts.
  3. Inspect the response; you should see a JSON array of posts. If you receive a 401, check the server logs for “JWT token not found” or “Invalid token”.
  4. Force token expiry by deleting the token from localStorage and repeat step 2 to ensure your error handling works.

Conclusion

By installing the JWT Authentication plugin, adding a secret key, configuring the web server to forward the Authorization header, and attaching the token to each fetch request, you can securely expose WordPress data to a headless React front‑end without exposing the admin UI.

Illustration of a LibraFire team member on a video call with a client

How can we assist you?

Contact Us