A guide to authentication in GraphQL with Passport.js

In this post, I want to talk about how you might go about authentication and authorization when using GraphQL



In this post, I want to talk about how you might go about authentication and authorization when using GraphQL. The examples I’m giving work for graphql-js, Facebook’s reference implementation, but they are quite generic and could be adapted to other implementations as well.

Before we get started, let’s make sure we’re all on the same page when it comes to the terms I’ll be using:

Authentication means checking the identity of the user making a request.

Authorization refers to the set of rules that is applied to determine what a user is allowed to see / do.

The GraphQL specification doesn’t tell you how to do authentication or authorization. That’s a good thing because it lets you use any technology you like, but it can be a bit confusing to developers who are new to GraphQL. For the rest of this post, I will write my thoughts on the topic so far and show a few options for authentication and authorization.

GraphQL is still relatively new, so a best practice has yet to emerge. I don’t claim to have all the answers, so if you have a different idea or a different opinion, feel free to share that in the comments so we can all learn together!

Alright, that’s enough disclaimers for now. Let’s get started…

Authentication

Unless your GraphQL API is completely public, your server will need to authenticate its users. Your options are twofold:
  1. Let the web server (e.g. express or nginx) take care of authentication.
  2. Handle authentication in GraphQL itself.

Both of these options have some advantages and some disadvantages.

If you do authentication in the web server, you can use a standard auth package (e.g. passport.js for express) and many existing authentication methods will work out of the box. You can also add and remove methods at your liking without modifying the GraphQL schema.

However, doing authentication in the web server has the downside of requiring the client to speak to an authentication endpoint in addition to the GraphQL endpoint.

. . .

Let’s start with the first option: doing authentication in the web server. What would that look like?

It’s quite straight forward. If you’re using express for example, you could use a middleware like passport.js and any authentication strategy it supports, for example username+password, or OAuth. The middleware will authenticate the user or reject/redirect the request if it fails. You’ll have to provide the requisite methods for persisting users and expose the login endpoints, but I’ll spare you the details — if you need them, you can look them up in the middleware’s documentation.

Once the user is authenticated, you simply pass the user information into GraphQL as context. If you were using express, graphql-js and passport.js, that would look something like this:
import { Schema } from ‘./schema/schema.js’; //your GraphQL schema import { graphql } from ‘graphql’; import bodyParser from ‘body-parser’; import express from ‘express’; import passport from ‘passport’; import session from ‘express-session’; import uuid from ‘node-uuid’;require(‘./auth.js’); //see snippet belowconst app = express(); const PORT = 3000;//passport's session piggy-backs on express-session app.use(session({ genid: function(req) { return uuid.v4(); }, secret: ‘Z3]GJW!?9uP”/Kpe’ }));app.use(passport.initialize()); app.use(passport.session());app.post('/graphql', (req, res) => { graphql(schema, req.body, { user: req.user }) .then((data) => { res.send(JSON.stringify(data)); }); });//login route for passport app.use(bodyParser.urlencoded({ extended: true }) ); app.post('/login', passport.authenticate('local', { successRedirect: '/', failureRedirect: '/login', failureFlash: true }) );app.listen(PORT, () => { console.log(“GraphQL server listening on port %s”, PORT); });

And in auth.js:
import passport from ‘passport’; let LocalStrategy = require(‘passport-local’).Strategy;import {DB} from ‘./schema/db.js’;passport.use(‘local’, new LocalStrategy( function(username, password, done) { let checkPassword = DB.Users.checkPassword( username, password); let getUser = checkPassword.then( (is_login_valid) => { if(is_login_valid){ return DB.Users.getUserByUsername( username ); } else { throw new Error(“invalid username or password”); } }) .then( ( user ) => { return done(null, user); }) .catch( (err) => { return done(err); }); } ));passport.serializeUser(function(user, done) { done(null, user.id); });passport.deserializeUser(function(id, done) { DB.Users.get(id).then( (user, err) => { return done(err, user); }); });

And voila, you now have access to the user object in the GraphQL resolver!

. . .

Alright, now let’s look at the second option: doing authentication in GraphQL. What would that look like?
A word of caution here: If you’re implementing authentication yourself, make sure to never store passwords in clear text or a MD5 or SHA-256 hash. Use something like bcrypt that is designed for this kind of thing. While we’re at it, make sure to not store your session tokens as-is on the server, you should hash them first.
Here, things get a little bit more complicated, because you have to do more of the heavy lifting yourself. To keep it at least reasonably simple, let’s go with usernames and passwords in this example. So an authentication flow could look like this:
First you get a session token (so your client doesn’t have to store the username and password in cleartext):
mutation { getSessionToken(username: 'alice', password: 'hard2remember') } > { getSessionToken: "6e37a03e-9ee4-42fd-912d-3f67d2d0d852" }

Then you pass this token in subsequent requests:
query { user(token: "6e37a03e-9ee4-42fd-912d-3f67d2d0d852"){ todoLists{ name } } }> { user { todoLists: [ "Today", "Tomorrow" ] } }

When your session is over, you can call a logout mutation which invalidates the session token.

On the surface, this looks quite reasonable, but it has some downsides:
  • Every query or mutation that needs permissions has to add the token field and check it. That can be a lot of repetition, if your schema is complex.
  • If any field besides your root queries need to be aware of the token, you have to pass it down by returning it from each resolver. So for example, if the todoLists needed to be filtered based on the logged in user, you would have to do the following:
user: { resolve: (root, {token}){ return { token: token, user: /*...*/ }; } },/* ... */todoLists: { resolve: (parent) => { let user = parent.user; let token = parent.token; /* Return lists filtered based on token ... */ } }

That looks bad enough, but if you need the token further down, it gets even worse. If your resolver returns a GraphQLList type, you’ll have to add the token to every item in that list.

Luckily, there’s a slightly better way in graphql-js: You can set the token on the context that every resolver receives as its third argument:
user: { resolve: (root, {token}, ctx){ ctx.token = token; //assuming ctx is already an object return { /* user */ }; } }

If you are doing multiple queries, you will still have to pass the token into each one, because they get executed in parallel. In that case, writing to the context is also not a great idea any more. There is however a neat trick — or an awful hack, depending on who you ask — you can use, but it only works for mutations:

You can write a login method, which sets the context. Since mutations are executed one after the other and not in parallel, you can be sure the context is set after the login mutation:
mutation { loginWithToken(token: "6e37a03e-9ee4-42fd-912d-3f67d2d0d852"), do_stuff(greeting: "Hello", name: "Tom"), do_more_stuff(submarine_color: "Yellow") }

I’ll let you in on a secret here, but please don’t shoot yourself in the foot with it: There is no difference between mutations and queries, except that mutations are executed serially, in the order given. That means the second mutation has to wait for the first to finish, the third has to wait for the second to finish, and so on. But there is nothing that prevents you from mutating state in your queries, and there is nothing that prevents you from calling a query a mutation.

That said, I would not recommend calling everything a mutation just to use the above trick. Parallel execution is an important for performance. As you can see, this approach is not very elegant, and the workaround writes the token to the context, just like the web server would. There may be cases where you cannot modify the web server and you’re forced to do authentication in GraphQL, but in most cases I think I would prefer to handle authentication in the web server. It’s not only more generic, but also more flexible.

. . .
As always, discussion and feedback is very welcome. Just leave your comments below!

Never miss a post from Sumit Sharma, when you sign up for Ednsquare.