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
- Install and activate the JWT plugin from the Plugins screen or manually upload it to wp-content/plugins.
- 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.
- 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 .htaccessHeader 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
- Client posts credentials to /wp-json/jwt-auth/v1/token.
- Server verifies credentials and returns a signed JWT and user info.
- Client stores token securely and uses Authorization: Bearer lttokengt for subsequent requests.
- Server checks signature and expiry on incoming requests and sets the current user so REST endpoints can use standard WP permission checks.
- 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 🙂 |