GraphQL APIs are often implemented with a single endpoint, which can make traditional request-based authentication tricky.

Let’s say we have a GraphQL API serving user data, and we want to ensure only authenticated users can access their own profiles.

# schema.graphql
type User {
  id: ID!
  username: String!
  email: String
}

type Query {
  me: User
}

type Mutation {
  updateUser(email: String!): User
}

Here’s a basic implementation in Node.js using Apollo Server:

// server.js
const { ApolloServer, gql } = require('apollo-server');

const typeDefs = gql`
  type User {
    id: ID!
    username: String!
    email: String
  }

  type Query {
    me: User
  }

  type Mutation {
    updateUser(email: String!): User
  }
`;

// Dummy user data and authentication logic
const users = {
  'user-1': { id: 'user-1', username: 'alice', email: 'alice@example.com' },
};

const resolvers = {
  Query: {
    me: (parent, args, context) => {
      // context will contain user info if authenticated
      if (!context.user) {
        throw new Error('Not authenticated');
      }
      return users[context.user.id];
    },
  },
  Mutation: {
    updateUser: (parent, args, context) => {
      if (!context.user) {
        throw new Error('Not authenticated');
      }
      const userId = context.user.id;
      users[userId].email = args.email;
      return users[userId];
    },
  },
};

const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: ({ req }) => {
    // This is where we'll extract authentication info from headers
    const token = req.headers.authorization || '';
    // In a real app, you'd verify this token with your auth provider
    const user = verifyToken(token); // Assume verifyToken exists
    return { user };
  },
});

// Dummy token verification function
function verifyToken(token) {
  if (token === 'Bearer valid-token-for-user-1') {
    return { id: 'user-1' };
  }
  return null;
}

server.listen().then(({ url }) => {
  console.log(`🚀 Server ready at ${url}`);
});

The context function is the central piece for authentication in GraphQL. It runs for every incoming request and has access to the original request object (req). We can attach any authentication-related information (like the authenticated user object) to the context object, making it available to all resolver functions.

When a client makes a request, it typically sends an Authorization header. In our example, we’re looking for a Bearer token. The verifyToken function (a placeholder here) would usually interact with an identity provider or a JWT validation library to confirm the token’s validity and extract user information. If the token is valid, we attach a user object to the context; otherwise, the user will be null or undefined.

Resolvers then check context.user to enforce access control. If context.user is missing, they can throw an AuthenticationError or a custom error, preventing unauthorized access to data or mutations.

A common pattern is to use JSON Web Tokens (JWTs). The client obtains a JWT upon successful login and then includes it in the Authorization: Bearer <token> header of subsequent GraphQL requests.

// Example GraphQL Request (using Apollo Client)
// {
//   "query": "query { me { id username email } }",
//   "variables": {}
// }
// Headers: { "Authorization": "Bearer valid-token-for-user-1" }

The most surprising thing about GraphQL authentication is that it’s not typically handled at the HTTP level with middleware applied to a single endpoint. Instead, it’s woven into the execution of the GraphQL query itself, specifically within the resolver chain via the context. This allows for fine-grained, operation-specific authentication and authorization checks.

When you define your context function in Apollo Server (or a similar mechanism in other GraphQL servers), you are essentially creating a per-request scope where you can prepare and pass down essential information. This includes not just the authenticated user but also data loaders, database connections, or any other resources that resolvers might need. The context acts as a bridge between the incoming HTTP request and the GraphQL execution engine.

Consider the scenario where you have different permissions for reading a user’s email versus updating it. Your Query.me resolver might only check if context.user exists, while Mutation.updateUser might also check if context.user.isAdmin is true. This level of detail is easily managed because the authentication context is readily available at every step of the GraphQL resolution.

The core mechanism for authorization often involves checking roles or permissions associated with the authenticated user object within the resolvers. For instance, a resolver might look like this:

// Inside resolvers.js
Mutation: {
  deleteUser: (parent, { id }, context) => {
    if (!context.user) {
      throw new Error('Not authenticated');
    }
    if (!context.user.isAdmin) {
      throw new Error('Not authorized');
    }
    // ... logic to delete user by id
    return { success: true };
  },
},

The one thing most people don’t realize is that while you can pass a JWT in the Authorization header, the verification of that token and the retrieval of the corresponding user object happens after the GraphQL request has been received by the server but before any of your specific GraphQL resolvers (Query.me, Mutation.updateUser, etc.) are executed. This happens within the context function you define for your Apollo Server. If the token verification fails in the context function, the user object in the context will be null, and subsequent resolvers can then immediately throw an authentication error, short-circuiting the rest of the query execution.

The next challenge you’ll likely face is implementing robust authorization, distinguishing between simply being logged in and having the permission to perform specific actions.

Want structured learning?

Take the full Graphql-tools course →