Simple Auth in GraphQL

How to add an Authentication layer for your GraphQL API

Marco Pegoraro
Jul 7, 2019 · 13 min read
Simple Auth in GraphQL and ForrestJS

At the end of this article, you are able to authenticate GraphQL requests and protect your sensible queries.

query {
getGeneralInfo { temperature wind }
...

auth {
getPersonalInfo { name surname }
...
}
}

PART 1 — The Codebase

We are going to build over the codebase from the GraphQL Made Easy tutorial. You can download it from here.

mkdir ./grahpql-auth

# Feature's Entry Point
touch ./grahpql-auth/index.js

# Feature's Hooks Manifest
touch ./grahpql-auth/hooks.js
const { FEATURE } = require('@forrestjs/hooks')
exports.FEATURE_NAME = `${FEATURE} graphql-auth`
const hooks = require('./hooks')

const extendsGraphQLSchema = () => {
console.log('we will extend the GraphQL schema')
}

module.exports = ({ registerHook, registerAction }) => {
// Add any custom hooks into the App's context so that other features ca
// refer to them as "$HOOK_NAME" (we will use this later on)
registerHook(hooks)

// Register an action to the GraphQL service's hook
registerAction({
hook: '$EXPRESS_GRAPHQL',
name: hooks.FEATURE_NAME,
handler: extendsGraphQLSchema,
})
}
...
runHookApp([
...
require('./graphql-auth'),
])

🔔 bonus — How to add a Boot Trace

As we are working with the main entry point I suggest we register a small Hooks utility that will help to visualize what is going on in our app at boot time:

...
runHookApp({
trace: true,
settings: ({ setConfig }) => {
...

GraphQL Wrapper, what is it?

We work now in ./graphql-auth/index.js and play around with extendsGraphQLSchema() so to create the auth wrapper as we have imagined it before:

const { GraphQLObjectType, GraphQLString } = require('graphql')
const hooks = require('./hooks')

// Demo sub-query that provides some confidential informations:
const getPersonalInfo = {
type: new GraphQLObjectType({
name: 'PersonalInfo',
fields: {
name: { type: GraphQLString },
surname: { type: GraphQLString },
},
}),
resolve: () => ({
name: 'Marco',
surname: 'Pegoraro',
})
}

// GraphQL shape of the `auth` wrapper, here we can add the
// protected sub-queries:
const AuthQuery = new GraphQLObjectType({
name: 'AuthQuery',
fields: { getPersonalInfo },
})

// A function that somehow defines whether the `auth` wrapper should
// be accessible or not. "null" means "no", "true" means "yes".
//
// --> This is the critical piece of logic! <--
const canAccessAuth = () => true

const extendsGraphQLSchema = ({ registerQuery }) => {
registerQuery('auth', {
type: AuthQuery,
resolve: canAccessAuth,
})
}

module.exports = ({ registerHook, registerAction }) => {
registerHook(hooks)
registerAction({
hook: '$EXPRESS_GRAPHQL',
name: hooks.FEATURE_NAME,
handler: extendsGraphQLSchema,
})
}
query {
auth {
getPersonalInfo { name }
}
}
{
"data": {
"auth": {
"getPersonalInfo": {
"name": "Marco"
}
}
}
}
const canAccessAuth = () => null
{
"data": {
"auth": null
}
}

✅ Strengths:

  • queries nested inside the auth wrapper doesn't need to implement any protection logic
  • canAccessAuth works like an Express middleware which is a widely adopted concept
  • it is possible to deep-nest more wrappers that implement fine-grained data-access policies

🔸Limitations:

  • only the access to auth is regulated, a more fine-grained regulation must be implemented query by query
  • you might not like the shape of the API, but this is personal and I hope you like it anyway 😇

PART II — Make it Reusable

From now on, we will work on refining the code above and implement two important responsibilities:

  1. Implement the canAccessAuth with some real (but still simple) protection mechanism

Create an Extensible Feature

Code reusability is a big deal of a problem, and ForrestJS’ Hooks aim to improve the chances to produce some really reusable features. In order to make graphql-auth truly reusable we are going to:

  • create some extension points (hooks)

Step n.1 — Isolate “extendsGraphQLSchema”

Now we can really focus on the extensibility task. The first step is to create a new hook definition in ./graphql-auth/hooks.js so that other features can import it:

const { FEATURE } = require('@forrestjs/hooks')
exports.FEATURE_NAME = `${FEATURE} graphql-auth`
exports.GRAPHQL_AUTH = `${exports.FEATURE_NAME}/queries`
vi ./graphql-auth/graphql-schema.js
const { GraphQLObjectType, GraphQLID } = require('graphql')
const { GRAPHQL_AUTH } = require('./hooks')

// We will work on this very soon
const canAccessAuth = () => true

exports.extendsGraphQLSchema = ({ registerQuery, registerMutation }, { createHook }) => {
// Collect queries and mutations that needs session validation
const queries = {}
const mutations = {}
const args = {
token: {
type: GraphQLID,
}
}

// Let other features integrate their own queries and mutations.
// It is always a good idea to provide getters/setters to the extensions
// so to retain full control over the internal data structure:
createHook(GRAPHQL_AUTH, {
args: {
registerQuery: (name, def) => queries[name] = def,
registerMutation: (name, def) => mutations[name] = def,
setArg: (key, val) => args[key] = val,
},
})

// Extends the app's queries with the "auth" wrapper
// (only if at least one query was registered)
Object.keys(queries).length && registerQuery('auth', {
args,
type: new GraphQLObjectType({
name: 'AuthQueryWrapper',
fields: queries,
}),
resolve: canAccessAuth,
})

// Extends the app's mutations with the "auth" wrapper
// (only if at least one mutation was registered)
Object.keys(mutations).length && registerMutation('auth', {
args,
type: new GraphQLObjectType({
name: 'AuthMutationWrapper',
fields: mutations,
}),
resolve: canAccessAuth,
})
}

Step n.2 — Refactor the Entry Point

Now that we have the graphql-schema module, the entry point becomes dramatically simple:

const hooks = require('./hooks')
const { extendsGraphQLSchema } = require('./graphql-schema')

module.exports = ({ registerHook, registerAction }) => {
registerHook(hooks)
registerAction({
hook: '$EXPRESS_GRAPHQL',
name: hooks.FEATURE_NAME,
handler: extendsGraphQLSchema,
})
}

Step n.3 — Register the “getPersonalInfo” query

I’m sure you will like this step.

vi ./get-personal-info.query.js
const { GraphQLObjectType, GraphQLString } = require('graphql')

// Fake resolver, this should connect to some dbms and fetch
// very sensible data!
const resolve = () => ({
name: 'Marco',
surname: 'Pegoraro',
})

// Shape the GraphQL Type for our sensible data:
const GraphQLPesonalInfo = new GraphQLObjectType({
name: 'PersonalInfo',
fields: {
name: { type: GraphQLString },
surname: { type: GraphQLString },
},
})

// Register the sensible query into the "auth" hook:
module.exports = [
'$GRAPHQL_AUTH',
({ registerQuery }) => registerQuery('getPersonalInfo', {
type: GraphQLPesonalInfo,
resolve,
}),
]
...
runHookApp([
...
require('./graphql-auth'),
require('./get-personal-info.query'),
])

Implement the Auth Business Logic

It’s finally time to play around the canAccessAuth() function in ./graphql-auth/graphql-schema.js, and refactor it into a standard GraphQL resolver:

const canAccessAuth = (_, args, { req }) =>
req.authValidateToken(args) ? true : null
vi ./graph-auth/auth-middleware.js
// This is just the scaffolding of a ForrestJS Hook's handler
exports.authMiddleware = () => {}
const hooks = require('./hooks')
const { extendsGraphQLSchema } = require('./graphql-schema')
const { authMiddleware } = require('./auth-middleware')

module.exports = ({ registerHook, registerAction }) => {
registerHook(hooks)
registerAction({
hook: '$EXPRESS_GRAPHQL',
name: hooks.FEATURE_NAME,
handler: extendsGraphQLSchema,
})
registerAction({
hook: '$EXPRESS_GRAPHQL_MIDDLEWARE',
name: hooks.FEATURE_NAME,
handler: authMiddleware,
})
}
exports.authMiddleware = ({ registerMiddleware }) =>
registerMiddleware((req, res, next) => {
req.authValidateToken = args => args.token === 'xxx'
next()
})
query GoodQuery {
auth (token: "xxx") {
getPersonalInfo { name }
}
}

query BadQuery {
auth {
getPersonalInfo { name }
}
}
  • Wouldn’t be cool to let a custom extension completely change the authorization logic?

Make it a Service

In ForrestJS’s land, there are features and services. They look like the same and you write them in the exact same way. So you already know how to build services.

  • make it generic and extensible to the point that we are happy to call it a service

An Extensible Middleware

First thing let’s add a new hook name to the service’s manifest ./graphql-auth/hooks.js:

...
exports.GRAPHQL_VALIDATE = `${exports.FEATURE_NAME}/validate`
const hooks = require('./hooks')

exports.authMiddleware = ({ registerMiddleware }, { getConfig, createHook }) => {
// 1. Enforce settings and fail at boot time
const validToken = getConfig('authToken', process.env.GRAPHQL_AUTH_TOKEN)

// 2. Let other extensions mess up with the Business Logic
let validateRequest = null
let validateToken = null

createHook.sync(hooks.GRAPHQL_VALIDATE, {
setValidateRequest: fn => validateRequest = fn,
setValidateToken: fn => validateToken = fn,
})

// 3. Provide a default Business Logic
validateRequest = validateRequest || (req => {
try {
req.authToken = req.headers.authorization.substr(7)
req.authTokenIsValid = req.authToken === validToken
} catch (err) {
req.authToken = null
req.authTokenIsValid = false
}
})

validateToken = validateToken || ((req, { token }) => {
if (token) {
req.authToken = token
req.authTokenIsValid = token === validToken
}
return req.authTokenIsValid
})

// 4. Create the middleware
registerMiddleware((req, res, next) => {
validateRequest(req)
req.authValidateToken = args => validateToken(req, args)
next()
})
}

Configuration Made Easy

App’s Configuration

The easiest approach is to provide a static piece of configuration in ./index.js:

...
runHookApp({
trace: true,
settings: ({ setConfig }) => {
setConfig('expressGraphql.mountPoint', '/graphql')
setConfig('authToken', 'xxx')
},
...

Runtime ENV Variable

We can provide the value at boot-time:

GRAPHQL_AUTH_TOKEN=xxx yarn start

ENV File to Rescue

A common practice is to use .env files to list boot-time environment variables. It really makes things easy for us.

vi .env
GRAPHQL_AUTH_TOKEN=xxx
ANOTHER_VAR=yyy
YET_ANOTHER="hoho"
process.env.GRAPHQL_AUTH_TOKEN
yarn add @forrestjs/service-env
const { runHookApp } = require('@forrestjs/hooks')

runHookApp({
trace: true,
settings: ({ setConfig }) => {
setConfig('expressGraphql.mountPoint', '/graphql')
},
services: [
require('@forrestjs/service-env'),
require('@forrestjs/service-express'),
require('@forrestjs/service-express-graphql'),
require('./graphql-auth'),
],
features: [
require('./home.route'),
require('./welcome.query'),
require('./get-personal-info.query'),
]
})

Takeaways

This was a long tutorial, I hope you survived to it! Anyway, the relevant takeaways that you may want to explore in more details are:

  • how to delegate Business Logic outside the GraphQL layer
  • how to build extensible features/services by offering hooks

Download

If you experienced any trouble following the steps above, download this tutorial codebase here.

Challenge

Can you create an extension to this feature that allows to log-in and persists the “logged in” status in a cookie?

The Startup

Get smarter at building your thing. Join The Startup’s +788K followers.

Sign up for Top 10 Stories

By The Startup

Get smarter at building your thing. Subscribe to receive The Startup's top 10 most read stories — delivered straight into your inbox, once a week. Take a look.

By signing up, you will create a Medium account if you don’t already have one. Review our Privacy Policy for more information about our privacy practices.

Check your inbox
Medium sent you an email at to complete your subscription.

Marco Pegoraro

Written by

Life, boats, mountains, planes, web enthusiast

The Startup

Get smarter at building your thing. Follow to join The Startup’s +8 million monthly readers & +788K followers.

Marco Pegoraro

Written by

Life, boats, mountains, planes, web enthusiast

The Startup

Get smarter at building your thing. Follow to join The Startup’s +8 million monthly readers & +788K followers.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store