API Authorization with AWS and GraphQL

AWS API Gateway, Lambda Authorizer and Apollo GraphQL Server

I recently worked as an API Engineer on a project hosted on AWS, using Apollo GraphQL Server. The complexity in setting up those resources for GraphQL is that AWS products for APIs are centered around REST principles, offering means to have different behaviors and destinations depending on the path and the method, thus making public and private endpoints management complicated for GraphQL APIS.

In this article I will present a way to make it work using common AWS resources for APIs. I will rely on AWS API Gateway, using a lambda authorizer, and Apollo GraphQL Server.

REST and GraphQL Differences

With REST APIs we have the capability to define different rules down to a specific route or method. That granularity allows us to have public endpoints while the rest of the endpoints can be behind an authorization middleware.

With GraphQL, we only have one endpoint for the client to use, and we cannot reuse the same patterns as for REST to handle public and private queries and mutations.

One way to achieve that would be to create 2 distinct GraphQL servers, a server for public queries and mutations, and the second server for private ones. But that would mean duplicating resources and resolvers in a lot of cases.

How can I achieve that using only one GraphQL server?

API Gateway

The first thing I need to do is set up an API and create the resource that will represent the path on which the GraphQL server is available. On the resource, I will be able to define where to forward the request. Doing so, I can set up that resource to use a Lambda Authorizer.

Lambda Authorizer

AWS Documentation:

When a client makes a request to one of your API's methods, API Gateway calls your Lambda authorizer, which takes the caller's identity as input and returns an IAM policy as output.

The role of this lambda is to define whether or not the request is authorized. As an example I will be relying on Bearer Tokens, so that's when I will validate the JWT provided and grant access.

But the whole purpose here, is to also grant access to public endpoints. So instead of granting access solely on if the token is valid or not, I also need to grant access to requests without a token at all. The objective here is to provide the GraphQL server with context so it can determine if it's an authorized request or not.

exports.handler = async (event, context) => {
  // Define the skeleton of the Lambda Authorizer response
  const response = {
    context: {
      authenticated: false
    },
    // Provide a IAM Allow Policy Document
    policyDocument: getAcceptPolicyDocument()
  }
 
  // In case of no Authorization header, or if it's equal to none, grant access with authenticated: false in the context
  if (!event.headers.Authorization || event.headers.Authorization === 'none') {
    return context.succeed(response)
  }
 
  // Validate the JWT and populate the context with the result
  const authenticated = await isValid(event.headers.Authorization)
  response.context = { authenticated }
 
  // If the token is valid, grant access
  if (authenticated) {
    return context.succeed(response)
  }
 
  // Deny access with a 401 code
  return context.fail('Unauthorized')
}

GraphQL Middleware

Within the GraphQL Server, I will have to define the context based on what the Lambda Authorizer returned and apply middlewares on the schema. For applying the middlewares, I rely on the graphql-middleware package.

The first element is to create a function that will generate the configuration for the GraphQL Server. That function will expect the schema, the context and the middlewares to apply. If some middlewares are provided, we will overwrite the schema with the result of the function applyMiddleware provided by graphql-middleware.

The second element is the middleware itself. Based on graphql-middleware documentation, it has to return a function that will contain the logic of our middleware and that will eventually resolve the GraphQL request. This is where I will verify if the requested query or mutation is whitelisted to be public and if the user is authenticated or not, based on what the Lambda Authorizer provided in its response.

The logic here is to throw a 401 Unauthorized error if the query or mutation is not whitelisted and the user is not authenticated, otherwise, let the request resolve normally.

...
const { applyMiddleware } = require('graphql-middleware')
const { AuthenticationError } = require('apollo-server')
 
// This function allow me to apply middleware to the GraphQL server
const configureServer = ({ schema, context, middleware = [] }) => {
  const config = { schema, context, middleware }
 
  // If some middlewares are provided, apply them on the schema
  if (middleware.length) {
    config.schema = applyMiddleware(schema, ...middleware)
  }
 
  return config
}
 
// This is my middleware that provided with queries or mutations names, will let the request through or throw a 401 error
const authCheck = (whitelisted) => async (resolve, root, args, context, info) => {
  const isPublic = ['_service', ...whitelisted].includes(info.fieldName)
 
  // I am checking if the query or mutation in the request is whitelisted and if the user is authenticated when going through the Lambda Authorizer
  if (!isPublic && !context.authenticated) {
    throw new AuthenticationError('Unauthorized')
  }
 
  const result = await resolve(root, args, context, info)
  return result
}
 
// graphql-middleware allow me to specify different middlewares depending if it's a query or a mutation, here I use the same middleware for both
const authMiddleware = (publicQueries, publicMutations) => ({
  Query: authCheck(publicQueries),
  Mutation: authCheck(publicMutations)
})
 
// When creating the GraphQL configuration, I provide the middlewares to apply
const serverConfig = configureServer({
  schema,
  // The arguments that receives the context function will vary depending on how I am running the GraphQL Server, here it uses the event provided by a Lambda function
  context: ({ event }) => event.requestContext.authorizer.context,
  middleware: [authMiddleware(publicQueries, publicMutations)]
})
 
const server = new ApolloServer(serverConfig)
...

Summary

The logic behind creating public queries and mutations on a GraphQL Server relies on the ability to make a distinction between user that are authenticated and authorized and unauthenticated user.

Provided a list of whitelisted queries or mutations, we can then check if the current one being requested is part of it, and if the user is authenticated. If the user is not, and the query or mutation is not whitelisted, we can then return a 401 error.

Materials

Yorick Demichelis

Lead Software Engineer

Published: Jun 2, 2020

Updated: Oct 2, 20206 min read

More blogs

AWS Certified Team

Tech Holding Team is a AWS Certified & validates cloud expertise to help professionals highlight in-demand skills and organizations build effective, innovative teams for cloud initiatives using AWS.

By using this site, you agree to thePrivacy Policy.