A comprehensive WordPress plugin that provides both JWT with refresh tokens (HttpOnly cookies) and OAuth2 authentication for the WordPress REST API.
- JWT Authentication with refresh tokens stored as HttpOnly cookies
- OAuth2 Authorization Code flow (simplified for demo)
- Secure token storage and management
- CORS support for frontend applications
- Token revocation and cleanup
- User-friendly admin interface
- Download or clone this plugin to your
wp-content/plugins/directory - Add the following constants to your
wp-config.phpfile:
define('WP_JWT_AUTH_SECRET', 'your-very-long-and-random-secret-key-here');
define('WP_JWT_ACCESS_TTL', 900); // 15 minutes (optional)
define('WP_JWT_REFRESH_TTL', 1209600); // 14 days (optional)- Activate the plugin through the WordPress admin interface
- Clone this repository:
git clone https://github.com/YOUR_USERNAME/wp-rest-auth-multi.git
cd wp-rest-auth-multi- Install dependencies:
# Install PHP dependencies
composer install
# Install Node.js dependencies for wp-env
npm install- Set up the development environment:
# Start WordPress environment using wp-env
npm run env:start
# The plugin will be automatically installed and activated- Run tests:
# Run all tests
npm run test
# Run only unit tests
npm run test:unit
# Run only integration tests
npm run test:integration- Access your development site:
- WordPress site: http://localhost:8888
- WordPress admin: http://localhost:8888/wp-admin (admin/password)
- Stop the development environment:
npm run env:stopPOST /wp-json/jwt/v1/token
Content-Type: application/json
{
"username": "your-username",
"password": "your-password"
}Response:
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
"token_type": "Bearer",
"expires_in": 900,
"user": {
"id": 1,
"username": "admin",
"email": "admin@example.com",
"roles": ["administrator"]
}
}The refresh token is automatically set as an HttpOnly cookie.
POST /wp-json/jwt/v1/refreshResponse:
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
"token_type": "Bearer",
"expires_in": 900
}POST /wp-json/jwt/v1/logoutGET /wp-json/jwt/v1/verify
Authorization: Bearer <access_token>Once you have an access token, include it in the Authorization header:
curl -X POST "https://yoursite.com/wp-json/wp/v2/posts" \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{"title": "My New Post", "content": "Post content here"}'The plugin creates a demo OAuth2 client automatically:
- Client ID:
demo-client - Client Secret:
demo-secret - Redirect URIs:
http://localhost:3000/callback,http://localhost:5173/callback
GET /wp-json/oauth2/v1/authorize?response_type=code&client_id=demo-client&redirect_uri=http://localhost:3000/callback&state=xyz123
POST /wp-json/oauth2/v1/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=<authorization_code>&redirect_uri=http://localhost:3000/callback&client_id=demo-client&client_secret=demo-secretGET /wp-json/oauth2/v1/userinfo
Authorization: Bearer <access_token>// Get JWT access token
async function login(username, password) {
const response = await fetch('/wp-json/jwt/v1/token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include', // Important for refresh token cookie
body: JSON.stringify({ username, password })
});
if (response.ok) {
const data = await response.json();
localStorage.setItem('access_token', data.access_token);
return data;
}
throw new Error('Login failed');
}
// Refresh access token
async function refreshToken() {
const response = await fetch('/wp-json/jwt/v1/refresh', {
method: 'POST',
credentials: 'include' // Include refresh token cookie
});
if (response.ok) {
const data = await response.json();
localStorage.setItem('access_token', data.access_token);
return data.access_token;
}
throw new Error('Token refresh failed');
}
// Make authenticated API calls
async function apiCall(url, options = {}) {
let token = localStorage.getItem('access_token');
const response = await fetch(url, {
...options,
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
...options.headers
}
});
// If token expired, try to refresh
if (response.status === 401) {
try {
token = await refreshToken();
return fetch(url, {
...options,
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
...options.headers
}
});
} catch (error) {
// Redirect to login
window.location.href = '/login';
}
}
return response;
}import { useState, useEffect } from 'react';
export function useAuth() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const login = async (username, password) => {
const response = await fetch('/wp-json/jwt/v1/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ username, password })
});
if (response.ok) {
const data = await response.json();
localStorage.setItem('access_token', data.access_token);
setUser(data.user);
return data;
}
throw new Error('Login failed');
};
const logout = async () => {
await fetch('/wp-json/jwt/v1/logout', {
method: 'POST',
credentials: 'include'
});
localStorage.removeItem('access_token');
setUser(null);
};
const refreshToken = async () => {
try {
const response = await fetch('/wp-json/jwt/v1/refresh', {
method: 'POST',
credentials: 'include'
});
if (response.ok) {
const data = await response.json();
localStorage.setItem('access_token', data.access_token);
return data.access_token;
}
} catch (error) {
console.error('Token refresh failed:', error);
logout();
}
return null;
};
useEffect(() => {
const token = localStorage.getItem('access_token');
if (token) {
// Verify token on app load
fetch('/wp-json/jwt/v1/verify', {
headers: { 'Authorization': `Bearer ${token}` }
})
.then(response => response.json())
.then(data => {
if (data.authenticated) {
setUser(data.user);
}
})
.finally(() => setLoading(false));
} else {
setLoading(false);
}
}, []);
return { user, login, logout, refreshToken, loading };
}- HTTPS Required: Always use HTTPS in production
- Secure Cookies: Refresh tokens use HttpOnly, Secure, and SameSite=Strict cookies
- Token Rotation: Refresh tokens are rotated on each use (configurable)
- Short-lived Access Tokens: Default 15-minute expiration
- CORS Configuration: Configure allowed origins properly
add_filter('wp_auth_multi_cors_origins', function($origins) {
return array_merge($origins, [
'https://yourfrontend.com',
'https://anotherdomain.com'
]);
});add_filter('wp_auth_multi_rotate_refresh_token', '__return_false');The plugin creates a wp_jwt_refresh_tokens table to store refresh tokens securely with the following columns:
id- Primary keyuser_id- WordPress user IDtoken_hash- Hashed refresh tokenexpires_at- Expiration timestamprevoked_at- Revocation timestamp (if revoked)issued_at- Issue timestampuser_agent- User agent stringip_address- IP address
# 1. Get token
curl -X POST "http://yoursite.local/wp-json/jwt/v1/token" \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"your-password"}' \
-c cookies.txt
# 2. Use token to create a post
curl -X POST "http://yoursite.local/wp-json/wp/v2/posts" \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{"title":"Test Post","status":"draft"}'
# 3. Refresh token
curl -X POST "http://yoursite.local/wp-json/jwt/v1/refresh" \
-b cookies.txt
# 4. Logout
curl -X POST "http://yoursite.local/wp-json/jwt/v1/logout" \
-b cookies.txt# 1. Get authorization code (requires login in browser first)
# Visit: http://yoursite.local/wp-json/oauth2/v1/authorize?response_type=code&client_id=demo-client&redirect_uri=http://localhost:3000/callback&state=test
# 2. Exchange code for token
curl -X POST "http://yoursite.local/wp-json/oauth2/v1/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code&code=<code>&redirect_uri=http://localhost:3000/callback&client_id=demo-client&client_secret=demo-secret"
# 3. Get user info
curl -X GET "http://yoursite.local/wp-json/oauth2/v1/userinfo" \
-H "Authorization: Bearer <access_token>"For issues and feature requests, please create an issue in the project repository.
GPL v2 or later