4 min read

Securing GraphQL - Pt. 1: Basic Auth

Authentication is a must for any graphql server that can be accessed via the public internet.
Securing GraphQL - Pt. 1: Basic Auth

Authentication is a must for any graphql server that can be accessed via the public internet. While graphql's single endpoint and querying method can add a little complexity to the task vs. a REST setup, you will find that there are quite a few methods that shouldn't take too much effort to implement.

Setup

For these examples, we will use Apollo Server (and Typescript because we are not barbarians!)

We are utilizing a fake auth check function. For any actual implementation, you will need to replace this with a function that calls your auth provider.

 /// fake auth check
export function validateToken(token: string): boolean {
return token === "testToken";
}

Complete code examples for all these solutions can be found on my GitHub.

Super Simple: Request Context

The easiest way to implement a simple auth check for your graph is directly in the request context. This method has a few benefits and drawbacks:

Pros:
- Very simple to implement.
- Single call to your auth provider per graphql request.
Cons:
- No fine grain control. You either can or can not access the graph based on if your request passes the auth check.

startStandaloneServer(server, {
  context: async ({req}) => {
    const token = req.headers.authorization;

    if (!validateToken(token)) throw new GraphQLError("invalid token");

    const context: GraphQLContext = {token};
    return context;
  },
})

To implement, you can create a function that pulls the request header's value that contains your auth key (JWT or otherwise), then run your check function against that value.
If the auth check fails, you can throw an GraphQLError to stop the execution of the request.

This solution would be best suited to a graph that only deals with a single tier of user, as you cannot differentiate roles or attributes without additional complexity.

Fine-Grained Control: Auth at the Resolver

This solution builds on the previous context solution by adding a wrapper function around resolvers.
The benefit of this method is that you can wrap resolvers that need auth protection and omit the wrapper on public resolvers.
The potential downside is that you are still limited in flexibility regarding roles or different permission sets (without much additional complexity).

type ResolverSignature<args, result> = (
  parent: ResolversParentTypes,
  args: args,
  context: GraphQLContext,
  info: GraphQLResolveInfo
) => result;

export function checkAuth<A, R>(
  resolver: ResolverSignature<A, R>
): ResolverSignature<A, R> {
  return (parent, args, context, info) => {
    // check auth
    if (!validateToken(context.token)) throw new GraphQLError("invalid token");

    // continue execution.
    return resolver(parent, args, context, info);
  };
}

This example is pretty Typescript heavy, so take your time to understand what is going on if you need to.

Effectively, we have a function that takes in two generic type arguments and your resolver function.
We will return a function that checks the auth and, if successful, returns the original resolver to be executed as usual.

As you can see from the ResolverSignature type, the generics should be

  1. The type of your query arguments.
  2. The type of your query result.

If you are using typeScript to build your graphQL server, you should already have these types on hand.

Utilizing our newly created checkAuth wrapper function can be done like so:

  Query: {
    getUsers: checkAuth<unknown, User[]>(getUsers),
  }

Our resolver is getUsers in this case, and to protect this with our auth wrapper, we only need to wrap the resolver function with checkAuth and provide the types of our args and our result (unknown and User[] in this case)

The All-in-One Solution: GraphQL Shield

As with most Node development, there is a perfect package solution that will save you from needing to develop your own solution from scratch, which with security, is a really good thing.

My preferred solution is GraphQL Shield, which is maintained by a big player in the graphql space, The Guild.

Setup requires a few packages and a little more setup than the previous examples, but you can create custom auth rules to check user roles and permissions; Shield also provides request caching, which helps validation speed and prevents duplicated validation calls.

The package's docs are excellent, and the system is easy to set up, but as part of my example repo, I'll provide some insight into the areas that I got hung up on.

Building Rule Functions

Rule functions make use of the advanced Javascript concept of currying. More info on currying here.
While the concept is not difficult, it can be confusing if you haven't seen it before.

Effectively, you will call the Shield rule() function, then pass a second set of arguments in the form of your custom auth rule rule({...rule config})(ruleFn).

A real-world example of this:

const isAuthenticated = rule({ cache: "contextual" })(
  async (parent, args, ctx: GraphQLContext, info) => {
    return ctx.token !== null;
  }
);

Permissions Tree

Your permissions tree object needs to match your resolver tree if you are getting errors like it seems like you have applied rules to ${typeErrors} types but Shield cannot find them in your schema., the issue is probably a mismatch between your permissions and resolvers.

Executable Schema

Passing a ready-made executable schema to your Apollo Server is the easiest way to use Shield. This is a little different than the standard method of passing typeDefs and resolvers.

Luckily, Graphql Tools has a function to turn your typeDefs and resolvers into a complete graphql schema object. You can pass this schema into your Apollo Server directly.

The process happens in two steps, which you could break out or do all in one line.
1. Create the schema object with makeExecutableSchema
2. Wrap the schema object with our permissions policy using the Graphql Middleware method applyMiddleware. This will apply our policy rules to the schema object.
The complete scheme definition and passing it to your server will look like this:

const schema = applyMiddleware(
  makeExecutableSchema({ typeDefs, resolvers }),
  permissions
);

const server = new ApolloServer<GraphQLContext>({
  cache: "bounded",
  schema,
});

I'm just getting started, but I could use your help!

If you found this article useful, please consider subscribing for free! If you really enjoyed it, becoming a supporting member will enable me to continue writing more articles and future level-up training offerings!