GraphQL Field Guide to Auth

Despite GraphQL being open sourced over 6 months ago, folks haven’t posted too many production-ready examples (maybe they’re too busy complaining about the state of JavaScript to actually contribute something positive :-P). So, when I converted my personal boilerplate over to GraphQL I figured out some neat stuff and thought I’d share.

All the code examples come straight out of a project I call Meatier, which I open sourced on github: http://github.com/mattkrick/meatier. It’s a boilerplate for a scalable, production-ready, realtime SaaS, but I’ll stay focused on GraphQL for this article. And when discussing auth, what better way to do it than to implement an authentication system (how meta!). This is written for an audience that already knows the basics of GraphQL. If you aren’t already comfortable with GraphQL…

Setting up GraphQL on the Server

import {graphql} from 'graphql';
import jwt from 'express-jwt';
app.post('/graphql',
jwt({secret: jwtSecret, credentialsRequired: false}), httpGraphQLHandler);
const httpGraphQLHandler = async (req, res) => {
const {query, variables, ...rootVals} = req.body;
const authToken = req.user || {};
const result = await graphql(Schema, query, {authToken, ...rootVals}, variables);
res.send(result);
}

The first thing we do is check for a JWT (a futuristic cookie, pronounced JOT) with the express-jwt middleware, which turns a legit JWT into a req.user object. This is the first auth gateway. Since we’re using GraphQL for things like signup and login, it’s possible the client doesn’t have her authToken yet, so we’ll let everyone in, JWT or not. We may also send along other things like a resetPasswordToken. To account for that, we’ll decompose all the body extras into something we call rootVals and pass it along with the authToken in the rootValue param. Now, we need to make sure the client is sending a JWT with every HTTP request…

Setting up GraphQL on the Client

export const getClientError = errors => {
if (!errors) return;
const error = errors[0].message;
return (error.indexOf('{"_error"') === -1) ? {_error: 'Server query error'} : JSON.parse(error);
}

export const fetchGraphQL = async (graphParams) => {
const authToken = localStorage.getItem('myToken');
const res = await fetch('http://localhost:3000/graphql', {
method: 'post',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify(graphParams)
});
const resJSON = await res.json();
const {data, errors} = resJSON;
return {data, error: getClientError(errors)}
}

We send the JWT by pulling it out of localStorage and passing it in the Authorization header. When data comes back, we check to see if it has errors & if the errors were thrown by an internal GraphQL function (and not my code) then we replace it with something readable by a non-geek. More on GraphQL error-handling shortcomings below.

Building Auth into your Queries

Query Level Auth

getUserById: {
type: User,
args: {
id: {type: new GraphQLNonNull(GraphQLID)}
},
async resolve(source, args, {rootValue}) {
isAdminOrSelf(rootValue, args);
const user = await r.table('users').get(args.id);
if (!user) {
throw errorObj({_error: 'User not found'});
}
return user;
}
},

It looks just like we expected, except for that call to isAdminOrSelf. Let’s take a deeper look:

export const isAdminOrSelf = ({authToken}, {id}) => {
const {id: verifiedId, isAdmin} = authToken;
if (!isAdmin && verifiedId !== id) {
throw errorObj({_error: 'Unauthorized'});
}
}

Here we’re making good use of our JWT that we received from the client. The JWT has an id and isAdmin field that we can trust because it was signed with our secret key. If that verifiedId from the JWT matches the id from the query, or the user is an admin, we’ll let the query continue. If not, we’ll throw an error back to the client. You can imagine how powerful this pattern becomes if you add roles to your JWT.

Side note: Error handling

I do this by sending a stringified object in an Error:

export const errorObj = obj => {
return new Error(JSON.stringify(obj));
}

GraphQL likes to also send an originalError field, which in this case will be identical to the error we throw. To cut it out (and reduce our payload by 50%) we put our stringified object inside an Error. This process isn’t ideal, but it allows for field-specific errors & makes it easy to determine if the error came from my code vs. GraphQL internal code.

Field Level Auth

fields: () => ({
isVerified: {type: GraphQLBoolean, description: 'Account state of email verification'},
password: {
type: GraphQLString,
description: 'Hashed password',
resolve: () => null
},
verifiedEmailToken: {
type: GraphQLString,
description: 'The token sent to the user\'s email for verification',
resolve: (source, args, info) => resolveForAdmin(source, args, info)
}
})

So what’s going on here? First, the password field resolves to null. period. That means no one can see it, not even a rogue admin user. Then, we do something special for the verifiedEmailToken, let’s take a look at that resolveForAdmin:

export const defaultResolveFn = (source, args, { fieldName }) => {
var property = source[fieldName];
return typeof property === 'function' ? property.call(source) : property;
};
export function resolveForAdmin(source, args, ref) {
return ref.rootValue.authToken.isAdmin ?
defaultResolveFn.apply(this, arguments) :
null;
}

Here’s where we get wild. We check the JWT to see if the user is an admin, and if so, we resolve as usual by using the defaultResolveFn that we stole straight out of the GraphQL source code. Otherwise, we return null. You can get as crazy as you want here, referencing peer fields, parent fields, etc. from the source argument. Done!

Bonus: GraphQL Auth with Websockets

socket.on('graphql', async (body, cb) => {
const {query, variables, ...rootVals} = body;
const authToken = socket.getAuthToken();
const result = await graphql(Schema, query, {authToken, ...rootVals}, variables);
const {error, data} = prepareClientError(result);
cb(error, data);
});

…What? That’s it? It’s even easier than the HTTP version!

Yep, that’s because we save the JWT server side on the connection (SocketCluster makes this a breeze). When you think about it, events are just glorified REST endpoints, and at the expense of a little bigger payload (the query) you don’t need to set up separate event handlers. Nice!

Closing Remarks

Building the future of work

Building the future of work