Access Control Best Practices for GraphQL with Authentication and Authorization

Confusion between authentication and authorization causes data leaks. Learn the difference and how to implement the right access control pattern in your GraphQL API.

Access Control Best Practices for GraphQL with Authentication and Authorization

This post is part of our series about GraphQL security, where we cover (almost) every vulnerability to give you the right tools to understand and solve them 🛠️

One random Wednesday morning, your team received several emails from your users complaining that they don’t have access to your web application anymore. You start investigating what could have happened, and a few moments later, you find out that your whole database is compromised!

However, it is not the worst… later on, you find out that your database is now available on the darknet and that all your users’ information is online: their email, password, address, bank account, everything!

How is it possible?

It seems that an attacker has found a way to access your entire database, and after copying it, he has overwritten it and made it unusable…

You are very confused because you are particularly proud of your authentication system and no user can access your application without logging in with his email and password. How did all of this happen then?

You may have confused authentication and authorization…


Catching and avoiding access control vulnerabilities can be tricky. Escape GraphQL Security Platform allows you to find access control misconfiguration as well as 100+ other GraphQL security vulnerabilities.


Authentication vs Authorization

First, we need to go back to the difference between authentication and authorization.

Authentication is the mechanism to identify users securely. Authentication systems seek to provide answers to these questions:

  • Who is the user?
  • Is the user really the one they pretend to be?

This task is often performed with user credentials (e.g. email and password) or token.

Authorization is the process that determines what level of access a particular user should have to resources controlled by the system. Authorization systems hence provide answers to these questions:

  • Is the user authorised to access this specific resource?
  • Is the user authorised to perform this specific operation?
  • Is the user authorised to perform this specific operation on this specific resource?

⚠️ Once the user is authenticated, the authorization process must still be performed!

In our introduction example, the attacker is a simple user with a valid account, so first, he authenticates like any other user. Then, as there isn’t any authorization process in place, he can access everything, and he just asks the database for other user information (modifying URL or using Postman for example).

This vulnerability comes from a lack or a misconfiguration of Access Control.

Therefore, a user can access objects and object fields they are not supposed to access.

It is a common but harmful vulnerability, which often appears in OWASP’s Top 10.

To avoid it, developers must ensure that the authentication and authorization processes are properly implemented.

Access Control best practices to secure your GraphQL API

To implement authorization properly, you need to define an access control policy. Here are the main concepts:

  • Enforce least privileges: assign to your users the minimum privilege necessary for their usage (ABAC can help doing so, read below),
  • Deny by default: sometimes there is no matching rules and the application must still make a decision, in this case, you should deny the access to the requested resources,
  • Perform server-side authorization: you must never rely on client-side access control checks, which are too easy to bypass. Validate the permissions on every request: the authorization process must always be performed in the same way no matter how the request was made. As a consequence, you must place your authorization process in your business logic layer.

Placing your code in the business logic layer ensures that all API routes must comply with the same authorization policy. This is also much more maintainable (all access control policies live in one place).

Concretely it means that you need to place all your authorization logic and functions in a separate file and then call the functions in your resolvers (code example at the end).

If you use a code-first approach, you can go one step further and completely decouple the GraphQL layer from the business logic layer. This method allows you to treat access control policies in configuration files instead of code.

  • Prefer Attribute Based Access Control (ABAC) over role based (RBAC):

RBAC may look much simpler, but ABAC has many advantages over RBAC.

RBAC assigns roles (administrator, editor, author, etc) to users, while ABAC assigns attributes (job role, project name, time of day, MAC address) to user and assets, and then permissions are granted according to policies based on roles or attributes.

Even if RBAC is still very popular, ABAC should be prefered for the following reasons:

  • it allows for more complex and fine-grained permission configuration (attributes can be based off of time, geographic location, device, etc)
  • it is more robust, especially in large projects when numerous roles are present without always having a strict hierarchy
  • it is easier to maintain and update. Indeed, even if the initial setup of RBAC can be simpler with fewer roles, it quickly becomes far more difficult to manage. ABAC is more expressive and its attribute-based logic is closer to real world concerns, which makes it easier to update.
  • Make sure lookup IDs cannot be tampered with: when a user is authenticated, he has access to this account details, but he must not be able to look at another user account! A simple example is when the user ID is present in the URL and the user can just change it to access another user's account. Even if IDs are complex to guess, it is not enough to make sure an attacker will not access another user account. To avoid it, there exist methods to avoid exposing user ID (JSON Web Token (JWT) or server-side session for example), and you must also perform access control checks on every request.
  • Enforce authorization checks on static resources: depending on your company context, not only your database needs to be protected, but also your static resources.
  • Implement appropriate logging: logging is essential to quickly react during or after an attack. Make sure to implement consistent logs with a well-defined format (easy to parse and analyse) and with the appropriate amount of information.
  • Create unit test cases for authorization logic: these tests ensure your code behaves as expected even during future updates.
  • Be careful in using existing libraries: there are many wonderful libraries and frameworks, but you need to configure them well! Make sure to take the time to see the different configuration options and respect the concepts mentioned previously.

How to implement proper Access Control in GraphQL: code examples

Here we have a simple article management application, where users can publish articles or edit existing ones if they are editors and the article is editable.

// server.ts

const typeDefs = gql`
  type User {
    id: ID!
    username: String!
    position: String
  }
  type Article {
    id: ID!
    title: String!
    content: String!
    isEditable: Boolean
  }
  type Mutation {
    publishArticle(title: String!, content: String!): Article!,
    editArticle(id: ID!, title: String!, content: String): Article!
  }
`

const resolvers = {
    Mutation: {
    publishArticle: authenticated(
      validateCapacity('PUBLISH')((root, { title, content }, context) =>
        createNewArticle(title, content, context.currentUser)
      )
    ),
    editArticle: authenticated(
      validateCapacity('EDIT')((root, { title, content }, context) =>
        editArticle(title, content, context.currentUser)
      )
   )
  },
 }

Don’t forget to put your access control logic in a separate file and use ABAC!

//validate-capacity.ts

export const validateCapacity = (action) => (next) => (root, args, context, info) => {
  if (action == 'PUBLISH') {
    if (context.currentUser.position != 'EDITOR') {
    throw new Error(`Unauthorised action!`)
  }

    return next(root, args, context, info)
  }

  if (action == 'EDIT') {
    if (context.currentUser.position != 'EDITOR' || 
      context.currentArticle.isEditable == False ||
      !time in WORKING_HOURS){
    throw new Error(`Unauthorised action!`)
  }

  return next(root, args, context, info)
  }

  console.log('No corresponding access control policy')
}

Are my authentication mechanisms properly implemented in my GraphQL application?

The best way to know is to test it yourself for free in 1 minute using Escape, our platform dedicated to GraphQL Security.

Escape

Additional resources for GraphQL security

đź’ˇ
Do you prefer hands-on learning about GraphQL Security? Start your lessons with our API Security Academy focused on GraphQL and learn how to build safe GraphQL APIs.