Skip to content

GraphQL Authentication — Securing Your API with JWT and Context

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about Graphql Authentication. We cover key concepts, practical examples, and best practices to help you master this topic.

GraphQL authentication is typically handled in the context layer, where tokens are verified once per request and the authenticated user is made available to all resolvers.

What You'll Learn

You will learn how to implement JWT authentication in Apollo Server, create auth context, build directive-based auth guards, handle token refresh, and integrate with third-party auth providers.

Why Authentication Matters

GraphQL APIs need authentication to protect user data and enforce access control. Without authentication, anyone can query any data. DodaTech's Durga Antivirus Pro uses JWT-based authentication — users log in via the web app, receive a signed JWT, and include it in every GraphQL request. The server verifies the token in the context function and attaches the user to every resolver.

sequenceDiagram
    participant Client
    participant AuthAPI as Auth Endpoint
    participant GraphQL
    participant Context
    
    Client->>AuthAPI: POST /login (email, password)
    AuthAPI-->>Client: JWT token
    Client->>GraphQL: query { devices } + Authorization header
    GraphQL->>Context: Verify token
    Context->>Context: Decode JWT, find user
    Context-->>GraphQL: { user: User, db, loaders }
    GraphQL->>Resolver: context.user available
    Resolver-->>Client: Filtered response
â„šī¸ Info

Prerequisites: GraphQL server setup. Understanding of JWT and HTTP headers.

JWT Authentication in Context

const jwt = require('jsonwebtoken');
const { ApolloServer, AuthenticationError } = require('apollo-server');

const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';

const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: async ({ req }) => {
    // Get token from Authorization header
    const authHeader = req.headers.authorization || '';
    const token = authHeader.startsWith('Bearer ') 
      ? authHeader.slice(7) 
      : null;
    
    let user = null;
    
    if (token) {
      try {
        // Verify and decode the JWT
        const decoded = jwt.verify(token, JWT_SECRET);
        // Fetch user from database (ensures user still exists)
        user = await db.users.findById(decoded.userId);
      } catch (err) {
        // Token invalid or expired — user stays null
        // Do NOT throw here — let individual resolvers decide
        console.warn('Invalid token:', err.message);
      }
    }
    
    return {
      user,
      db,
      loaders: createLoaders(),
    };
  },
});

Resolver-Level Auth Checks

const { AuthenticationError, ForbiddenError } = require('apollo-server');

const resolvers = {
  Query: {
    myDevices: (parent, args, context) => {
      if (!context.user) {
        throw new AuthenticationError('You must be logged in');
      }
      return context.db.devices.findByUserId(context.user.id);
    },
    
    allDevices: (parent, args, context) => {
      if (!context.user) {
        throw new AuthenticationError('You must be logged in');
      }
      if (context.user.role !== 'ADMIN') {
        throw new ForbiddenError('Only admins can list all devices');
      }
      return context.db.devices.findAll();
    },
  },
  
  Device: {
    // Owner-only fields
    internalNotes: (parent, args, context) => {
      if (!context.user || parent.userId !== context.user.id) {
        return null; // Hide sensitive data from unauthorized users
      }
      return parent.internalNotes;
    },
  },
};

Directive-Based Auth Guards

enum Role {
  ADMIN
  ANALYST
  VIEWER
}

directive @auth(requires: Role!) on FIELD_DEFINITION | OBJECT

type Threat {
  id: ID!
  name: String!
  severity: Severity!
  # Only admins can see internal notes
  internalNotes: String @auth(requires: ADMIN)
  # Analysts and admins can see analysis
  analysis: String @auth(requires: ANALYST)
}

type Query {
  threats: [Threat!]!
  sensitiveThreats: [Threat!]! @auth(requires: ANALYST)
}
const { mapSchema, getDirectives, MapperKind } = require('@graphql-tools/utils');

function authDirectiveTransformer(schema) {
  return mapSchema(schema, {
    [MapperKind.OBJECT_FIELD]: (fieldConfig) => {
      const authDirective = getDirectives(schema, fieldConfig)?.auth;
      if (!authDirective) return;
      
      const { requires } = authDirective;
      const originalResolver = fieldConfig.resolve || defaultFieldResolver;
      
      fieldConfig.resolve = (source, args, context, info) => {
        if (!context.user) {
          throw new AuthenticationError('Authentication required');
        }
        
        const roleHierarchy = { ADMIN: 3, ANALYST: 2, VIEWER: 1 };
        if ((roleHierarchy[context.user.role] || 0) < (roleHierarchy[requires] || 0)) {
          throw new ForbiddenError(`Requires ${requires} role`);
        }
        
        return originalResolver(source, args, context, info);
      };
    },
  });
}

const server = new ApolloServer({
  typeDefas,
  resolvers,
  schemaTransforms: [authDirectiveTransformer],
});

Token Refresh Pattern

// Refresh token mutation
const resolvers = {
  Mutation: {
    refreshToken: async (_, { refreshToken }, context) => {
      try {
        const decoded = jwt.verify(refreshToken, JWT_REFRESH_SECRET);
        const user = await db.users.findById(decoded.userId);
        
        if (!user) {
          throw new AuthenticationError('User not found');
        }
        
        // Issue new access token
        const newToken = jwt.sign(
          { userId: user.id, role: user.role },
          JWT_SECRET,
          { expiresIn: '15m' }
        );
        
        return {
          token: newToken,
          user,
        };
      } catch (err) {
        throw new AuthenticationError('Invalid refresh token');
      }
    },
  },
};

Integrating with Auth0

const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');

const client = jwksClient({ jwksUri: 'https://your-domain.auth0.com/.well-known/jwks.json' });

function getKey(header, callback) {
  client.getSigningKey(header.kid, (err, key) => {
    callback(null, key?.publicKey || key?.rsaPublicKey);
  });
}

const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: async ({ req }) => {
    const token = req.headers.authorization?.split(' ')[1];
    if (!token) return { user: null };
    
    return new Promise((resolve) => {
      jwt.verify(token, getKey, { 
        algorithms: ['RS256'],
        audience: 'https://api.dodatech.com',
        issuer: 'https://your-domain.auth0.com/',
      }, (err, decoded) => {
        if (err) return resolve({ user: null });
        resolve({ user: decoded });
      });
    });
  },
});

Common Mistakes

1. Throwing Auth Errors in Context

If context throws, the entire request fails — even for public queries. Let context return { user: null } and let individual resolvers enforce auth.

2. Not Validating Token on Every Request

Caching JWT verification results across requests introduces security holes. Verify the token on every request in the context function.

3. Storing Sensitive Data in JWT

JWTs are signed, not encrypted. Never store passwords, API keys, or sensitive PII in the token payload.

4. Using Short-Lived Tokens Without Refresh

15-minute access tokens without refresh tokens force users to log in every 15 minutes. Implement a refresh token flow.

5. Not Checking Revoked Tokens

If a user logs out, their JWT is still valid until expiry. Maintain a token blacklist (Redis) or check user status in the context function.

Practice Questions

  1. Where does authentication happen in GraphQL?
  2. How do you access the authenticated user in resolvers?
  3. What is the recommended pattern for public vs private fields?
  4. How do you implement role-based access control?
  5. Why should context not throw auth errors?

Answers:

  1. Authentication happens in the context function — verify the token from request headers once per request and attach the user to context.
  2. Access context.user in any resolver. It's set by the context function and available to all resolvers in that request.
  3. Let context attach user or null. Resolvers check context.user to decide visibility. Directive-based guards provide declarative control on fields.
  4. Define role levels (ADMIN, ANALYST, VIEWER) in an enum. Implement role checks in resolvers or via an @auth(requires: Role) directive transformer.
  5. If context throws auth errors, the entire request fails — even public queries. Let context attach null and let individual resolvers decide.

Challenge: Design a complete authentication system for DodaTech's GraphQL API. Include JWT-based login with access and refresh tokens, context-based auth with user loading, directive-based role guards (ADMIN, ANALYST, VIEWER), token refresh mutation, and integration with Auth0 for SSO.

FAQ

Should I put auth tokens in headers or the query?

Always use HTTP headers (Authorization: Bearer ). Never put tokens in query arguments — they appear in server logs, browser history, and cache systems.

How do I handle auth in subscriptions?

Pass the token in WebSocket connectionParams. Validate in the onConnect callback. Return user from onConnect — it becomes the context for all subscription events.

What is the difference between authentication and authorization?

Authentication verifies who you are (JWT verification). Authorization determines what you can do (role checks). Authentication happens in context; authorization happens in resolvers.

Can I use session-based auth with GraphQL?

Yes — but sessions violate REST's statelessness if stored server-side. For GraphQL, JWT-based auth is more common and scalable. If you must use sessions, store session IDs in signed cookies.

How do I test authenticated resolvers?

Write unit tests that pass mock context: resolver(parent, args, { user: mockUser, db: mockDb }). Integration tests set the Authorization header.

Mini Project

Build a complete authentication system for DodaTech's GraphQL API. Implement JWT authentication with access tokens (15min) and refresh tokens (7 days), a @auth directive for role-based access control, a login mutation that returns tokens, a refreshToken mutation, a logout mutation with token blacklisting, and integrate with Auth0 for enterprise SSO.

What's Next

Topic Description
Authorization Role-based access control patterns
Error Handling Error patterns in GraphQL
Security Depth limiting, cost analysis
âŦ… Error Handling
➡ Authorization

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro