GraphQL Authentication and Authorization

Authentication asks "Who are you?" Authorization asks "What are you allowed to do?" Both are your responsibility as the API developer — GraphQL itself has no built-in security layer. You implement both through context and resolver logic.

Authentication vs Authorization — The Difference

  Authentication                       Authorization
  ──────────────                       ─────────────
  Verifies identity                    Checks permissions
  "Prove you are Priya"                "Priya can edit her posts only"
  Happens first                        Happens after authentication
  Fails → 401 Unauthorized             Fails → 403 Forbidden
  Uses JWT, session cookie, API key    Uses roles, permissions, ownership

Step 1 – Authenticate in Context

Context runs once per request before any resolver executes. This is the right place to verify the user's token and attach the user object to context. Every resolver then uses context.user to know who made the request.

  import jwt from 'jsonwebtoken';

  await startStandaloneServer(server, {
    context: async ({ req }) => {
      const token = req.headers.authorization?.replace('Bearer ', '');

      let user = null;
      if (token) {
        try {
          user = jwt.verify(token, process.env.JWT_SECRET);
        } catch {
          // Invalid token — user stays null (guest)
        }
      }

      return { db: dbPool, user };
    }
  });

Step 2 – Authorize in Resolvers

Each resolver checks whether the authenticated user has permission to perform the requested operation. Throw a descriptive error when access is denied.

  import { GraphQLError } from 'graphql';

  const resolvers = {
    Query: {
      // Anyone can view products
      products: () => db.products.findAll(),

      // Must be logged in to see own orders
      myOrders: (_, __, ctx) => {
        if (!ctx.user) {
          throw new GraphQLError('You must be logged in', {
            extensions: { code: 'UNAUTHENTICATED' }
          });
        }
        return db.orders.findByUser(ctx.user.id);
      },

      // Must be admin to see all orders
      allOrders: (_, __, ctx) => {
        if (!ctx.user) throw new GraphQLError('Not authenticated',
          { extensions: { code: 'UNAUTHENTICATED' } });
        if (ctx.user.role !== 'ADMIN') throw new GraphQLError('Forbidden',
          { extensions: { code: 'FORBIDDEN' } });
        return db.orders.findAll();
      }
    },

    Mutation: {
      // Can only edit your own post
      editPost: async (_, args, ctx) => {
        if (!ctx.user) throw new GraphQLError('Not authenticated',
          { extensions: { code: 'UNAUTHENTICATED' } });

        const post = await db.posts.findById(args.id);
        if (post.authorId !== ctx.user.id) {
          throw new GraphQLError('You do not own this post',
            { extensions: { code: 'FORBIDDEN' } });
        }
        return db.posts.update(args.id, args.input);
      }
    }
  };

Role-Based Access with a Helper

  function requireRole(user, role) {
    if (!user) throw new GraphQLError('Not authenticated',
      { extensions: { code: 'UNAUTHENTICATED' } });
    if (user.role !== role) throw new GraphQLError('Forbidden',
      { extensions: { code: 'FORBIDDEN' } });
  }

  // Usage in resolvers:
  adminReport: (_, __, ctx) => {
    requireRole(ctx.user, 'ADMIN');
    return db.reports.getAdmin();
  }

Do Not Rely on Schema Shape for Security

Never assume a field is "hidden" because it is hard to guess. GraphQL introspection reveals all field names. Protect sensitive fields with resolver-level checks, not by hoping clients do not discover them.

  ✗ Wrong approach — "no one will guess this field name":
  ─────────────────────────────────────────────────────
  type User {
    secretAdminToken: String   ← Anyone who introspects finds this
  }

  ✓ Correct approach — resolver-level guard:
  ──────────────────────────────────────────
  User: {
    secretAdminToken: (user, _, ctx) => {
      if (ctx.user?.role !== 'ADMIN') return null;
      return user.secretAdminToken;
    }
  }

Key Points

  • Verify the user's identity once in context — all resolvers share the resulting context.user object.
  • Enforce authorization inside each resolver that needs it, not at the schema level alone.
  • Use GraphQLError with an extensions.code of UNAUTHENTICATED or FORBIDDEN to signal the error type.
  • Never rely on field obscurity for security — introspection makes every field name visible.

Leave a Comment