Apollo GraphQL – Private (Authentication)/Public API using Schema Directives/Annotation

This is one of the most common use case where we need to disable authentication for APIs such as Login API (Generate Access Token). Basically we will have all our APIs hosted in single instance of Apollo GraphQL server (We did not use any of the middleware such as Express). There are number of ways to solve this problem. The main idea behind the solution is that we should not throw error from the context.

Context: Do not throw any error from the context

Note: Make sure you build the context only If the Authorization header is present in the HTTP request. Do not assume that the header will always be available to avoid null pointer errors.

const apolloServer: ApolloServer = new ApolloServer({      
    context: async ({ req }) => {
        let context = null;
        try {
            context = //Build Context
            //If Unauthorized, set error context
            // context = {
            //    error: Unauthorized
            //};       
        }
        catch (Error e) {
            context = {
                error: // Anything as you like
            };
        }
        return context;
    });
}

Approach 1: Throw Error from Resolver (Typical Approach)

Resolver Example

export default {
    Query: {
        async testAPI(parent: any, args: any, context: any, info: any): Promise<any> {
            if(context.error) {
                throw context.error;
            }
            //Business Logic
        }
    }
}

You will have to add the above code in each of the resolver to throw the required error back the to the client.

Approach 2: Schema Directives

Authentication Directive

import { GraphQLField } from "graphql";
import { SchemaDirectiveVisitor } from "apollo-server";
export class AuthenticationDirective extends SchemaDirectiveVisitor {
   visitFieldDefinition(field: GraphQLField<any, any>) {
      const { resolve } = field;
      field.resolve = async function (source, args, context, info) {
         if (context.error) {
            throw context.error;
         }
         return resolve.apply(field, [source, args, context, info]);
      };
   }
}

Schema

export default gql`

   directive @authenticate on FIELD_DEFINITION

    extend type Query {
        #AuthenticationDirective will be executed
        persons: PersonInfo! @authenticate

        #AuthenticationDirective will not be executed since the annotation 
        #@authenticate is not added
        login: LoginInfo!
    }
}`

Apollo Server

Add schemaDirectives while initializing the Apollo Server instance.

const apolloServer: ApolloServer = new ApolloServer({      
    schemaDirectives: {
        authenticate: AuthenticationDirective
    },
    context: async ({ req }) => {
        let context = null;
        try {
            context = //Build Context
            //If Unauthorized, set error context
            // context = {
            //    error: Unauthorized
            //};       
        }
        catch (Error e) {
            context = {
                error: // Anything as you like
            };
        }
        return context;
    });
}

Note: We should not move the Authentication logic (API/DB calls) into the directive since the directive will be called for each query/mutation in the request. There might be better solutions as well. Kindly comment below If any.

Leave a Reply