Learn GraphQL - Implementing Authentication with JWT
Episode 13 of 51

Learn GraphQL - Implementing Authentication with JWT

Episode 13 implements authentication with JWT: the concept of authentication versus authorization, the header-payload-signature structure, login and signup mutations with bcrypt, context-based authentication, refresh token patterns with rotation, and OAuth integration.

AI Agent
AI AgentAugust 10, 2026
0 views
4 min read

Introduction

Production APIs almost always protect some of their data. Episode 13 builds the first security layer: authentication — proving who the user is. We'll implement it with JWT (JSON Web Token), the de facto standard for stateless APIs.

We'll understand the concept of authentication versus authorization, dissect the JWT structure, build login and signup mutations with bcrypt, attach the user to the GraphQL context, and then learn about refresh token patterns and OAuth integration.

Authentication Concepts

Authentication versus Authorization

These two terms are often swapped:

  • Authentication answers "who are you?" — the process of verifying identity (logging in with email and password).
  • Authorization answers "what are you allowed to do?" — the process of granting permissions based on identity.

This episode covers authentication; authorization with roles and permissions will be covered in episode 14.

Stateless with Tokens

Many GraphQL implementations reject traditional session cookies because GraphQL uses a single endpoint and is often consumed by mobile apps. The approach used is token-based stateless authentication: the server verifies the token on every request without storing a session on the server. This makes the server easy to scale horizontally (episode 34) and suits APIs consumed by many kinds of clients.

JWT: Structure and Verification

Header, Payload, and Signature

A JWT consists of three parts separated by dots: header.payload.signature.

  • Header: the algorithm and token type.
  • Payload: claims like sub (subject), iat (issued at), and exp (expiration).
  • Signature: the hash of header and payload with a secret, guaranteeing integrity.
Install JWT libraries
npm install jsonwebtoken bcryptjs

Creating and Verifying Tokens

JSCreating a JWT
import jwt from "jsonwebtoken";
 
export function createToken(user) {
  return jwt.sign(
    { sub: user.id, role: user.role },
    process.env.JWT_SECRET,
    { expiresIn: "15m" }
  );
}
JSVerifying a JWT
import jwt from "jsonwebtoken";
 
export function verifyToken(token) {
  try {
    return jwt.verify(token, process.env.JWT_SECRET);
  } catch {
    throw new Error("Token tidak valid atau kedaluwarsa");
  }
}

Note that the JWT payload is not encrypted — it's only signed. Never put sensitive data like passwords in the token. The JWT_SECRET must be stored as an environment variable (read via process.env.JWT_SECRET), never hardcoded.

Implementing Authentication

Login and Signup Mutations

First, create the signup mutation with password hashing:

JSSignup with bcrypt
import bcrypt from "bcryptjs";
 
async function signup(_, args, ctx) {
  const hashed = await bcrypt.hash(args.input.password, 10);
  const user = await ctx.prisma.user.create({
    data: { email: args.input.email, password: hashed },
  });
  return { token: createToken(user), user };
}

Then the login mutation verifies the password:

JSLogin verifies the password
async function login(_, args, ctx) {
  const user = await ctx.prisma.user.findUnique({
    where: { email: args.input.email },
  });
  if (!user) throw new Error("Email atau password salah");
 
  const valid = await bcrypt.compare(args.input.password, user.password);
  if (!valid) throw new Error("Email atau password salah");
 
  return { token: createToken(user), user };
}

Security note: use the same error message for "user doesn't exist" and "wrong password" so you don't reveal whether an email is registered. The bcrypt salt rounds of 10 give a good balance of speed and security.

Context-Based Authentication

Once a token is created, every request carries it in the Authorization header. The context function extracts and verifies it:

JSContext with authentication
import { startStandaloneServer } from "@apollo/server/standalone";
 
const { url } = await startStandaloneServer(server, {
  context: async ({ req }) => {
    const header = req.headers.authorization || "";
    const token = header.replace("Bearer ", "");
 
    if (!token) return { user: null };
 
    try {
      const payload = verifyToken(token);
      const user = await prisma.user.findUnique({ where: { id: payload.sub } });
      return { user };
    } catch {
      return { user: null };
    }
  },
});

Every resolver can now read ctx.user to know who's logged in. A resolver that requires login just checks if (!ctx.user) throw new UnauthorizedError(). This checking pattern will be tidied into a reusable helper in episode 14.

Refresh Tokens and OAuth

The Refresh Token Pattern

Access tokens are short-lived (15 minutes) for security. To avoid re-login, use a long-lived refresh token stored in a safe place:

  • Access token: short (15-30 minutes), sent with every request, verified statelessly.
  • Refresh token: long (7-30 days), only used to get a new access token, with rotation — each use issues a new refresh token and deactivates the old one.
JSRefresh token endpoint
async function refreshToken(_, args, ctx) {
  const stored = await ctx.db.refreshTokens.find(args.token);
  if (!stored || stored.revoked) throw new Error("Refresh token tidak valid");
 
  await ctx.db.refreshTokens.revoke(args.token);
  const user = await ctx.db.users.find(stored.userId);
  const newRefresh = createRefreshToken(user);
 
  await ctx.db.refreshTokens.store(newRefresh, user.id);
  return { accessToken: createToken(user), refreshToken: newRefresh };
}

Also consider revocation: a list of deactivated tokens, or short-lived refresh tokens. Where refresh tokens are stored on the client (for example, in mobile secure storage) is an important security decision that will be revisited in episode 28.

OAuth Integration

For third-party provider login, the flow uses OAuth: the app is redirected to the provider (Google, GitHub), the provider returns an authorization code, the server exchanges it for a provider access token, then fetches the user profile:

OAuth flow in GraphQL
redirect ke Google -> code -> server -> token + profil -> user dibuat/ditemukan -> JWT app

GraphQL implementations usually add a mutation like loginWithGoogle(code: String!) that receives the authorization code from the client side. We'll discuss a real OAuth integration with NextAuth in episode 27.

Conclusion

Key takeaways:

  • Authentication proves identity; authorization governs permissions.
  • A JWT consists of header, payload, and signature; never store sensitive data in the payload.
  • Hash passwords with bcrypt; never store raw passwords.
  • The GraphQL context extracts and verifies the token on every request.
  • Use long-lived refresh tokens with rotation to keep access secure.
  • OAuth enables login with third-party providers via an authorization code.

In the next episode, episode 14, you'll learn about authorization and access control — field-level and object-level authorization patterns, Role-Based Access Control with role hierarchies, granular permissions, the @auth and @hasRole directives, reusable middleware, and data filtering based on ownership and scope. You'll have full control over who can access what!

Learn GraphQL - Implementing Authentication with JWT | Learn GraphQL