In our journey into GraphQL on Azure we’ve only created endpoints that can be accessed by anyone. In this post we’ll look at how we can add authentication to our GraphQL server.
If you’re new to GraphQL on Azure, I’d encourage you to check out part 3 in which I go over how we can create a GraphQL server using Apollo and deploy that to an Azure Function, which is the process we’ll be using for this post.
Creating an application
The application we’re going to use today is a basic blog application, in which someone can authenticate against, create a new post with markdown and before saving it (it’ll just use an in-memory store). People can then comment on a post, but only if they are logged in.
Let’s start by defining set of types for our schema:
And now we have our schema ready to use. So let’s talk about authentication.
Authentication in GraphQL
Authentication in GraphQL is an interesting problem, as the language doesn’t provide anything for it, but instead relies on the server to provide the authentication and for you to work out how that is applied to the queries and mutations that schema defines.
Apollo provides some guidance on authentication, through the use of a context function, that has access to the incoming request. We can use this function to unpack the SWA authentication information and add it to the context object. To get some help here, we’ll use the @aaronpowell/static-web-apps-api-auth library, as it can tell us if someone is logged in and unpack the client principal from the header.
Let’s implement a context function to add the authentication information from the request (for this post, I’m going to skip over some of the building blocks and implementation details, such as how resolvers work, but you can find them in the complete sample at the end):
Here we’re using the npm package to set the isAuthenticated and user properties of the context, which works by unpacking the SWA authentication information from the header (you don’t need my npm package, it’s just helpful).
Applying Authentication with custom directives
This context object will be available in all resolvers, so we can check if someone is authenticated and the user info, if required. So now that that’s available, how do we apply the authentication rules to our schema? It would make sense to have something at a schema level to handle this, rather than a set of inline checks within the resolvers, as then it’s clear to someone reading our schema what the rules are.
GraphQL Directives are the answer. Directives are a way to add custom behaviour to GraphQL queries and mutations. They’re defined in the schema, and can be applied to a type, field, argument or query/mutation.
Let’s start by defining a directive that, when applied somewhere, requires a user to be authenticated:
This directive will be applied to any type, field or argument, and will only be applied if the isAuthenticated property of the context is true. So, where shall we use it? The logical first place is on all mutations that happen, so let’s update the mutation section of the schema:
We’ve now added @isAuthenticated to the MutationsObject Type in the schema. We could have added it to each of the Field Definitions, but it’s easier to just add it to the MutationsObject Type, want it on all mutations. Right now, we don’t have any query that would require authentication, so let’s just stuck with the mutation.
Implementing a custom directive
Defining the Directive in the schema only tells GraphQL that this is a thing that the server can do, but it doesn’t actually do anything. We need to implement it somehow, and we do that in Apollo by creating a class that inherits from SchemaDirectiveVisitor.
To implement these methods, we're going to need to override the resolve function of the fields, whether it's all fields of the Object Type, or a single field. To do this we'll create a common function that will be called:
You'll notice that we always pass in a GraphQLObjectType (either the argument or unpacking it from the field details), and that's so we can normalise the wrapper function for all the things we need to handle. We're also adding a _authRequired property to the field definition or object type, so we can check if authentication is required.
Note: If you're using TypeScript, as I am in this codebase, you'll need to extend the type definitions to have the new fields as follows:
We're going to first check if the directive has been applied to this object already or not, since the directive might be applied multiple times, we don't need to wrap what's already wrapped.
Next, we'll get all the fields off the Object Type, loop over them, grab their resolve function (if defined, otherwise we'll use the default GraphQL field resolver) and then wrap that function with our isAuthenticatedResolver function.
constisAuthenticatedResolver=(field:GraphQLField<any,any>,objectType:GraphQLObjectType,resolve:typeofdefaultFieldResolver):typeofdefaultFieldResolver=>(...args)=>{constauthRequired=field._authRequired||objectType._authRequired;if (!authRequired){returnresolve.apply(this,args);}constcontext=args[2];if (!context.isAuthenticated){thrownewAuthenticationError("Operation requires an authenticated user");}returnresolve.apply(this,args);};
This is kind of like partial application, but in JavaScript, we're creating a function that takes some arguments and in turn returns a new function that will be used at runtime. We're going to pass in the field definition, the object type, and the original resolve function, as we'll need those at runtime, so this captures them in the closure scope for us.
For the resolver, it is going to look to see if the field or object type required authentication, if not, return the result of the original resolver.
If it did, we'll grab the context (which is the 3rd argument to an Apollo resolver), check if the user is authenticated, and if not, throw an AuthenticationError, which is provided by Apollo, and if they are authenticated, we'll return the original resolvers result.
Using the directive
We've added the directive to our schema, created an implementation of what to do with that directive, all that's left is to tell Apollo to use it.
For this, we'll update the ApolloServer in our index.ts file:
The schemaDirectives property is where we'll tell Apollo to use our directive. It's a key/value pair, where the key is the directive name, and the value is the implementation.
Conclusion
And we're done! This is a pretty simple example of how we can add authentication to a GraphQL server using a custom directive that uses the authentication model of Static Web Apps.
We saw that using a custom directive allows us to mark up the schema, indicating, at a schema level, which fields and types require authentication, and then have the directive take care of the heavy lifting for us.
You can find the full sample application, including a React UI on my GitHub, and the deployed app is here, but remember, it's an in-memory store so the data is highly transient.
This repository contains a template for creating an Azure Static Web App projects using React + TypeScript.
In the template there is Create React App site using TypeScript and an api folder with an empty Azure Functions, also using TypeScript.
To get started, click the Use this template button to create a repository from this template, and check out the GitHub docs on using templates.
Running The Application
From a terminal run npm start from both the repository root and api folder to start the two servers, the web application will be on http://localhost:3000 and the API on http://localhost:7071. Alternatively, you can use the VS Code launch of Run full stack to run both together with debuggers attached.
If we look at the Author type, there's some fields available that we might want to restrict to just the current user, such as their email or ID. Let's create an isSelf directive that can handle this for us.
With this we're saying that the Author.name field is available to anyone, but everything else about their profile is restricted to just them. Now we can implement that directive:
import{UserInfo}from"@aaronpowell/static-web-apps-api-auth";import{AuthenticationError,SchemaDirectiveVisitor}from"apollo-server-azure-functions";import{GraphQLObjectType,defaultFieldResolver,GraphQLField}from"graphql";import{Author}from"../generated";import"./typeExtensions";constisSelfResolver=(field:GraphQLField<any,any>,objectType:GraphQLObjectType,resolve:typeofdefaultFieldResolver):typeofdefaultFieldResolver=>(...args)=>{constselfRequired=field._isSelfRequired||objectType._isSelfRequired;if (!selfRequired){returnresolve.apply(this,args);}constcontext=args[2];if (!context.isAuthenticated||!context.user){thrownewAuthenticationError("Operation requires an authenticated user");}constauthor=args[0]asAuthor;constuser:UserInfo=context.user;if (author.userId!==user.userId){thrownewAuthenticationError("Cannot access data across user boundaries");}returnresolve.apply(this,args);};exportclassIsSelfDirectiveextendsSchemaDirectiveVisitor{visitObject(type:GraphQLObjectType){this.ensureFieldsWrapped(type);type._isSelfRequired=true;}visitFieldDefinition(field:GraphQLField<any,any>,details:{objectType:GraphQLObjectType;}){this.ensureFieldsWrapped(details.objectType);field._isSelfRequired=true;}ensureFieldsWrapped(objectType:GraphQLObjectType){if (objectType._isSelfRequiredWrapped){return;}objectType._isSelfRequiredWrapped=true;constfields=objectType.getFields();for (constfieldNameofObject.keys(fields)){constfield=fields[fieldName];const{resolve=defaultFieldResolver}=field;field.resolve=isSelfResolver(field,objectType,resolve);}}}
This directive does take an assumption on how it's being used, as it assumes that the first argument to the resolve function is an Author type, meaning it's trying to resolve the Author through a query or mutation return, but otherwise it works very similar to the isAuthenticated directive, it ensures someone is logged in, and if they are, it ensures that the current user is the Author requested, if not, it'll raise an error.