-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
Update authorization and pagination docs #1814
base: source
Are you sure you want to change the base?
Changes from 2 commits
0a8a63c
d1f0d5a
909fabf
a1df64e
a345622
f7292a7
57b4d4d
626c623
184c0a3
d32e433
4b502c7
5ef9c15
01d0d07
4fafb88
9aa424f
b0fe7e7
f8e82d2
cf48ffd
fc47a37
587906a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,52 +1,92 @@ | ||
# Authorization | ||
|
||
> Delegate authorization logic to the business logic layer | ||
<p className="learn-subtitle">Delegate authorization logic to the business logic layer</p> | ||
|
||
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. | ||
|
||
## Type and field authorization | ||
|
||
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: | ||
|
||
_"Only authors can see their drafts"_ | ||
|
||
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: | ||
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: | ||
|
||
```graphql | ||
type Post { | ||
authorId: ID! | ||
body: String | ||
} | ||
``` | ||
|
||
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. | ||
|
||
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: | ||
|
||
```js | ||
const postType = new GraphQLObjectType({ | ||
name: 'Post', | ||
fields: { | ||
body: { | ||
type: GraphQLString, | ||
resolve(post, args, context, { rootValue }) { | ||
// return the post body only if the user is the post's author | ||
if (context.user && (context.user.id === post.authorId)) { | ||
return post.body | ||
} | ||
return null | ||
} | ||
} | ||
function Post_body(obj, args, context, info) { | ||
// return the post body only if the user is the post's author | ||
if (context.user && (context.user.id === obj.authorId)) { | ||
return obj.body | ||
} | ||
}) | ||
return null | ||
} | ||
``` | ||
|
||
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. | ||
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. | ||
|
||
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: | ||
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: | ||
|
||
```js | ||
// Authorization logic lives inside postRepository | ||
const postRepository = require('postRepository'); | ||
|
||
const postType = new GraphQLObjectType({ | ||
name: 'Post', | ||
fields: { | ||
body: { | ||
type: GraphQLString, | ||
resolve(post, args, context, { rootValue }) { | ||
return postRepository.getBody(context.user, post) | ||
} | ||
// authorization logic lives inside `postRepository` | ||
export const postRepository = { | ||
getBody({ user, post }) { | ||
if (user?.id && (user.id === post.authorId)) { | ||
return post.body | ||
} | ||
return null | ||
} | ||
}) | ||
} | ||
``` | ||
|
||
The resolver function for the post's `body` field would then call a `postRepository` method instead of implementing the authorization logic directly: | ||
|
||
```js | ||
import { postRepository } from 'postRepository' | ||
|
||
function Post_body(obj, args, context, info) { | ||
// return the post body only if the user is the post's author | ||
return postRepository.getBody({ user: context.user, post: obj }) | ||
} | ||
``` | ||
|
||
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. | ||
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. | ||
|
||
## Using type system directives | ||
|
||
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. | ||
|
||
Another approach when implementing authorization checks for a GraphQL API is to use [type system directives](/learn/schema/#directives), where a directive such as `@auth` is defined in the schema with arguments that can indicate what roles or permissions a user must have to access the data provided by the and fields where the directive is applied. For example: | ||
|
||
```graphql | ||
directive @auth(rule: Rule) on FIELD_DEFINITION | ||
|
||
enum Rule { | ||
IS_AUTHOR | ||
} | ||
|
||
type Post { | ||
authorId: ID! | ||
body: String @auth(rule: IS_AUTHOR) | ||
} | ||
``` | ||
|
||
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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm struggling to see how this leaves the logic in the business logic layer - isn't it the GraphQL layer here that's dictating that the rule to use is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree, this one is a bit ambiguous. I thought it was worth mentioning the type system directive approach because I've seen it used so often (whether or not it's a great idea to mix up auth rules in a schema). But I struggled to come up with a good example here that was adjacent to the prior example and also adheres to what would be considered the best practice. If you'd rather not invite readers to consider this option, I can just remove the section. Alternatively, I could remove the example and just provide a couple sentences explaining that people may see the type system directive approach in the wild. Let me know. |
||
|
||
## Recap | ||
|
||
To recap these recommendations for authorization in GraphQL: | ||
|
||
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. | ||
- Authorization logic should be delegated to the business logic layer, not the GraphQL layer | ||
- 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 | ||
- Type system directives may be defined and added to the types and fields in a schema to apply generalized authorization rules |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Following on from https://github.com/graphql/graphql.github.io/pull/1814/files#r1852923240
Instead of trying to fit the auth directive approach into the "business logic layer" narrative, let's explicitly call it out as an alternative - something that people do but is not the recommended way:
(merge with next paragraph)
The remainder of the text in this section would also need light editorial to reflect this.
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good, addressed in 587906a.