Labs ICT
Pro Login

Authentication with GraphQL

Authentication with GraphQL

Authentication is about verifying who a user is. In GraphQL, you typically handle authentication in the context and check it in resolvers.

Setting Up Authentication Context

The most common pattern is to extract the authentication token from the request headers and add the user to the context:

const { ApolloServer } = require('@apollo/server');

async function getContext({ req }) {
  // Get the token from headers
  const token = req.headers.authorization?.replace('Bearer ', '');
  
  if (!token) {
    return { user: null };
  }
  
  try {
    // Verify the token and get the user
    const user = await verifyToken(token);
    return { user };
  } catch (error) {
    return { user: null };
  }
}

const server = new ApolloServer({
  typeDefs,
  resolvers,
});

// Pass context to Apollo
const { url } = await startStandaloneServer(server, {
  context: getContext,
});

The context function runs for every request and makes the user available to all resolvers.

Checking Authentication in Resolvers

Use the context user to check authentication in resolvers:

const resolvers = {
  Query: {
    me: (parent, args, context) => {
      if (!context.user) {
        throw new Error('Not authenticated');
      }
      return context.user;
    },
    
    // Public query - no auth required
    publicPosts: (parent, args, context) => {
      return context.db.posts.find({ published: true });
    },
  },
  
  Mutation: {
    createPost: (parent, args, context) => {
      if (!context.user) {
        throw new Error('Not authenticated');
      }
      
      return context.db.posts.create({
        ...args.input,
        authorId: context.user.id,
      });
    },
  },
};

Always check authentication at the resolver level, not in the schema.

Authorization

Authorization determines what an authenticated user can do. Check roles and permissions in resolvers:

class ForbiddenError extends Error {
  constructor(message = 'Not authorized') {
    super(message);
    this.extensions = { code: 'FORBIDDEN' };
  }
}

const resolvers = {
  Mutation: {
    deleteUser: (parent, args, context) => {
      if (!context.user) {
        throw new Error('Not authenticated');
      }
      
      if (context.user.role !== 'ADMIN') {
        throw new ForbiddenError('Only admins can delete users');
      }
      
      return context.db.users.delete(args.id);
    },
    
    updatePost: (parent, args, context) => {
      if (!context.user) {
        throw new Error('Not authenticated');
      }
      
      const post = context.db.posts.find(args.id);
      
      // Only the author can update their own posts
      if (post.authorId !== context.user.id) {
        throw new ForbiddenError('You can only update your own posts');
      }
      
      return context.db.posts.update(args.id, args.input);
    },
  },
};

Authorization logic belongs in resolvers, not middleware. This keeps your auth logic close to the data.

Using Directives for Auth

GraphQL directives let you add auth rules directly in your schema:

const typeDefs = `
  directive @auth(requires: Role = USER) on FIELD_DEFINITION
  
  enum Role {
    ADMIN
    USER
    GUEST
  }
  
  type User {
    id: ID!
    name: String!
    email: String!
    password: String! @auth(requires: ADMIN)
  }
  
  type Query {
    me: User! @auth
    users: [User!]! @auth(requires: ADMIN)
  }
  
  type Mutation {
    createPost(input: CreatePostInput!): Post! @auth
    deleteUser(id: ID!): Boolean! @auth(requires: ADMIN)
  }
`;

Directives make your schema self-documenting about which fields require authentication.