Skip to content

Commit 4065ffc

Browse files
mandiwisebenjie
andauthored
Update authorization and pagination docs (#1814)
Co-authored-by: Benjie <[email protected]>
1 parent 75884df commit 4065ffc

File tree

2 files changed

+133
-53
lines changed

2 files changed

+133
-53
lines changed

src/pages/learn/authorization.mdx

Lines changed: 72 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,92 @@
11
# Authorization
22

3-
> Delegate authorization logic to the business logic layer
3+
<p className="learn-subtitle">Delegate authorization logic to the business logic layer</p>
4+
5+
Most APIs will need to secure access to certain types of data depending on who requested it, and GraphQL is no different. GraphQL execution should begin after [authentication](/graphql-js/authentication-and-express-middleware/) middleware confirms the user's identity and passes that information to the GraphQL layer. But after that, you still need to determine if the authenticated user is allowed to view the data provided by the specific fields that were included in the request. On this page, we'll explore how a GraphQL schema can support authorization.
6+
7+
## Type and field authorization
48

59
Authorization is a type of business logic that describes whether a given user/session/context has permission to perform an action or see a piece of data. For example:
610

711
_"Only authors can see their drafts"_
812

9-
Enforcing this kind of behavior should happen in the [business logic layer](/learn/thinking-in-graphs/#business-logic-layer). It is tempting to place authorization logic in the GraphQL layer like so:
13+
Enforcing this behavior should happen in the [business logic layer](/learn/thinking-in-graphs/#business-logic-layer). Let's consider the following `Post` type defined in a schema:
14+
15+
```graphql
16+
type Post {
17+
authorId: ID!
18+
body: String
19+
}
20+
```
21+
22+
In this example, we can imagine that when a request initially reaches the server, authentication middleware will first check the user's credentials and add information about their identity to the `context` object of the GraphQL request so that this data is available in every field resolver for the duration of its execution.
23+
24+
If a post's body should only be visible to the user who authored it, then we will need to check that the authenticated user's ID matches the post's `authorId` value. It may be tempting to place authorization logic in the resolver for the post's `body` field like so:
1025

1126
```js
12-
const postType = new GraphQLObjectType({
13-
name: 'Post',
14-
fields: {
15-
body: {
16-
type: GraphQLString,
17-
resolve(post, args, context, { rootValue }) {
18-
// return the post body only if the user is the post's author
19-
if (context.user && (context.user.id === post.authorId)) {
20-
return post.body
21-
}
22-
return null
23-
}
24-
}
27+
function Post_body(obj, args, context, info) {
28+
// return the post body only if the user is the post's author
29+
if (context.user && (context.user.id === obj.authorId)) {
30+
return obj.body
2531
}
26-
})
32+
return null
33+
}
2734
```
2835

29-
Notice that we define "author owns a post" by checking whether the post's `authorId` field equals the current user’s `id`. Can you spot the problem? We would need to duplicate this code for each entry point into the service. Then if the authorization logic is not kept perfectly in sync, users could see different data depending on which API they use. Yikes! We can avoid that by having a [single source of truth](/learn/thinking-in-graphs/#business-logic-layer) for authorization.
36+
Notice that we define "author owns a post" by checking whether the post's `authorId` field equals the current user’s `id`. Can you spot the problem? We would need to duplicate this code for each entry point into the service. Then if the authorization logic is not kept perfectly in sync, users could see different data depending on which API they use. Yikes! We can avoid that by having a [single source of truth](/learn/thinking-in-graphs/#business-logic-layer) for authorization, instead of putting it the GraphQL layer.
3037

31-
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. Here’s an example:
38+
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. Here’s an example of how authorization of the `Post` type's fields could be implemented separately:
3239

3340
```js
34-
// Authorization logic lives inside postRepository
35-
const postRepository = require('postRepository');
36-
37-
const postType = new GraphQLObjectType({
38-
name: 'Post',
39-
fields: {
40-
body: {
41-
type: GraphQLString,
42-
resolve(post, args, context, { rootValue }) {
43-
return postRepository.getBody(context.user, post)
44-
}
41+
// authorization logic lives inside `postRepository`
42+
export const postRepository = {
43+
getBody({ user, post }) {
44+
if (user?.id && (user.id === post.authorId)) {
45+
return post.body
4546
}
47+
return null
4648
}
47-
})
49+
}
50+
```
51+
52+
The resolver function for the post's `body` field would then call a `postRepository` method instead of implementing the authorization logic directly:
53+
54+
```js
55+
import { postRepository } from 'postRepository'
56+
57+
function Post_body(obj, args, context, info) {
58+
// return the post body only if the user is the post's author
59+
return postRepository.getBody({ user: context.user, post: obj })
60+
}
4861
```
4962
50-
In the example above, we see that the business logic layer requires the caller to provide a user object. If you are using GraphQL.js, the User object should be populated on the `context` argument or `rootValue` in the fourth argument of the resolver.
63+
In the example above, we see that the business logic layer requires the caller to provide a user object, which is available in the `context` object for the GraphQL request. We recommend passing a fully-hydrated user object instead of an opaque token or API key to your business logic layer. This way, we can handle the distinct concerns of [authentication](/graphql-js/authentication-and-express-middleware/) and authorization in different stages of the request processing pipeline.
64+
65+
## Using type system directives
66+
67+
In the example above, we saw how authorization logic can be delegated to the business logic layer through a function that is called in a field resolver. In general, it is recommended to perform all authorization logic in that layer, but if you decide to implement authorization in the GraphQL layer instead then one approach is to use [type system directives](/learn/schema/#directives).
68+
69+
For example, a directive such as `@auth` could be defined in the schema with arguments that indicate what roles or permissions a user must have to access the data provided by the types and fields where the directive is applied:
70+
71+
```graphql
72+
directive @auth(rule: Rule) on FIELD_DEFINITION
73+
74+
enum Rule {
75+
IS_AUTHOR
76+
}
77+
78+
type Post {
79+
authorId: ID!
80+
body: String @auth(rule: IS_AUTHOR)
81+
}
82+
```
83+
84+
It would be up to the GraphQL implementation to determine how an `@auth` directive affects execution when a client makes a request that includes the `body` field for `Post` type. However, the authorization logic should remain delegated to the business logic layer.
85+
86+
## Recap
87+
88+
To recap these recommendations for authorization in GraphQL:
5189
52-
We recommend passing a fully-hydrated User object instead of an opaque token or API key to your business logic layer. This way, we can handle the distinct concerns of [authentication](/graphql-js/authentication-and-express-middleware/) and authorization in different stages of the request processing pipeline.
90+
- Authorization logic should be delegated to the business logic layer, not the GraphQL layer
91+
- After execution begins, a GraphQL server should make decisions about whether the client that made the request is authorized to access data for the included fields
92+
- Type system directives may be defined and added to the types and fields in a schema to apply generalized authorization rules

src/pages/learn/pagination.mdx

Lines changed: 61 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
# Pagination
22

3-
> Different pagination models enable different client capabilities
3+
<p className="learn-subtitle">Traverse lists of objects with a consistent field pagination model</p>
44

5-
A common use case in GraphQL is traversing the relationship between sets of objects. There are a number of different ways that these relationships can be exposed in GraphQL, giving a varying set of capabilities to the client developer.
5+
A common use case in GraphQL is traversing the relationship between sets of objects. There are different ways that these relationships can be exposed in GraphQL, giving a varying set of capabilities to the client developer. On this page, we'll explore how fields may be paginated using a cursor-based connection model.
66

77
## Plurals
88

9-
The simplest way to expose a connection between objects is with a field that returns a plural type. For example, if we wanted to get a list of R2-D2's friends, we could just ask for all of them:
9+
The simplest way to expose a connection between objects is with a field that returns a plural [List type](/learn/schema/#list). For example, if we wanted to get a list of R2-D2's friends, we could just ask for all of them:
1010

1111
```graphql
1212
# { "graphiql": true }
13-
{
13+
query {
1414
hero {
1515
name
1616
friends {
@@ -22,10 +22,10 @@ The simplest way to expose a connection between objects is with a field that ret
2222

2323
## Slicing
2424

25-
Quickly, though, we realize that there are additional behaviors a client might want. A client might want to be able to specify how many friends they want to fetch; maybe they only want the first two. So we'd want to expose something like:
25+
Quickly, though, we realize that there are additional behaviors a client might want. A client might want to be able to specify how many friends they want to fetchmaybe they only want the first two. So we'd want to expose something like this:
2626

2727
```graphql
28-
{
28+
query {
2929
hero {
3030
name
3131
friends(first: 2) {
@@ -37,20 +37,22 @@ Quickly, though, we realize that there are additional behaviors a client might w
3737

3838
But if we just fetched the first two, we might want to paginate through the list as well; once the client fetches the first two friends, they might want to send a second request to ask for the next two friends. How can we enable that behavior?
3939

40-
## Pagination and Edges
40+
## Pagination and edges
4141

42-
There are a number of ways we could do pagination:
42+
There are several ways we could do pagination:
4343

4444
- We could do something like `friends(first:2 offset:2)` to ask for the next two in the list.
4545
- We could do something like `friends(first:2 after:$friendId)`, to ask for the next two after the last friend we fetched.
4646
- We could do something like `friends(first:2 after:$friendCursor)`, where we get a cursor from the last item and use that to paginate.
4747

48-
In general, we've found that **cursor-based pagination** is the most powerful of those designed. Especially if the cursors are opaque, either offset or ID-based pagination can be implemented using cursor-based pagination (by making the cursor the offset or the ID), and using cursors gives additional flexibility if the pagination model changes in the future. As a reminder that the cursors are opaque and that their format should not be relied upon, we suggest base64 encoding them.
48+
The approach described in the first bullet is classic _offset-based pagination_. However, this style of pagination can have performance and security downsides, especially for larger data sets. Additionally, if new records are added to the database after the user has made a request for a page of results, then offset calculations for subsequent pages may become ambiguous.
4949

50-
That leads us to a problem; though; how do we get the cursor from the object? We wouldn't want cursor to live on the `User` type; it's a property of the connection, not of the object. So we might want to introduce a new layer of indirection; our `friends` field should give us a list of edges, and an edge has both a cursor and the underlying node:
50+
In general, we've found that _cursor-based pagination_ is the most powerful of those designed. Especially if the cursors are opaque, either offset or ID-based pagination can be implemented using cursor-based pagination (by making the cursor the offset or the ID), and using cursors gives additional flexibility if the pagination model changes in the future. As a reminder that the cursors are opaque and their format should not be relied upon, we suggest base64 encoding them.
51+
52+
But that leads us to a problem—how do we get the cursor from the object? We wouldn't want the cursor to live on the `User` type; it's a property of the connection, not of the object. So we might want to introduce a new layer of indirection; our `friends` field should give us a list of edges, and an edge has both a cursor and the underlying node:
5153

5254
```graphql
53-
{
55+
query {
5456
hero {
5557
name
5658
friends(first: 2) {
@@ -67,14 +69,14 @@ That leads us to a problem; though; how do we get the cursor from the object? We
6769

6870
The concept of an edge also proves useful if there is information that is specific to the edge, rather than to one of the objects. For example, if we wanted to expose "friendship time" in the API, having it live on the edge is a natural place to put it.
6971

70-
## End-of-list, counts, and Connections
72+
## End-of-list, counts, and connections
7173

72-
Now we have the ability to paginate through the connection using cursors, but how do we know when we reach the end of the connection? We have to keep querying until we get an empty list back, but we'd really like for the connection to tell us when we've reached the end so we don't need that additional request. Similarly, what if we want to know additional information about the connection itself; for example, how many total friends does R2-D2 have?
74+
Now we can paginate through the connection using cursors, but how do we know when we reach the end of the connection? We have to keep querying until we get an empty list back, but we'd like for the connection to tell us when we've reached the end so we don't need that additional request. Similarly, what if we want additional information about the connection itself, for example, how many friends does R2-D2 have in total?
7375

74-
To solve both of these problems, our `friends` field can return a connection object. The connection object will then have a field for the edges, as well as other information (like total count and information about whether a next page exists). So our final query might look more like:
76+
To solve both of these problems, our `friends` field can return a connection object. The connection object will be an Object type that has a field for the edges, as well as other information (like total count and information about whether a next page exists). So our final query might look more like this:
7577

7678
```graphql
77-
{
79+
query {
7880
hero {
7981
name
8082
friends(first: 2) {
@@ -96,20 +98,50 @@ To solve both of these problems, our `friends` field can return a connection obj
9698

9799
Note that we also might include `endCursor` and `startCursor` in this `PageInfo` object. This way, if we don't need any of the additional information that the edge contains, we don't need to query for the edges at all, since we got the cursors needed for pagination from `pageInfo`. This leads to a potential usability improvement for connections; instead of just exposing the `edges` list, we could also expose a dedicated list of just the nodes, to avoid a layer of indirection.
98100

99-
## Complete Connection Model
101+
## Complete connection model
100102

101-
Clearly, this is more complex than our original design of just having a plural! But by adopting this design, we've unlocked a number of capabilities for the client:
103+
Clearly, this is more complex than our original design of just having a plural! But by adopting this design, we've unlocked several capabilities for the client:
102104

103105
- The ability to paginate through the list.
104106
- The ability to ask for information about the connection itself, like `totalCount` or `pageInfo`.
105107
- The ability to ask for information about the edge itself, like `cursor` or `friendshipTime`.
106108
- The ability to change how our backend does pagination, since the user just uses opaque cursors.
107109

108-
To see this in action, there's an additional field in the example schema, called `friendsConnection`, that exposes all of these concepts. You can check it out in the example query. Try removing the `after` parameter to `friendsConnection` to see how the pagination will be affected. Also, try replacing the `edges` field with the helper `friends` field on the connection, which lets you get directly to the list of friends without the additional edge layer of indirection, when that's appropriate for clients.
110+
To see this in action, there's an additional field in the example schema, called `friendsConnection`, that exposes all of these concepts:
111+
112+
```graphql
113+
interface Character {
114+
id: ID!
115+
name: String!
116+
friends: [Character]
117+
friendsConnection(first: Int, after: ID): FriendsConnection!
118+
appearsIn: [Episode]!
119+
}
120+
121+
type FriendsConnection {
122+
totalCount: Int
123+
edges: [FriendsEdge]
124+
friends: [Character]
125+
pageInfo: PageInfo!
126+
}
127+
128+
type FriendsEdge {
129+
cursor: ID!
130+
node: Character
131+
}
132+
133+
type PageInfo {
134+
startCursor: ID
135+
endCursor: ID
136+
hasNextPage: Boolean!
137+
}
138+
```
139+
140+
You can try it out in the example query. Try removing the `after` argument for the `friendsConnection` field to see how the pagination will be affected. Also, try replacing the `edges` field with the helper `friends` field on the connection, which lets you get directly to the list of friends without the additional edge layer of indirection, when appropriate for clients:
109141

110142
```graphql
111143
# { "graphiql": true }
112-
{
144+
query {
113145
hero {
114146
name
115147
friendsConnection(first: 2, after: "Y3Vyc29yMQ==") {
@@ -129,6 +161,14 @@ To see this in action, there's an additional field in the example schema, called
129161
}
130162
```
131163

132-
## Connection Specification
164+
## Connection specification
165+
166+
To ensure a consistent implementation of this pattern, the Relay project has a formal [specification](https://relay.dev/graphql/connections.htm) you can follow for building GraphQL APIs that use a cursor-based connection pattern - whether or not use you Relay.
167+
168+
## Recap
169+
170+
To recap these recommendations for paginating fields in a GraphQL schema:
133171

134-
To ensure a consistent implementation of this pattern, the Relay project has a formal [specification](https://facebook.github.io/relay/graphql/connections.htm) you can follow for building GraphQL APIs which use a cursor based connection pattern.
172+
- List fields that may return a lot of data should be paginated
173+
- Cursor-based pagination provides a stable pagination model for fields in a GraphQL schema
174+
- The cursor connection specification from the Relay project provides a consistent pattern for paginating the fields in a GraphQL schema

0 commit comments

Comments
 (0)