How to Authenticate WordPress GraphQL Queries with Simple JWT Login and WPGraphQL
GraphQL and headless WordPress are a natural match. WPGraphQL gives you a flexible, typed query layer over all your WordPress data. But like the REST API, it's unauthenticated by default — anyone can query it. Simple JWT Login solves that.
In this guide I'll walk through connecting Simple JWT Login to WPGraphQL so your GraphQL operations can carry a JWT, granting access to protected data and enabling authenticated mutations like creating posts or updating user profiles.
Prerequisites
You'll need two plugins installed and active:
- WPGraphQL — available at wpgraphql.com
- Simple JWT Login — available in the WordPress plugin repository
No additional bridge plugin is required. Simple JWT Login's WPGraphQL integration is built in.
How It Works
The integration is straightforward: Simple JWT Login generates and validates JWTs using its standard /auth endpoint. When a request hits the WPGraphQL endpoint (/wp-json/graphql or /graphql), Simple JWT Login intercepts it, validates the bearer token, and — if valid — sets the current WordPress user context before WPGraphQL resolves the query.
From WPGraphQL's perspective, the request simply arrives as an authenticated WordPress user. Every resolver that checks current_user_can() or relies on wp_get_current_user() works exactly as it would in a browser session.
Step 1 — Configure Simple JWT Login
Navigate to Simple JWT Login in your WordPress admin. The core settings you need:
General tab:
- Set a strong JWT Decryption Key
- Choose HS256 as your algorithm (or RS256 for asymmetric setups)
- Set a sensible JWT expiration (e.g. 3600 seconds / 1 hour)
WPGraphQL tab:
Enable the WPGraphQL integration:
Settings > Simple JWT Login > WPGraphQL > Enable WPGraphQL authentication
That's the only required toggle. The plugin will now validate JWT tokens on every request to your GraphQL endpoint.
Step 2 — Obtain a JWT
Use the standard auth endpoint to get a token:
curl -X POST "https://example.com/wp-json/simple-jwt-login/v1/auth" \
-H "Content-Type: application/json" \
-d '{
"email": "editor@example.com",
"password": "their_password"
}'
Response:
{
"success": true,
"data": {
"jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
}
Step 3 — Make Authenticated GraphQL Queries
Pass the token as a bearer token in the Authorization header on every GraphQL request:
curl -X POST "https://example.com/graphql" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
-d '{
"query": "{ viewer { id name email roles { nodes { name } } } }"
}'
Without the token, viewer returns null. With it, you get the full authenticated user object:
{
"data": {
"viewer": {
"id": "dXNlcjoyNQ==",
"name": "Jane Editor",
"email": "editor@example.com",
"roles": {
"nodes": [{ "name": "editor" }]
}
}
}
}
Step 4 — Authenticated Mutations
This is where the integration becomes genuinely useful. Many WPGraphQL mutations require an authenticated user. Creating a post, for example:
curl -X POST "https://example.com/graphql" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
-d '{
"query": "mutation CreatePost($input: CreatePostInput!) { createPost(input: $input) { post { id title status } } }",
"variables": {
"input": {
"title": "My First API Post",
"content": "Written via GraphQL with JWT auth.",
"status": "PUBLISH",
"clientMutationId": "create-post-1"
}
}
}'
Without authentication, this mutation returns a permission error. With a valid JWT for a user who has the editor or administrator role, it succeeds and returns the created post.
Using the Integration in a JavaScript Client
In a Next.js or React app, you'll typically store the JWT after login and attach it to every GraphQL request. Here's how that looks with a simple fetch-based client:
const GQL_ENDPOINT = 'https://example.com/graphql';
async function graphqlRequest(query, variables = {}, token = null) {
const headers = { 'Content-Type': 'application/json' };
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const response = await fetch(GQL_ENDPOINT, {
method: 'POST',
headers,
body: JSON.stringify({ query, variables }),
});
return response.json();
}
// Login and get a token
async function login(email, password) {
const response = await fetch(
'https://example.com/wp-json/simple-jwt-login/v1/auth',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
}
);
const data = await response.json();
return data.data.jwt;
}
// Example usage
const token = await login('editor@example.com', 'password');
const { data } = await graphqlRequest(
`{ viewer { name email } }`,
{},
token
);
console.log(data.viewer); // { name: 'Jane Editor', email: 'editor@example.com' }
If you're using Apollo Client, set the token in your auth link:
import { ApolloClient, InMemoryCache, createHttpLink } from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
const httpLink = createHttpLink({ uri: 'https://example.com/graphql' });
const authLink = setContext((_, { headers }) => {
const token = localStorage.getItem('jwt'); // or however you store it
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : '',
},
};
});
const client = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache(),
});
Enriching Queries with Custom JWT Claims
Sometimes you want to avoid an extra viewer query on page load by embedding user metadata directly in the token. Use the simple_jwt_login_jwt_payload filter to add whatever your front-end needs:
add_filter('simple_jwt_login_jwt_payload', function($payload, $user) {
$payload['wp_roles'] = $user->roles;
$payload['display_name'] = $user->display_name;
$payload['avatar_url'] = get_avatar_url($user->ID, ['size' => 48]);
return $payload;
}, 10, 2);
Your Apollo or fetch client can decode the JWT (it's just base64) and read these values without any extra network request:
function decodeJwtPayload(token) {
const base64 = token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/');
return JSON.parse(atob(base64));
}
const payload = decodeJwtPayload(token);
console.log(payload.display_name); // "Jane Editor"
console.log(payload.wp_roles); // ["editor"]
Protecting the GraphQL Endpoint Itself
By default, WPGraphQL allows unauthenticated introspection queries, which exposes your full schema to anyone. You can lock this down at two levels:
WPGraphQL side: Disable public introspection in WPGraphQL settings.
Simple JWT Login side: Add the GraphQL endpoint to your protected routes:
Settings > Simple JWT Login > Protect Endpoints
Add route: /graphql
Methods: GET, POST
This ensures every GraphQL request — including introspection — must carry a valid JWT. Ideal for private internal APIs.
Token Refresh for Long-Running Sessions
GraphQL apps often run as SPAs where the user stays on the page for extended periods. Implement proactive token refresh to avoid mid-session 401s:
function isTokenExpiringSoon(token, bufferSeconds = 300) {
const { exp } = decodeJwtPayload(token);
return (exp - bufferSeconds) < (Date.now() / 1000);
}
async function refreshToken(currentToken) {
const response = await fetch(
'https://example.com/wp-json/simple-jwt-login/v1/auth/refresh',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ JWT: currentToken }),
}
);
const data = await response.json();
return data.data.jwt;
}
// Before each GraphQL request:
if (isTokenExpiringSoon(storedToken)) {
storedToken = await refreshToken(storedToken);
// persist updated token
}
Conclusion
Simple JWT Login and WPGraphQL complement each other cleanly. WPGraphQL handles the query layer; Simple JWT Login handles identity. The integration requires no extra plugins, just a single settings toggle, and the result is a fully authenticated GraphQL API that honours WordPress's existing user roles and capabilities.
From there, the hooks system gives you room to customize the JWT payload, and the endpoint protection feature lets you lock down the GraphQL route to authenticated traffic only. It's a solid foundation for any headless WordPress application built on GraphQL.
