Securing Your GraphQL API with JWT Authentication

Sharukhan PatanSharukhan Patan
5 min read

When I first started building GraphQL APIs, security was an afterthought. I was more focused on crafting schemas, wiring up resolvers, and making sure queries returned the right data. But once I deployed my first production-ready GraphQL server, it hit me — this thing is wide open. Anyone who could hit the endpoint could send any query they wanted.

That’s when I started diving into authentication — specifically, using JWT (JSON Web Tokens) to secure GraphQL APIs. It completely changed the way I approach backend development. In this article, I’ll walk you through how to secure your own GraphQL API with JWT-based authentication in Node.js. We’ll cover how to issue tokens, validate them, protect resolvers, and implement role-based access.

Why JWT for GraphQL?

JWT is a compact, URL-safe way of representing claims between two parties. What makes it so popular in the GraphQL world is its statelessness. Once a token is issued, it doesn’t require server-side storage. The token carries the payload — like user ID or role — and the server just needs to verify it.

In REST APIs, you typically protect each route. In GraphQL, however, you have a single endpoint that supports multiple operations. That’s both a superpower and a vulnerability. Anyone who gets access can probe your schema and potentially exploit it. JWT allows us to enforce identity checks at the resolver level, making sure only authenticated and authorized users can access sensitive operations.

Project Setup

To make things simple, I set up a basic GraphQL server using Express and Apollo Server. Here’s the skeleton setup I worked with:

  • Node.js + Express

  • Apollo Server

  • jsonwebtoken for issuing and verifying tokens

  • bcrypt for hashing passwords

  • A simple in-memory or mock database

This isn’t enterprise-level production code, but it’s a solid foundation to understand the concepts.

Step 1: User Sign Up and Token Generation

The first step was to create a sign-up mutation that accepts a username and password, hashes the password, and stores the user. After that, I generated a JWT and returned it.

Here’s the flow:

  1. Hash the password using bcrypt

  2. Store the user

  3. Sign a JWT with the user’s ID and role

  4. Return the token

When I implemented this, I made sure the JWT included minimal data: userId and role. Anything more would increase payload size and exposure risk.

Step 2: Login Mutation

Next came the login mutation. It validated credentials and returned a fresh JWT. This mirrors the sign-up flow but includes credential checks. I can’t stress enough how important it is to validate passwords using bcrypt.compare() instead of manually matching strings.

At this point, a logged-in user had a token. The next challenge was using that token in subsequent requests.

Step 3: Middleware to Verify JWT

This part was pivotal. Every request to the GraphQL endpoint should be checked for a valid JWT. In Express, I added middleware that looked for the Authorization header, decoded the token using jsonwebtoken.verify, and attached the user payload to the request context.

app.use((req, res, next) => {
  const authHeader = req.headers.authorization || '';
  const token = authHeader.replace('Bearer ', '');

  if (token) {
    try {
      const decoded = jwt.verify(token, JWT_SECRET);
      req.user = decoded;
    } catch (err) {
      console.warn('Invalid token');
    }
  }

  next();
});

This allowed my resolvers to access context.req.user and make informed access decisions.

Step 4: Protecting Resolvers

Now for the fun part — actually using the token to secure GraphQL operations.

In each resolver, I checked whether the user was present in the context. If not, I threw an authentication error.

const resolvers = {
  Query: {
    me: (parent, args, context) => {
      if (!context.req.user) {
        throw new AuthenticationError('You must be logged in');
      }

      // fetch user data using context.req.user.userId
    },
  },
};

This pattern worked great for basic access control. But what about roles?

Step 5: Role-Based Access Control

In my app, I had two roles: USER and ADMIN. When signing the JWT, I added a role field to the payload. Inside sensitive resolvers — like deleting users or accessing admin-only data — I checked if the role matched.

const resolvers = {
  Mutation: {
    deleteUser: (parent, { id }, context) => {
      const user = context.req.user;

      if (!user || user.role !== 'ADMIN') {
        throw new ForbiddenError('Admins only');
      }

      // Proceed with deletion
    },
  },
};

This turned out to be extremely scalable. Down the road, I added a MODERATOR role with limited privileges, and the same structure held up.

Handling Token Expiry and Refresh

One lesson I learned the hard way: don’t make your tokens live forever. Expired tokens are inevitable, so you need a refresh strategy.

In production-grade apps, I implement a refresh token system. The access token (JWT) is short-lived (15 mins), while the refresh token (stored securely) allows users to obtain a new access token without logging in again.

For this article’s scope, I kept it simple — 1-hour expiry on the JWT and manual re-login when it expires.

Testing It All

Once everything was wired up, I used Postman and GraphQL Playground to test:

  • Register a user

  • Log in and copy the JWT

  • Paste it in the Authorization header: Bearer <token>

  • Run protected queries like me or getAdminStats

Everything clicked. Queries that were unauthenticated returned 401s. Admin-only mutations were properly gated. And the token payload was small, secure, and efficient.

Real-World Lessons

  • Never store passwords in plain text — always hash them with bcrypt.

  • Use environment variables to store your JWT secret.

  • Use HTTPS in production. Never send tokens over insecure channels.

  • Keep JWTs short-lived. Treat them as disposable.

  • Don’t overload your token payload. Include only what’s needed.

  • Consider a token revocation strategy or expiration short enough to mitigate abuse.

Wrapping Up

Implementing JWT authentication in a GraphQL API felt like unlocking a new level of control and confidence in my apps. Suddenly, I had the power to secure routes, protect data, and build admin interfaces without a huge security surface.

JWTs work especially well with GraphQL because they’re lightweight, self-contained, and easy to verify on each request. Paired with middleware and a well-structured role system, you can secure your API without sacrificing developer experience or performance.

If you’re just starting out with GraphQL security, don’t wait. Add JWT today. You’ll sleep better knowing your resolvers aren’t exposed to the wild west of the internet.

0
Subscribe to my newsletter

Read articles from Sharukhan Patan directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Sharukhan Patan
Sharukhan Patan