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
Before we get started, let’s set up GraphQL on the server. express-graphql, the standard for a quick and easy GraphQL endpoint, is currently too limited to handle auth, so we’ll make our own endpoint… with blackjack and, uh, JWTs.
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
On the client, we’ll want an easy way to send the JWT with every request, and then when the data comes back, we’ll want to mask the cryptic GraphQL errors so the client has something nice (albeit vague) to look at. Note: You’ll probably also want to install GraphiQL (the insanely beautiful GraphQL admin tool) to the admin section of your site. Code example here
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
Now that we’re all set up to send & receive tokens, we can build auth into our schema. As I see it, there are two useful auth (herein meaning authentication AND authorization) patterns. You can apply auth in the primary resolve() function of your query or mutation, which I call query-level auth, or you can auth at the specific field, which I call field-level auth.
Query Level Auth
Let’s say we have a query called getUserById. As you can imagine, you send in an ID, it looks that up in the database, and sends back a user. This is probably useful for administrative purposes. It’s also useful if, say, Alice wants to look up her own account. But you’ll probably want to keep Bob out of Alice’s profile. So, our query might look like this (using RethinkDB as a backend):
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
GraphQL is still young, so error handling leaves something to be desired. It got some things right, like catching errors thrown in child functions (as shown above); but, I also like multiple, field-specific errors. For example, if a user fails to log in, I’d like an overall error (eg “Login Failed”) and a field-specific error (eg “Password is incorrect”). We already fixed up the client to replace ugly internal GraphQL errors, and now we need to make sure that the majority of errors sent from the server are pretty.
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
Finally, we reach field-level auth. Now that Alice can only access her own user profile, we must ask ourselves, should we let her see everything? Should we let admin see everything? Probably not. For example, we’ve got a hashed password in there that no one should see. We’ve also got an email verification token that admin should see, but if Alice saw that, she could verify an email address that isn’t her own. Yikes! Let’s see how to solve it.
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
Getting on my soapbox for a second, I think GraphQL is great, but the decision to use only event-driven subscriptions instead of live-query is an answer to a problem that only 5 or 6 websites in the world have (facebook being one of them). Websockets can scale very well, just look at StackExchange: they manage 130,000+ concurrent websocket connections with only a handful of servers. So while GraphQL is cool new tech, don’t blindly follow facebook’s pattern to kill the realtime web. That’s why I figure it’s my duty to show you how easy it is to mix GraphQL 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
GraphQL is cool tech, but it’s overkill for small projects. It’s even overkill for something simple like user authentication, which uses 1 database table. However, if you invest the time to switch over, it’ll pay dividends. The beauty of having your queries & mutations mapped to schemas becomes invaluable as your team and app grow. It forces best practices, and I’m a huge fan of that (and if you like React, you are too). Also, after an hour of using GraphiQL to write your queries, you won’t be able to live without it. So, if your project warrants GraphQL, you might as well use it for your auth (and oauth2). Now if only there was a smarter client cache…