How to authenticate with JWT for REST and configure CORS in WordPress

Contents

Overview

This article is a complete, detailed tutorial for authenticating WordPress REST API requests with JWT (JSON Web Token) and configuring CORS so browser clients can call the API from other origins. It covers installing and configuring a popular JWT plugin, server and WordPress configuration changes required for Authorization headers and preflight requests, client examples (curl, fetch, axios, PHP), protecting custom REST endpoints, adjusting token payload and expiration, implementing refresh tokens and token revocation strategies, and common troubleshooting and security best practices.

Prerequisites

  • WordPress 5.x or newer (REST API available by default)
  • Server access to modify wp-config.php, .htaccess (Apache) or Nginx configuration, and possibly php-fpm settings
  • Ability to install WordPress plugins (admin access)
  • HTTPS configured (strongly recommended and effectively required for secure bearer tokens)

Recommended plugin

A widely-used plugin is JWT Authentication for WP REST API (commonly referenced as jwt-auth). You can install it from the WordPress plugin directory or GitHub. Example GitHub link: https://github.com/Tmeister/wp-api-jwt-auth

Step 1 — Install and configure the JWT plugin

  1. Install and activate the JWT plugin from the Plugins screen or manually upload it to wp-content/plugins.
  2. Set a secret key in wp-config.php to sign the tokens. The plugin expects a constant like JWT_AUTH_SECRET_KEY (name can vary by fork — check the plugin readme). Add to wp-config.php above the Thats all comment:
    
        

    Use a long random value (at least 32 characters) and ensure it is kept secret.

  3. Optionally configure token expiry or extra claims via plugin-provided filters (examples later).

Step 2 — Ensure Authorization header reaches PHP

Many servers strip the Authorization header before PHP sees it. You must forward Authorization to PHP so the plugin can read Bearer tokens.

Apache (.htaccess) solution

# In the sites .htaccess (top of WordPress rules)
RewriteEngine On
RewriteRule . - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
# Alternatively:
SetEnvIf Authorization (.) HTTP_AUTHORIZATION=1

Nginx / php-fpm solution

# In your server/location block that handles WordPress /wp-json
location / {
    # pass Authorization header to PHP
    fastcgi_param HTTP_AUTHORIZATION http_authorization
    # existing fastcgi_pass / php-fpm settings...
}
# If proxying to a backend, forward the header:
proxy_set_header Authorization http_authorization

Some environments (e.g. CGI) may need additional changes

If you use a managed host, check their docs or contact support to ensure the Authorization header is preserved to PHP.

Step 3 — How to obtain a JWT token

Most JWT plugins expose an endpoint like /wp-json/jwt-auth/v1/token. You POST username and password, and receive a JSON response containing the token and user info.

Example: curl

curl -X POST https://example.com/wp-json/jwt-auth/v1/token 
  -H Content-Type: application/json 
  -d {username:alice,password:hunter2}

Typical JSON response

{
  token: eyJ0eXAiO...your.jwt.token...lQ,
  user_email: alice@example.com,
  user_nicename: alice,
  user_display_name: Alice Example
}

Example: fetch (browser)

fetch(https://example.com/wp-json/jwt-auth/v1/token, {
  method: POST,
  headers: {Content-Type: application/json},
  body: JSON.stringify({ username: alice, password: hunter2 })
})
  .then(r => r.json())
  .then(data => {
    // store data.token securely client-side (see security notes)
    console.log(JWT token, data.token)
  })

Example: PHP (server-side)

alice,password=>hunter2]))
response = curl_exec(ch)
curl_close(ch)
data = json_decode(response, true)
token = data[token] ?? null
?>

Step 4 — Use the token for authenticated requests

Include the token in the Authorization header as a Bearer token:

Authorization: Bearer eyJ0eXAiO...

Example: GET with curl

curl -H Authorization: Bearer eyJ0eXAiO... 
     https://example.com/wp-json/wp/v2/posts

Example: fetch (browser)

fetch(https://example.com/wp-json/wp/v2/posts, {
  method: GET,
  headers: {
    Authorization: Bearer    token,
    Content-Type: application/json
  }
})
.then(r => r.json())
.then(posts => console.log(posts))

Step 5 — Configure CORS (Cross-Origin Resource Sharing)

Browsers block cross-origin requests unless the server allows them. You must add the appropriate CORS headers and properly handle preflight OPTIONS requests. Prefer restricting Access-Control-Allow-Origin to the exact client origin for security instead of using .

WordPress approach: add headers via rest_pre_serve_request

Put the following code in a simple plugin or your themes functions.php (a plugin is recommended so it survives theme changes). This approach sets CORS headers for REST API responses and handles OPTIONS preflight.


Server-level CORS for Nginx (optional / complementary)

# In the location block that serves /wp-json
location ~ /wp-json {
    if (request_method = OPTIONS) {
        add_header Access-Control-Allow-Origin https://app.example.com
        add_header Access-Control-Allow-Methods GET, POST, OPTIONS, PUT, DELETE
        add_header Access-Control-Allow-Headers Authorization, Content-Type, X-WP-Nonce
        add_header Access-Control-Allow-Credentials true
        return 204
    }
    add_header Access-Control-Allow-Origin https://app.example.com
    add_header Access-Control-Allow-Methods GET, POST, OPTIONS, PUT, DELETE
    add_header Access-Control-Allow-Headers Authorization, Content-Type, X-WP-Nonce
    add_header Access-Control-Allow-Credentials true
    # ensure Authorization header forwarded to PHP:
    fastcgi_param HTTP_AUTHORIZATION http_authorization
    # proxy_pass / fastcgi_pass etc.
}

Apache example (vhost or .htaccess)

# In your sites Apache config or .htaccess

    Header set Access-Control-Allow-Origin https://app.example.com
    Header set Access-Control-Allow-Methods GET,POST,OPTIONS,PUT,DELETE
    Header set Access-Control-Allow-Headers Authorization,Content-Type,X-WP-Nonce
    Header set Access-Control-Allow-Credentials true


# Ensure Authorization header makes it to PHP
RewriteEngine On
RewriteRule . - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]

Step 6 — Protecting custom REST endpoints with JWT

When JWT authentication is active, a valid token sets the current user for REST requests. Use register_rest_route and a permission_callback checking current_user or capabilities.

 GET,
        callback => function( request ) {
            current = wp_get_current_user()
            return rest_ensure_response( array(
                message => Hello  . current->display_name,
                user_id => current->ID
            ) )
        },
        // permission callback: allow only logged-in users
        permission_callback => function() {
            return is_user_logged_in()
            // OR more strict: return current_user_can(edit_posts)
        },
    ) )
} )
?>

Step 7 — Customizing token payload and expiration

Many JWT plugins expose filters to change the token payload or expiration. Typical filters you may find:

  • jwt_auth_token_before_dispatch — modify the payload returned to the client
  • jwt_auth_expire — change token expiration timestamp

Example: add extra claims and change expiry (adjust filter names if your plugin uses different names — check the plugin readme).

roles )
    data[app_meta] = array( plan => pro )
    return data
}, 10, 2 )

// Extend the token lifetime (example: 7 days)
add_filter( jwt_auth_expire, function() {
    return time()   ( 7  DAY_IN_SECONDS )
} )
?>

Step 8 — Decoding and verifying tokens server-side (optional)

If you need to decode a token manually (e.g., in custom code outside the plugin), use a JWT library. The plugin already uses firebase/php-jwt, but if you decode independently:


Step 9 — Refresh tokens and revocation strategies

Standard JWT is stateless: the server validates signature and expiry but doesnt store issued tokens. This means revoke token is not possible unless you implement a server-side blacklist or use refresh tokens with stored state. Options:

  • Short-lived access tokens and long-lived refresh tokens: store refresh tokens in DB (hashed) and provide an endpoint /refresh that exchanges a valid refresh token for a new access token. Validate refresh tokens against DB and revoke by removing DB entry.
  • Token blacklist: store revoked token IDs (jti claim) and check them on each request (requires hooking into token validation process or adding a permission callback).
  • Maintain a token issuance counter per user: include a token_version claim and store current version in usermeta. To revoke all tokens, increment token_version in DB and reject tokens that contain an older version.

Example: refresh token concept (outline)


Security best practices

  • Always use HTTPS for any request that contains tokens or credentials.
  • Do not store JWT access tokens in localStorage if you can avoid it localStorage is vulnerable to XSS. Prefer HttpOnly secure cookies for web apps when possible, or store tokens in memory with refresh token in an httpOnly cookie on the server side.
  • Use short expiry for access tokens (minutes to hours) and refresh tokens for longer-lived sessions if you implement them.
  • Restrict Access-Control-Allow-Origin to specific origins, not , when using credentials.
  • Use strong, random JWT secrets and rotate them carefully (rotation strategy must consider issued tokens validity or implement versioning in claims).
  • Validate scopes or capabilities in permission callbacks for sensitive endpoints.
  • Sanitize and escape any data returned by custom endpoints to avoid leaking sensitive information.

Troubleshooting — common pitfalls and solutions

  • Empty/missing Authorization header: Check .htaccess or Nginx fastcgi_param settings. Use curl to verify headers reach server. For Apache, ensure RewriteRule or SetEnvIf is applied.
  • Plugin returns invalid credentials even with the right password: Confirm payload keys match expected (username vs email), confirm password not being altered by middleware, and ensure user exists and has correct login method.
  • CORS preflight failing: Ensure server responds to OPTIONS with 200/204 and includes Access-Control-Allow-Headers including Authorization and Content-Type. Server-level CORS may be required (Nginx/Apache) in addition to WordPress header setting.
  • Token expired errors: Check server time synchronization (NTP) on both client and server. Adjust jwt_auth_expire filter if you need a different TTL.
  • Debugging: Use curl (server-to-server) to verify JWT endpoints and headers independent of browser CORS. Inspect network tab in browser to view preflight and actual request headers and responses.

Example end-to-end flow summary

  1. Client posts credentials to /wp-json/jwt-auth/v1/token.
  2. Server verifies credentials and returns a signed JWT and user info.
  3. Client stores token securely and uses Authorization: Bearer lttokengt for subsequent requests.
  4. Server checks signature and expiry on incoming requests and sets the current user so REST endpoints can use standard WP permission checks.
  5. CORS configuration on server/WordPress allows requests from the client origin and handles OPTIONS preflight.

Appendix — useful code snippets

Example: .htaccess to forward Authorization and add CORS (Apache)

# .htaccess (WordPress root)
RewriteEngine On
RewriteRule . - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]


    Header set Access-Control-Allow-Origin https://app.example.com
    Header set Access-Control-Allow-Methods GET,POST,OPTIONS,PUT,DELETE
    Header set Access-Control-Allow-Headers Authorization,Content-Type,X-WP-Nonce
    Header set Access-Control-Allow-Credentials true

Example: client-side login subsequent request (axios)

// login
axios.post(https://example.com/wp-json/jwt-auth/v1/token, {
  username: alice,
  password: hunter2
}).then(res => {
  const token = res.data.token
  // subsequent request with token
  return axios.get(https://example.com/wp-json/wp/v2/posts, {
    headers: { Authorization: Bearer    token }
  })
}).then(res => {
  console.log(res.data)
}).catch(err => {
  console.error(err.response ? err.response.data : err.message)
})

Final notes

This tutorial covers the full lifecycle of using JWT for WordPress REST API authentication, handling CORS for browser clients, protecting custom endpoints and implementing improved stateful features like refresh tokens when necessary. Adjust code snippet names and filter hooks if your chosen JWT plugin uses slightly different hook names or endpoints — always consult the plugins README for exact details. Prioritize HTTPS, minimal token lifetime, and securing refresh tokens (if used).



Acepto donaciones de BAT's mediante el navegador Brave 🙂



Leave a Reply

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