v3
Guides
Securing Your GraphQL API

Securing Your GraphQL API

Building a secure GraphQL API is hard by design because of the “Graph” nature of GraphQL. Libraries for making different aspects of a GraphQL server secure have existed since the early days of GraphQL. However, combining those tools is often cumbersome and results in messy code. With envelop securing your server is now as easy as pie!

Protection against Malicious GraphQL Operations

One of the main benefits of GraphQL is that data can be requested individually. However, this also introduces the possibility for attackers to send operations with deeply nested selection sets that could block other requests being processed. Fortunately, infinite loops are not possible by design as a fragment cannot self-reference itself. Unfortunately, that still does not prevent possible attackers from sending selection sets that are hundreds of levels deep.

The following schema:

type Query {
  author(id: ID!): Author!
}
type Author {
  id: ID!
  posts: [Post!]!
}
type Post {
  id: ID!
  author: Author!
}

Would allow sending and executing queries such as:

query {
  author(id: 42) {
    posts {
      author {
        posts {
          author {
            posts {
              author {
                posts {
                  author {
                    posts {
                      author {
                        posts {
                          author {
                            id
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

There are a few measurements you can use for preventing the execution of such operations.

Persisted Operations

Instead of allowing any arbitrary GraphQL operation in production usage, we could use an allow-list of operations that the server is allowed to execute. We can collect such a list by scanning the code-base and extracting the list of operations. This can be done with GraphQL Codegen.

With the usePersistedOperations plugin such an extracted map can easily be used for allow-listing such operations.

import * as GraphQLJS from 'graphql'
import { envelop, useEngine } from '@envelop/core'
import { PersistedOperationsStore, usePersistedOperations } from '@envelop/persisted-operations'
import persistedOperations from './codegen-artifact'
 
const store: PersistedOperationsStore = {
  canHandle: key => key in persistedOperations,
  get: key => persistedOperations[key]
}
 
const getEnveloped = envelop({
  plugins: [
    useEngine(GraphQLJS),
    // ... other plugins ...
    usePersistedOperations({
      store: myStore,
      // only allow the operations within our persistedOperations object
      onlyPersistedOperations: true
    })
  ]
})

Reject Malicious Operation Documents

Parsing a GraphQL operation document is a very expensive and compute intensive operation that blocks the JavaScript event loop. If an attacker sends a very complex operation document with slight variations over and over again he can easily degrade the performance of the GraphQL server. Because of the variations simply having an LRU cache for parsed operation documents is not enough.

A potential solution is to limit the maximal allowed count of tokens within a GraphQL document.

In computer science, lexical analysis, lexing or tokenization is the process of converting a sequence of characters into a sequence of lexical tokens.

E.g. given the following GraphQL operation.

graphql {
  me {
    id
    user
  }
}

The tokens are query, {, me, {, id, user, } and }. Having a total count of 8 tokens.

The optimal maximum token count for your application depends on the complexity of the GrapHQL operations and documents. Usually 800-2000 tokens seems like a sane default.

A handy tool for analyzing your existing GraphQL operations and finding the best defaults for your use case is graphql-inspector.

Learn more about graphql-inspector audit here.

You can limit the amount of allowed tokens per operation and automatically abort any further processing of a GraphQL operation document that exceeds the limit with the maxTokensPlugin.

import * as GraphQLJS from 'graphql'
import { envelop, useEngine } from '@envelop/core'
import { maxTokensPlugin } from '@escape.tech/graphql-armor-max-tokens'
 
const getEnveloped = envelop({
  plugins: [
    useEngine(GraphQLJS),
    // ... other plugins ...
    maxTokensPlugin({
      maxTokenCount: 1000 // Number of tokens allowed in a document
    })
  ]
})

Query Depth Limiting

Sometimes persisted operations cannot be used. E.g. if you are building an API that is used by third party users. However, we can still apply some protection.

The maxDepthPlugin allows a maximum nesting level an operation is allowed to have.

import * as GraphQLJS from 'graphql'
import { envelop, useEngine } from '@envelop/core'
import { maxDepthPlugin } from '@escape.tech/graphql-armor-max-depth'
 
const getEnveloped = envelop({
  plugins: [
    useEngine(GraphQLJS),
    // ... other plugins ...
    maxDepthPlugin({
      n: 10 // Number of depth allowed
    })
  ]
})

This can prevent malicious API users executing GraphQL operations with deeply nested selection sets. You need to tweak the maximum depth an operation selection set is allowed to have based on your schema and needs, as it could vary between users.

A handy tool for analyzing your existing GraphQL operations and finding the best defaults is graphql-inspector.

Learn more about graphql-inspector audit here.

Rate Limiting

Rate-limiting is a common practice with APIs, and with GraphQL it gets more complicated because of the flexibility of the graph and the ability to choose what fields to query.

The useRateLimiter to limit access to resources, by a field level.

import * as GraphQLJS from 'graphql'
import { envelop, useEngine } from '@envelop/core'
import { useRateLimiter } from '@envelop/rate-limiter'
 
const getEnveloped = envelop({
  plugins: [
    useEngine(GraphQLJS),
    // ... other plugins ...
    useRateLimiter({
      // ...
    })
  ]
})

And then in your GraphQL schema, you can define the limitations in a declarative way:

type Query {
  posts: [Post]! @rateLimit(
    window: "5s", // time interval window for request limit quota
    max: 10,  // maximum requests allowed in time window
    message: "Too many calls!"  // quota reached error message
  )
}

Authentication

Authentication is the process of identifying who is doing a request against your server. Rolling out your own authentication solution is a crucial and important task as any flaws can result in severe security issues.

Auth0

We recommend using an existing third-party service such as Auth0 for most users. With the @envelop/auth0 plugin, you can simply bootstrap the authorization process.

import * as GraphQLJS from 'graphql'
import { useAuth0 } from '@envelop/auth0'
import { envelop, useEngine, useExtendContext, useSchema } from '@envelop/core'
import { schema } from './schema'
 
const getEnveloped = envelop({
  plugins: [
    useEngine(GraphQLJS),
    useSchema(schema),
    useAuth0({
      domain: 'YOUR_AUTH0_DOMAIN_HERE',
      audience: 'YOUR_AUTH0_AUDIENCE_HERE',
      extendContextField: 'auth0'
    }),
    // load user from database and add it to context
    // so it is available within the resolvers
    useExtendContext(async ({ auth0, db }) => {
      if (auth0.sub) {
        const user = await db.users.load(auth0.sub)
        return {
          user
        }
      }
      return {
        user: null
      }
    })
  ]
})

On the client you just need to add the Authorization: Bearer <auth_token> header and you are good! For a full hands-on guide for setting things up check out our Adding Authentication with Auth0 guide.

Check out the Auth0 Envelop plugin for all possible configuration options.

Generic Auth

In some cases using a third party auth provider is not possible. But now worries, the generic auth plugin has you covered! Learn more over here.

Authorization

Authorization is the process of allowing or denying the authenticated (or sometimes unauthenticated) user to access information. Since GraphQL is a graph, applying authorization based on field resolvers is handy and allows fine-grained control.

Your GraphQL graph might become quite complicated over time, having a strategy for ensuring correct authorization as the graph grows is mandatory.

Most of the time this logic should be applied within your business logic that is called within your resolvers, however, for some use-cases it is possible to apply authorization rules before any execution is even happening. E.g. if we want to prevent the execution of a GraphQL operation that selects fields the viewer is not allowed to see.

Right now envelop ships with two plugins that allow applying authorization before the execution phase.

Schema Based on Context

With the useSchemaByContext plugin it is possible to dynamically select a schema for execution based on the context object. This is handy if you have a public schema (e.g. for third-party API consumers) and a private schema (for in-house API consumers).

Libraries such as graphql-public-schema-filter can be used for generating a schema with only access to a sub part of the original schema using either SDL directives or schema field extensions.

import * as GraphQLJS from 'graphql'
import { envelop, useEngine, useSchemaByContext } from '@envelop/core'
import { privateSchema, publicSchema } from './schema'
 
const getEnveloped = envelop({
  plugins: [
    useEngine(GraphQLJS),
    // ... other plugins (e.g. useAuth0)
    useSchemaByContext(context => (context.isPrivateApiUser ? privateSchema : publicSchema))
  ]
})

Allow-List Operation Fields

With the useOperationFieldPermissions plugin you can automatically reject GraphQL operations that include specific field selections within the operations selection set. It works by extracting a set of schema coordinates from the context object. A custom validation rule will verify whether the selection only includes allowed selections and prevent the execution of the operation if it encounters any prohibited selections.

This plugin is perfect for use-cases where you want the whole schema being introspectable, but restrict access to a certain part of the Graph only to specific users. E.g. in a payment subscription model, where API users should only have access to the data that is included within the plan.

import * as GraphQLJS from 'graphql'
import { envelop, useEngine, useSchema } from '@envelop/core'
import { useOperationFieldPermissions } from '@envelop/operation-field-permissions'
import { schema } from './schema'
 
const getEnveloped = envelop({
  plugins: [
    useEngine(GraphQLJS),
    // ... other plugins (e.g. useAuth0)
    useSchema(schema),
    useOperationFieldPermissions({
      // user is only allowed to select the Query.hello field within operations.
      getPermissions: context => new Set(['Query.hello'])
    })
  ]
})

Trying to execute the following operation:

query {
  notHello
}

would result in the following error:

Response
{
  "data": null,
  "errors": [
    {
      "message": "Insufficient permissions for selecting 'Query.notHello'.",
      "locations": [
        {
          "line": 2,
          "column": 2
        }
      ]
    }
  ]
}

Prevent Leaking Sensitive Information

Error Masking

In most GraphQL servers any thrown error or rejected promise will result in the original error leaking to the outside world. Some frameworks have custom logic for catching unexpected errors and mapping them to an unexpected error instead. With envelop this abstraction is now possible with any server! Just add the useMaskedErrors plugin and throw EnvelopError instances for expected errors that should leak to the outside world. You can also add custom extension fields that will also be sent to the clients.

import * as GraphQLJS from 'graphql'
import { envelop, EnvelopError, useEngine, useMaskedErrors, useSchema } from '@envelop/core'
import { makeExecutableSchema } from '@graphql-tools/schema'
 
const schema = makeExecutableSchema({
  typeDefs: /* GraphQL */ `
    type Query {
      something: String!
      somethingElse: String!
      somethingSpecial: String!
    }
  `,
  resolvers: {
    Query: {
      something() {
        throw new EnvelopError('Error that is propagated to the clients.')
      },
      somethingElse() {
        throw new Error("Unsafe error that will be masked as 'Unexpected Error.'.")
      },
      somethingSpecial() {
        throw new EnvelopError('The error will have an extensions field.', {
          code: 'ERR_CODE',
          randomNumber: 123
        })
      }
    }
  }
})
 
const getEnveloped = envelop({
  plugins: [useEngine(GraphQLJS), useSchema(schema), useMaskedErrors()]
})

For people migrating from apollo-server the useApolloServerErrors plugin provides full backwards-compatibility to how apollo-server handles GraphQL error masking.

Disable Schema Introspection

If your schema includes sensitive information that you want to hide from the outside world, disabling the schema introspection is a possible solution. The useDisableIntrospection plugin solves that in a single line of code!

import * as GraphQLJS from 'graphql'
import { envelop, useEngine } from '@envelop/core'
import { useDisableIntrospection } from '@envelop/disable-introspection'
 
const getEnveloped = envelop({
  plugins: [useEngine(GraphQLJS), useDisableIntrospection()]
})

However, just disabling introspection is not enough as graphql.js by default produces hints for possible selection set “typos” when querying for invalid selection sets. A potential attacker could still be getting all the schema information by brute-forcing a lot of operations against the API.

Thus, we rather recommend using persisted operations instead of disabling schema introspection.

Block Field Suggestions

If you disabled schema introspection, you should also disable field suggestions as these allow reverse-engineering a GraphQL schema.

import * as GraphQLJS from 'graphql'
import { envelop, useEngine } from '@envelop/core'
import { useRateLimiter } from '@envelop/rate-limiter'
import { blockFieldSuggestions } from '@escape.tech/graphql-armor-block-field-suggestions'
 
const getEnveloped = envelop({
  plugins: [useEngine(GraphQLJS), blockFieldSuggestions()]
})