|
| 1 | +--- |
| 2 | +layout: guide |
| 3 | +search: true |
| 4 | +section: Authorization |
| 5 | +title: Overview |
| 6 | +desc: Overview of GraphQL authorization in general and an intro to the built-in framework. |
| 7 | +index: 0 |
| 8 | +--- |
| 9 | + |
| 10 | +Here's a conceptual approach to GraphQL authorization, followed by an introduction to the built-in authorization framework. Each part of the framework is described in detail in its own guide. |
| 11 | + |
| 12 | +## Authorization: GraphQL vs REST |
| 13 | + |
| 14 | +In a REST API, the common authorization pattern is fairly simple. Before performing the requested action, the server asserts that the current client has the required permissions for that action. For example: |
| 15 | + |
| 16 | +```ruby |
| 17 | +class PostsController < ApiController |
| 18 | + def create |
| 19 | + # First, check the client's permission level: |
| 20 | + if current_user.can?(:create_posts) |
| 21 | + # If the user is permitted, then perform the action: |
| 22 | + post = Post.create(params) |
| 23 | + render json: post |
| 24 | + else |
| 25 | + # Otherwise, return an error: |
| 26 | + render nothing: true, status: 403 |
| 27 | + end |
| 28 | + end |
| 29 | +end |
| 30 | +``` |
| 31 | + |
| 32 | +However, this request-by-request mindset doesn't map well to GraphQL because there's only one controller and the requests that come to it may be _very_ different. To illustrate the problem: |
| 33 | + |
| 34 | +```ruby |
| 35 | +class GraphqlController < ApplicationController |
| 36 | + def execute |
| 37 | + # What permission is required for `query_str`? |
| 38 | + # It depends on the string! So, you can't generalize at this level. |
| 39 | + if current_user.can?(:"???") |
| 40 | + MySchema.execute(query_str, context: ctx, variables: variables) |
| 41 | + end |
| 42 | + end |
| 43 | +end |
| 44 | +``` |
| 45 | + |
| 46 | +So, what new mindset will work with a GraphQL API? |
| 47 | + |
| 48 | +For __mutations__, remember that each mutation is like an API request in itself. For example, `Posts#create` above would map to the `createPost(...)` mutation in GraphQL. So, each mutation should be authorized in its own right. |
| 49 | + |
| 50 | +For __queries__, you can think of each individual _object_ like a `GET` request to a REST API. So, each object should be authorized for reading in its own right. |
| 51 | + |
| 52 | +By applying this mindset, each part of the GraphQL query will be properly authorized before it is executed. Also, since the different units of code are each authorized on their own, you can be sure that each incoming query will be properly authorized, even if it's a brand new query that the server has never seen before. |
| 53 | + |
| 54 | +## What About Authentication? |
| 55 | + |
| 56 | +As a reminder: |
| 57 | + |
| 58 | +- _Authentication_ is the process of determining what user is making the current request, for example, accepting a username and password, or finding a `User` in the database from `session[:current_user_id]`. |
| 59 | +- _Authorization_ is the process of verifying that the current user has permission to do something (or see something), for example, checking `admin?` status or looking up permission groups from the database. |
| 60 | + |
| 61 | +In general, authentication is _not_ addressed in GraphQL at all. Instead, your controller should get the current user based on the HTTP request (eg, an HTTP header or a cookie) and provide that information to the GraphQL query. For example: |
| 62 | + |
| 63 | +```ruby |
| 64 | +class GraphqlController < ApplicationController |
| 65 | + def execute |
| 66 | + # Somehow get the the current `User` from this HTTP request. |
| 67 | + current_user = get_logged_in_user(request) |
| 68 | + # Provide the current user in `context` for use during the query |
| 69 | + context = { current_user: current_user } |
| 70 | + MySchema.execute(query_str, context: context, ...) |
| 71 | + end |
| 72 | +end |
| 73 | +``` |
| 74 | + |
| 75 | +After your HTTP handler has loaded the current user, you can access it via `context[:current_user]` in your GraphQL code. |
| 76 | + |
| 77 | +## Authorization in Your Business Logic |
| 78 | + |
| 79 | +Before introducing GraphQL-specific authorization, consider the advantages of application-level authorization. (See the [GraphQL.org post](https://graphql.org/learn/authorization/) on the same topic.) For example, here's authorization mixed into the GraphQL API layer: |
| 80 | + |
| 81 | +```ruby |
| 82 | +field :posts, [Types::Post], null: false |
| 83 | + |
| 84 | +def posts |
| 85 | + # Perform an auth check in the GraphQL field code: |
| 86 | + if context[:current_user].admin? |
| 87 | + Post.all |
| 88 | + else |
| 89 | + Post.published |
| 90 | + end |
| 91 | +end |
| 92 | +``` |
| 93 | + |
| 94 | +The downside of this is that, when `Types::Post` is queried in other contexts, the same authorization check may not be applied. Additionally, since the authorization code is coupled with the GraphQL API, the only way to test it is via GraphQL queries, which adds some complexity to tests. |
| 95 | + |
| 96 | +Alternatively, you could move the authorization to your business logic, the `Post` class: |
| 97 | + |
| 98 | +```ruby |
| 99 | +class Post < ActiveRecord::Base |
| 100 | + # Return the list of posts which `user` may see |
| 101 | + def self.posts_for(user) |
| 102 | + if user.admin? |
| 103 | + self.all |
| 104 | + else |
| 105 | + self.published |
| 106 | + end |
| 107 | + end |
| 108 | +end |
| 109 | +``` |
| 110 | + |
| 111 | +Then, use this application method in your GraphQL code: |
| 112 | + |
| 113 | +```ruby |
| 114 | +field :posts, [Types::Post], null: false |
| 115 | + |
| 116 | +def posts |
| 117 | + # Fetch the posts this user can see: |
| 118 | + Post.posts_for(context[:current_user]) |
| 119 | +end |
| 120 | +``` |
| 121 | + |
| 122 | +In this case, `Post.posts_for(user)` could be tested _independently_ from GraphQL. Then, you have less to worry about in your GraphQL tests. As a bonus, you can use `Post.posts_for(user)` in _other_ parts of the app, too, such as the web UI or REST API. |
| 123 | + |
| 124 | +## GraphQL-Ruby's Authorization Framework |
| 125 | + |
| 126 | +Despite the advantages of authorization at the application layer, as described above, there might be some reasons to authorize in the API layer: |
| 127 | + |
| 128 | +- Have an extra assurance that your API layer is secure |
| 129 | +- Authorize the API request _before_ running it (see "visibility" below) |
| 130 | +- Integrate with code that doesn't have authorization built-in |
| 131 | + |
| 132 | +To accomplish these, you can use GraphQL-Ruby's authorization framework. The framework has three levels, each of which is described in its own guide: |
| 133 | + |
| 134 | +- {% internal_link "Visibility", "/authorization/visibility" %} hides parts of the GraphQL schema from users who don't have full permission. |
| 135 | +- {% internal_link "Accessibility", "/authorization/accessibility" %} prevents running queries which access parts of the GraphQL schema, unless users have the required permission. |
| 136 | +- {% internal_link "Authorization", "/authorization/authorization" %} checks application objects during execution to be sure the user has permission to access them. |
| 137 | + |
| 138 | +Also, GraphQL::Pro has integrations for CanCan and Pundit. |
0 commit comments