SyntaxStudy
Sign Up
GraphQL Refresh Tokens and Secure Token Storage
GraphQL Beginner 1 min read

Refresh Tokens and Secure Token Storage

Short-lived access tokens (15 minutes) limit the window of exposure if a token is leaked. Refresh tokens are long-lived (7-30 days), stored in httpOnly cookies (inaccessible to JavaScript), and used only to obtain new access tokens. This two-token pattern balances security and UX. The refresh mutation verifies the refresh token from the cookie, issues a new access token, and optionally rotates the refresh token (refresh token rotation). Token rotation means each refresh token can only be used once, invalidating stolen tokens after the legitimate user refreshes.
Example
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import cookieParser from 'cookie-parser';
import express from 'express';

const REFRESH_TOKEN_EXPIRY = '30d';
const ACCESS_TOKEN_EXPIRY  = '15m';

const resolvers = {
  Mutation: {
    login: async (_, { email, password }, { res, db }) => {
      const user = await authenticateUser(email, password, db);
      const accessToken  = jwt.sign({ userId: user.id }, JWT_SECRET, { expiresIn: ACCESS_TOKEN_EXPIRY });
      const refreshToken = jwt.sign({ userId: user.id }, REFRESH_SECRET, { expiresIn: REFRESH_TOKEN_EXPIRY });

      // Store refresh token in httpOnly cookie
      res.cookie('refresh_token', refreshToken, {
        httpOnly: true,
        secure: process.env.NODE_ENV === 'production',
        sameSite: 'strict',
        maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days in ms
      });

      return { accessToken, user };
    },

    refreshToken: async (_, __, { req, res }) => {
      const token = req.cookies.refresh_token;
      if (!token) throw new Error('No refresh token');
      const { userId } = jwt.verify(token, REFRESH_SECRET);
      const user = await db.users.findById(userId);
      const newAccessToken = jwt.sign({ userId }, JWT_SECRET, { expiresIn: ACCESS_TOKEN_EXPIRY });
      return { accessToken: newAccessToken, user };
    },

    logout: (_, __, { res }) => {
      res.clearCookie('refresh_token');
      return true;
    },
  },
};