|
| 1 | +--- |
| 2 | +title: Authorization Strategies |
| 3 | +--- |
| 4 | + |
| 5 | +GraphQL gives you complete control over how to define and enforce access control. |
| 6 | +That flexibility means it's up to you to decide where authorization rules live and |
| 7 | +how they're enforced. |
| 8 | + |
| 9 | +This guide covers common strategies for implementing authorization in GraphQL |
| 10 | +servers using GraphQL.js. It assumes you're authenticating requests and passing a user or |
| 11 | +session object into the `context`. |
| 12 | + |
| 13 | +## What is authorization? |
| 14 | + |
| 15 | +Authorization determines what a user is allowed to do. It's different from |
| 16 | +authentication, which verifies who a user is. |
| 17 | + |
| 18 | +In GraphQL, authorization typically involves restricting: |
| 19 | + |
| 20 | +- Access to certain queries or mutations |
| 21 | +- Visibility of specific fields |
| 22 | +- Ability to perform mutations based on roles or ownership |
| 23 | + |
| 24 | +## Resolver-based authorization |
| 25 | + |
| 26 | +> **Note:** |
| 27 | +> All examples assume you're using Node.js 20 or later with [ES module (ESM) support](https://nodejs.org/api/esm.html) enabled. |
| 28 | +
|
| 29 | +The simplest approach is to enforce access rules directly inside resolvers |
| 30 | +using the `context.user` value: |
| 31 | + |
| 32 | +```js |
| 33 | +export const resolvers = { |
| 34 | + Query: { |
| 35 | + secretData: (parent, args, context) => { |
| 36 | + if (!context.user || context.user.role !== 'admin') { |
| 37 | + throw new Error('Not authorized'); |
| 38 | + } |
| 39 | + return getSecretData(); |
| 40 | + }, |
| 41 | + }, |
| 42 | +}; |
| 43 | +``` |
| 44 | + |
| 45 | +This works well for smaller schemas or one-off checks. |
| 46 | + |
| 47 | +## Centralizing access control logic |
| 48 | + |
| 49 | +As your schema grows, repeating logic like `context.user.role !=='admin'` |
| 50 | +becomes error-prone. Instead, extract shared logic into utility functions: |
| 51 | + |
| 52 | +```js |
| 53 | +export function requireUser(user) { |
| 54 | + if (!user) { |
| 55 | + throw new Error('Not authenticated'); |
| 56 | + } |
| 57 | +} |
| 58 | + |
| 59 | +export function requireRole(user, role) { |
| 60 | + requireUser(user); |
| 61 | + if (user.role !== role) { |
| 62 | + throw new Error(`Must be a ${role}`); |
| 63 | + } |
| 64 | +} |
| 65 | +``` |
| 66 | + |
| 67 | +You can use these helpers in resolvers: |
| 68 | + |
| 69 | +```js |
| 70 | +import { requireRole } from './auth.js'; |
| 71 | + |
| 72 | +export const resolvers = { |
| 73 | + Mutation: { |
| 74 | + deleteUser: (parent, args, context) => { |
| 75 | + requireRole(context.user, 'admin'); |
| 76 | + return deleteUser(args.id); |
| 77 | + }, |
| 78 | + }, |
| 79 | +}; |
| 80 | +``` |
| 81 | + |
| 82 | +This pattern makes your access rules easier to read, test, and update. |
| 83 | + |
| 84 | +## Field-level access control |
| 85 | + |
| 86 | +You can also conditionally return or hide data at the field level. This |
| 87 | +is useful when, for example, users should only see their own private data: |
| 88 | + |
| 89 | +```js |
| 90 | +export const resolvers = { |
| 91 | + User: { |
| 92 | + email: (parent, args, context) => { |
| 93 | + if (context.user.id !== parent.id && context.user.role !== 'admin') { |
| 94 | + return null; |
| 95 | + } |
| 96 | + return parent.email; |
| 97 | + }, |
| 98 | + }, |
| 99 | +}; |
| 100 | +``` |
| 101 | + |
| 102 | +Returning `null` is a common pattern when fields should be hidden from |
| 103 | +unauthorized users without triggering an error. |
| 104 | + |
| 105 | +## Declarative authorization with directives |
| 106 | + |
| 107 | +If you prefer a schema-first or declarative style, you can define custom |
| 108 | +schema directives like `@auth(role: "admin")` directly in your SDL: |
| 109 | + |
| 110 | +```graphql |
| 111 | +type Query { |
| 112 | + users: [User] @auth(role: "admin") |
| 113 | +} |
| 114 | +``` |
| 115 | + |
| 116 | +To enforce this directive during execution, you need to inspect it in your resolvers |
| 117 | +using `getDirectiveValues`: |
| 118 | + |
| 119 | +```js |
| 120 | +import { getDirectiveValues } from 'graphql'; |
| 121 | + |
| 122 | +function withAuthCheck(resolverFn, schema, fieldNode, variableValues, context) { |
| 123 | + const directive = getDirectiveValues( |
| 124 | + schema.getDirective('auth'), |
| 125 | + fieldNode, |
| 126 | + variableValues |
| 127 | + ); |
| 128 | + |
| 129 | + if (directive?.role && context.user?.role !== directive.role) { |
| 130 | + throw new Error('Unauthorized'); |
| 131 | + } |
| 132 | + |
| 133 | + return resolverFn(); |
| 134 | +} |
| 135 | +``` |
| 136 | + |
| 137 | +You can wrap individual resolvers with this logic, or apply it more broadly using a |
| 138 | +schema visitor or transformation. |
| 139 | + |
| 140 | +GraphQL.js doesn't interpret directives by default, they're just annotations. |
| 141 | +You must implement their behavior manually, usually by: |
| 142 | + |
| 143 | +- Wrapping resolvers in custom logic |
| 144 | +- Using a schema transformation library to inject authorization checks |
| 145 | + |
| 146 | +Directive-based authorization can add complexity, so many teams start with |
| 147 | +resolver-based checks and adopt directives later if needed. |
| 148 | + |
| 149 | +## Best practices |
| 150 | + |
| 151 | +- Keep authorization logic close to business logic. Resolvers are often the |
| 152 | +right place to keep authorization logic. |
| 153 | +- Use shared helper functions to reduce duplication and improve clarity. |
| 154 | +- Avoid tightly coupling authorization logic to your schema. Make it |
| 155 | +reusable where possible. |
| 156 | +- Consider using `null` to hide fields from unauthorized users, rather than |
| 157 | +throwing errors. |
| 158 | +- Be mindful of tools like introspection or GraphQL Playground that can |
| 159 | +expose your schema. Use caution when deploying introspection in production |
| 160 | +environments. |
| 161 | + |
| 162 | +## Additional resources |
| 163 | + |
| 164 | +- [Anatomy of a Resolver](./resolver-anatomy): Shows how resolvers work and how the `context` |
| 165 | +object is passed in. Helpful if you're new to writing custom resolvers or |
| 166 | +want to understand where authorization logic fits. |
| 167 | +- [GraphQL Specification, Execution section](https://spec.graphql.org/October2021/#sec-Execution): Defines how fields are |
| 168 | +resolved, including field-level error propagation and execution order. Useful |
| 169 | +background when building advanced authorization patterns that rely on the |
| 170 | +structure of GraphQL execution. |
| 171 | +- [`graphql-shield`](https://github.com/dimatill/graphql-shield): A community library for adding rule-based |
| 172 | +authorization as middleware to resolvers. |
| 173 | +- [`graphql-auth-directives`](https://github.com/the-guild-org/graphql-auth-directives): Adds support for custom directives like |
| 174 | +`@auth(role: "admin")`, letting you declare access control rules in SDL. |
| 175 | +Helpful if you're building a schema-first API and prefer declarative access |
| 176 | +control. |
| 177 | + |
| 178 | + |
0 commit comments