🎬 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.

ℹ️

In production systems authorization should be handled in your business logic layer, not your GraphQL resolvers. GraphQL is intended to be a thin execution layer that calls into your application’s domain logic, which enforces access control.

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

You can implement simple authorization checks directly in resolvers using context.user:

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

This approach can help when you’re learning how context works or building quick prototypes. However, for production systems, you should enforce access control in your business logic layer rather than in GraphQL resolvers.

Centralizing access control logic

If you’re experimenting or building a small project, repeating checks like context.user.role !== 'admin' across resolvers can become error-prone. One way to manage that duplication is by extracting 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}`);
  }
}

Then use those 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 improves readability and reusability, but like all resolver-based authorization, it’s best suited for prototypes or early-stage development.

For production use, move authorization into your business logic layer. These helpers can still be useful there, but they should be applied outside the GraphQL execution layer.

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") directly in your SDL:

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

To enforce this directive during execution, you need to inspect it in your resolvers using getDirectiveValues:

import { getDirectiveValues } from 'graphql';
 
function withAuthCheck(resolverFn, schema, fieldNode, variableValues, context) {
  const directive = getDirectiveValues(
    schema.getDirective('auth'),
    fieldNode,
    variableValues
  );
 
  if (directive?.role && context.user?.role !== directive.role) {
    throw new Error('Unauthorized');
  }
 
  return resolverFn();
}

You can wrap individual resolvers with this logic, or apply it more broadly using a schema visitor or transformation.

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 in your business logic layer, not in your GraphQL resolvers.
  • 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 caution 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.