Skip to content

docs: add page on authorization strategies #4396

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
May 28, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions website/pages/docs/_meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const meta = {
'object-types': '',
'mutations-and-input-types': '',
'authentication-and-express-middleware': '',
'authorization-strategies': '',
'-- 2': {
type: 'separator',
title: 'Advanced Guides',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Authentication and Express Middleware
sidebarTitle: Authentication & Middleware
title: Using Express Middleware with GraphQL.js
sidebarTitle: Using Express Middleware
---

import { Tabs } from 'nextra/components';
Expand Down Expand Up @@ -100,3 +100,5 @@ In a REST API, authentication is often handled with a header, that contains an a
If you aren't familiar with any of these authentication mechanisms, we recommend using `express-jwt` because it's simple without sacrificing any future flexibility.

If you've read through the docs linearly to get to this point, congratulations! You now know everything you need to build a practical GraphQL API server.

Want to control access to specific operations or fields? See [Authorization Strategies](\pages\docs\authorization-strategies.mdx).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it right that this is using backslashes?

161 changes: 161 additions & 0 deletions website/pages/docs/authorization-strategies.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
---
title: Authorization 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](https://nodejs.org/api/esm.html).
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

These features are supported in Node 12+ and fully stable in Node 16 and later.

## 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:

```js
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:

```js
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:

```js
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:

```js
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")`:

```graphql
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.
Comment on lines +151 to +152
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not feel like the right messaging to me; in general we encourage that authorization logic should be inside the business logic, and that resolvers should not be used for authorization. You should be able to expose the same business logic over GraphQL, REST, RPC, or any other API approach without having to re-implement authorization - GraphQL is just a method of access to these underlying methods.

From https://graphql.org/learn/authorization/:

Defining authorization logic inside the resolver is fine when learning GraphQL or prototyping. However, for a production codebase, delegate authorization logic to the business logic layer.

We also highlight this approach in the "thinking in graphs" article:

image

- 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](./resolver-anatomy): 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](https://spec.graphql.org/October2021/#sec-Execution): 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`](https://github.com/dimatill/graphql-shield): A community library for adding rule-based
authorization as middleware to resolvers.
- [`graphql-auth-directives`](https://github.com/the-guild-org/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.


Loading