The Ultimate A-Z Guide to GraphQL Authentication with JWT

Token-based versus Session-based GraphQL Authentication Best Practices

At some point when building your GraphQL API, you will have to decide who can interact with your data.

This is when you will need to figure out which user is logged in or out—Authentication, and what that user has permission to do or access—Authorization.

I have covered a step-by-step GraphQL authentication with JWT tokens example in the past. That article was based on a GraphQL API built using PostGraphile and PostgreSQL database. You should check it out.

This article discusses GraphQL Authentication with JWT tokens further by comparing the above approach with a popular alternative.

There are two broad ways of handling authentication in GraphQL APIs:

  1. Authentication via the GraphQL server: All users have to be logged in by the GraphQL server before they can query the endpoint—purely GraphQL workflows. Authentication is implemented in the GraphQL Schema
  2. Authentication via a Web Server e.g. Express and Passport: Users can make queries to the GraphQL endpoint once they are logged in. Authentication is implemented with middleware.

Choosing the right authentication technique for a GraphQL API can get tricky sometimes...

...and there are a number of viable options on both sides.

GraphQL Authentication via the GraphQL Server with JWT tokens

You might have realized that the approach illustrated in the PostGraphile tutorial falls in the first category.

It is a JWT token-based approach.

To use this approach:

  1. First get a jwt token:
  2. Then pass the token is subsequent requests.

In the PostGraphile example, the two PL/pgSQL functions: SIGNUP and SIGNIN, returned a jwt-token object which PostGraphile translated into an actual jwt token.

All we had to do to get the token was sign up or log in:

        
mutation {
  signup (
    input: {
      username: "Jill",
      email: "jill@example.com",
      password: "123456"
    }) {
    jwtToken
  }
}
        
      
        
{
  "data": {
    "signup": {
      "jwtToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYWRtaW4iLCJ1c2VyX2lkIjo2LCJuYW1lIjoiSmlsbCIsImlhdCI6MTUzMTU4MzUxMCwiZXhwIjoxNTMxNjY5OTEwLCJhdWQiOiJwb3N0Z3JhcGhpbGUiLCJpc3MiOiJwb3N0Z3JhcGhpbGUifQ.6aw1H2jlDUZmRxfHFM3hOGUv100L_iOHcQuVJJfVuMk"
    }
  }
}
        
      

Once we have this token. we can pass in subsequent request using:

        
query {
  user("jwtToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYWRtaW4iLCJ1c2VyX2lkIjo2LCJuYW1lIjoiSmlsbCIsImlhdCI6MTUzMTU4MzUxMCwiZXhwIjoxNTMxNjY5OTEwLCJhdWQiOiJwb3N0Z3JhcGhpbGUiLCJpc3MiOiJwb3N0Z3JhcGhpbGUifQ.6aw1H2jlDUZmRxfHFM3hOGUv100L_iOHcQuVJJfVuMk"){
    Posts{
      name
    }
  }
}
> { user { Posts: [ "Today", "Tomorrow" ] } }
        
      

When the session is over, all we have to do is call out an invalidation or recovation with a logout mutation.

Otherwise the session will expire.

With this approach:

  • Every query or mutation needs explicit permission (and it has to carry the token) which has to be checked. The schema can get complex.
  • Only the root queries know about the token: If the token is required elsewhere (in the resolvers), it has to be passed down with the context in the third argument.
        
user: {
  resolve: (root, {token}, context){
    context.token = token; //assuming ctx is already an object
    return { /* user */ };
  }
}
        
      

GraphQL Authentication via an External Server

This is the approach most REST APIs will use when migrating to GraphQL.

It handles authentication with a middleware without ever touching the GraphQL Schema. The middleware (such as Passport.js) will authenticate the user or reject / redirect if the request fails.

With the Passport.js approach, you can provide a method for persisting users and exposed endpoints.

Once the user is authenticated, you pass the user in with the context object.

Most developers tend to default to handling authentication with methods familiar from REST.

Which is fine.

Actually, in a REST API, we define an auth middleware and apply it to a number of endpoints we might want secured. This approach will work for a GraphQL server also:

        
const express    = require('express')
const bodyParser = require('body-parser')
const { graphqlExpress } = require('apollo-server-express')
const schema = require('./schema')
const jwt = require('express-jwt')

const app = express()

// bodyparser
app.use(bodyParser.json())

// authentication middleware
const authMiddleware = jwt({
  secret: 'securesecret'
})

app.use(authMiddleware)

app.use('/api', graphqlExpress(req => ({
  schema,
  context: {
    user: req.user
  }
})))

app.listen(4000, () => {
  console.log('Server is up on 4000')
})
        
      

Since GraphQL has only a single endpoint, through which all requests are made, we simply apply the middleware to that endpoint.

Just like in REST, the jwt will check if an Authorization header with a valid token exists in every request made to the GraphQL endpoint.

If present, it will decode it then add a user object to the request. Otherwise, the user will be null.

We can then pass it in GraphQL the context object and use it however we like.

We can add a login resolver function to verify the details provided by the user with what is in the database as follows:

        
const bcrypt = require('bcrypt')
const jwt = require('jsonwebtoken')

login (_, { email, password }) {
  const user = await User.findOne({ where: { email } })

  if (!user) {
      throw new Error('No user with that email')
  }

  const valid = await bcrypt.compare(password, user.password)

  if (!valid) {
      throw new Error('Your password was incorrect!')
  }

  // return jwt
  return jwt.sign({
      id: user.id,
      email: user.email
  }, 'securesecret', { expiresIn: '1m' })
}
        
      

Once the user is verified, the user is authenticated and a JWT is generated and returned as the response.

Authentication with Auth0

And if we are using AuthO:

        
const express    = require('express')
const bodyParser = require('body-parser')
const { graphqlExpress } = require('apollo-server-express')
const schema = require('./schema')
const jwt = require('express-jwt')
const jwksRsa = require('jwks-rsa')

const app = express()

// bodyparser
app.use(bodyParser.json())

// authentication middleware
const authMiddleware = jwt({
  // dynamically provide a signing key based on the kid in the header and
  // the signing keys provided by the JWKS endpoint.
  secret: jwksRsa.expressJwtSecret({
    cache: true,
    rateLimit: true,
    jwksRequestsPerMinute: 5,
    jwksUri: `https://YOUR_AUTH0_DOMAIN/.well-known/jwks.json`
  }),

  // validate the audience and the issuer.
  audience: '{YOUR_API_IDENTIFIER}',
  issuer: `https://YOUR_AUTH0_DOMAIN/`,
  algorithms: ['RS256']
})

app.use(authMiddleware)

app.use('/api', graphqlExpress(req => ({
  schema,
  context: {
    user: req.user
  }
})))

app.listen(4000, () => {
  console.log('Server is up on 4000')
})
        
      

Here, the authentication middleware uses AuthO details (YOUR_AUTH0_DOMAIN, ) to check and verify the JWT obtained from the incoming requests.

The above implementation automatically makes all our queries and mutations secure. You can, however, customize this behavior with express-jwt:

        
// auth middleware
const authMiddleware = jwt({
  credentialsRequired: false
})
        
      

The availability of the user object will determine if the user is authenticated or not using the following resolver function.

        
addComment (_, args, context) {
  // make sure user is authenticated
  if (!context.user) {
    throw new Error('You are not authorized!')
  }

  // user is authenticated, continue with adding comment
}
        
      

Here, we are throwing an error if the user is not authenticated.

Authentication with Passport.js Middleware

The same process can be done with Passport.js as our middleware as follows:

        
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 below
const app = express();
const PORT = 4000;
//passport's session piggy-backs on express-session
app.use(session({
 genid: function(req) {
   return uuid.v4();
 },
 secret: ‘fnefh4404?>!$%lojHH’
}));
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 the Auth.js will look like this:

        
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);
  });
});
        
      

Just like in the first approach, the user object will now be available in the GraphQL Resolver.

Remember to use bcrypt with password in the Passport.js example above.

Token-based versus Session-based GraphQL Authentication Best Practices

Token-based methods typically implement JWT tokens and Random tokens whereas session-based methods implement stateful cookies stored server-side or stateless cookies stored in the cookie itself.

A few rules of thumb are:

  1. Use JWT tokens only if you are okay with foregoing revocation and inactivity based timeouts:
    • Recovation is hard to implement with JWT because you need to maintain a separate recovation list in a distributed store which foregoes the benefits of statelessness
    • Inactivity based timeouts are painful to implement because you have to either add refresh tokens to the client which adds unnecessary complexity or maintain a last_seen_flag in a database or cache and use that to expire which, again, foregoes the benefits of statelessness.
  2. Use token-based methods if you do not mind handling storage explicitly—either with sessionStorage or localStorage:
    • Browsers handle cookie storage implicitly without any effort on the part of the programmer.
  3. Use token-based methods if you do not mind passing authorization credentials with every request:
    • Both JWT and Random tokens pass credentials in every request header using Authorization: Bearer <\token>. The programmer has to pass this explicitly in every request.
    • Browsers sent credentials in cookies to the server automatically.
  4. Use token-based authentication if you want to mitigate against Cross-Site Request Forgery (CSRF) Attacks:
    • Session cookies are vulnerable to CSRF attacks. It is upto to the Programmer to mitigate this.
    • Both JWT and random tokens are not vulnerable to CSRF attacks because tokens are passed explicitly as a header request.
  5. Use random tokens if you want to use Native Mobile Apps: Session based authentication is painful to implement in Native Mobile Apps.
    • Mobile users expect to login just once and don't ever login again. Such sessions will be hard to implement with Native mobile apps.
    • Although JWT with a long expiry can work, but a secure random token with no information in it is much more secure.
  6. Use JWT tokens with very short expiry if you want to mitigate Replay attacks: Replay attacks are only possible in JWT tokens if the expiry is high.
    • Session cookies leave you completely exposed to Replay attacks. Random tokens are possible the attacker obtains the API key.
  7. Prefer JWT tokens with public/private key pairs for server-to-server apps. Public/private key pairs are almost always better than a shared secret.