🎬 That's a Wrap for GraphQLConf 2024! • Watch the Videos • Check out the recorded talks and workshops
DocumentationAuthorization Strategies

GraphQL gives you complete control over how to define and enforce access control. That flexibility means it’s up to you to decide where authorization rules live and how they’re enforced.

This guide covers common strategies for implementing authorization in GraphQL servers using GraphQL.js. It assumes you’re authenticating requests and passing a user or session object into the context.

Before you start

All code examples in this guide use modern JavaScript with ES module (ESM) syntax.
To run them in Node.js, make sure to:

  • Add "type": "module" to your package.json, or
  • Use the .mjs file extension for your files

As of Node.js 22.7.0, module syntax detection is enabled by default. This means Node.js will attempt to run .js files using ES module syntax if it can’t parse them as CommonJS.

What is authorization?

Authorization determines what a user is allowed to do. It’s different from authentication, which verifies who a user is.

In GraphQL, authorization typically involves restricting:

  • Access to certain queries or mutations
  • Visibility of specific fields
  • Ability to perform mutations based on roles or ownership

Resolver-based authorization

The simplest approach is to enforce access rules directly inside resolvers using the context.user value:

export const resolvers = {
  Query: {
    secretData: (parent, args, context) => {
      if (!context.user || context.user.role !== 'admin') {
        throw new Error('Not authorized');
      }
      return getSecretData();
    },
  },
};

This works well for smaller schemas or one-off checks.

Centralizing access control logic

As your schema grows, repeating logic like context.user.role !=='admin' becomes error-prone. Instead, extract shared logic into utility functions:

export function requireUser(user) {
  if (!user) {
    throw new Error('Not authenticated');
  }
}
 
export function requireRole(user, role) {
  requireUser(user);
  if (user.role !== role) {
    throw new Error(`Must be a ${role}`);
  }
}

You can use these helpers in resolvers:

import { requireRole } from './auth.js';
 
export const resolvers = {
  Mutation: {
    deleteUser: (parent, args, context) => {
      requireRole(context.user, 'admin');
      return deleteUser(args.id);
    },
  },
};

This pattern makes your access rules easier to read, test, and update.

Field-level access control

You can also conditionally return or hide data at the field level. This is useful when, for example, users should only see their own private data:

export const resolvers = {
  User: {
    email: (parent, args, context) => {
      if (context.user.id !== parent.id && context.user.role !== 'admin') {
        return null;
      }
      return parent.email;
    },
  },
};

Returning null is a common pattern when fields should be hidden from unauthorized users without triggering an error.

Declarative authorization with directives

If you prefer a schema-first or declarative style, you can define custom schema directives like @auth(role: "admin"):

type Query {
  users: [User] @auth(role: "admin")
}

GraphQL.js doesn’t interpret directives by default, they’re just annotations. You must implement their behavior manually, usually by:

  • Wrapping resolvers in custom logic
  • Using a schema transformation library to inject authorization checks

Directive-based authorization can add complexity, so many teams start with resolver-based checks and adopt directives later if needed.

Best practices

  • Keep authorization logic close to business logic. Resolvers are often the right place to keep authorization logic.
  • Use shared helper functions to reduce duplication and improve clarity.
  • Avoid tightly coupling authorization logic to your schema. Make it reusable where possible.
  • Consider using null to hide fields from unauthorized users, rather than throwing errors.
  • Be mindful of tools like introspection or GraphQL Playground that can expose your schema. Use case when deploying introspection in production environments.

Additional resources

  • Anatomy of a Resolver: Shows how resolvers work and how the context object is passed in. Helpful if you’re new to writing custom resolvers or want to understand where authorization logic fits.
  • GraphQL Specification, Execution section: Defines how fields are resolved, including field-level error propagation and execution order. Useful background when building advanced authorization patterns that rely on the structure of GraphQL execution.
  • graphql-shield: A community library for adding rule-based authorization as middleware to resolvers.
  • graphql-auth-directives: Adds support for custom directives like @auth(role: "admin"), letting you declare access control rules in SDL. Helpful if you’re building a schema-first API and prefer declarative access control.