Setting Up Basic Authentication and Sessions on a GraphQL Server using Passport.js & Express Sessions.

A quick walkthrough on basic user authentication and persistence using Express sessions, Passport.js and GraphQL.

Richie Pineda
7 min readAug 30, 2019

On a recent project, I decided to make the jump from the familiar world of REST APIs to running a GraphQL powered back-end, and (for the most part) it has changed everything about how I will develop my applications moving forward. With that bit of brown-nosing out of the way, there were a few heart aches and “clumps-of-hair-violently-removed-from-my-scalp” moments when the transition from REST to GraphQL wasn’t so obvious or 1:1. In my case specifically, the stress and mania came about when I found it extremely difficult to integrate my beloved Express sessions and Passport.js authentication middlewares into my GraphQL server. Since virtually all of my requests would be serviced by a single endpoint, the use of routes as I knew them for API requests where no longer required, but I still needed to figure out how to integrate a cohesive authentication strategy. Maybe I overthought it, maybe you’re a bit smarter and pieced it together on the first try — but if you’re on the same boat I was then read on because we can get through this, because it really is that simple!

Copying a simplified bit of code from my project (basically a “favorite movies” app where users can login to browse movies and favorite them) for demonstrative purposes, lets first take a look at what the server looks like:

app
┣ src
┣ server
┃ ┣ database
┃ ┃ ┣ db.js
┃ ┃ ┣ Users.js
┃ ┃ ┣ Movies.js
┃ ┃ ┣ Favorites.js
┃ ┃ ┣ index.js
┃ ┣ gql
┃ ┃ ┣ schema.js
┃ ┣ auth
┃ ┃ ┣ google.js
┃ ┣ main.js

The most important files we will need to dive into are the gql/schema.js, auth/google.js and main.js in order to piece together some functionality for our back-end. As you’ve probably noticed, the schema.js file has essentially replaced my routes. With hindsight being 20/20, this is what ended up tripping me up while also being the simplest part… but we’ll get to that soon.

Firstly looking at main.js, we should see several familiarities:

const express = require('express');
const session = require('express-session');
const { User, db } = require('./database/index');
const SequelizeStore = require('connect-session-sequelize')(session.Store);
const PORT = 8080;
const sessionStore = new SequelizeStore({ db });
const graphqlHTTP = require('express-graphql');
const schema = require('./gql/schema');
const { googleAuth } = require('./auth/google');
const passport = require('passport');

const app = express();

app.use(
session({
secret: 'The most robust of secrets...',
store: sessionStore,
resave: false,
saveUninitialized: false
})
);

app.use(passport.initialize());
app.use(passport.session());

passport.serializeUser((user, done) => done(null, user.id));
passport.deserializeUser(async (id, done) => {
try {
const user = await User.findByPk(id);
done(null, user);
} catch (err) {
done(err);
}
});
app.use('/googleAuth', googleAuth)
app.use('/gqlapi', graphqlHTTP({ schema, graphiql: true }));

db.sync().then(() => {
console.log('db synced!');
app.listen(PORT, () =>
console.log(`Serving up the scenes on port ${PORT}!`)
);
});

Akin to its REST counterpart, all of the hits and classics are present in this GraphQL iteration of a server: Express session, sessionStore, passport methods, etc. However, as is obvious, the biggest difference is the single API endpoint:

app.use('/gqlapi', graphqlHTTP({ schema, graphiql: true }));

This initialization of graphqlHTTP is the foundation of how this will all come together. Specifically, the magic lies in the omitted context argument within graphqlHTTP — without an explicit context argument provided, the context for our graphqlHTTP server is defaulted to the request object. This will be instrumental in how we execute our validation logic later, especially for non-OAuth login so remember that!

Turning our attention to the passport middleware defined in google.js, things should still look rather familiar:

const passport = require('passport');
const googleAuth = require('express').Router();
const GoogleStrategy = require('passport-google-oauth').OAuth2Strategy;
const { User } = require('../database/index');
module.exports = googleAuth;

if (!process.env.GOOGLE_CLIENT_ID || !process.env.GOOGLE_CLIENT_SECRET) {
console.log(
'No Google Client ID or Google Client Secret found. Skipping Google OAuth.'
);
} else {
const googleConfig = {
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: process.env.GOOGLE_CALLBACK
};

const strategy = new GoogleStrategy(
googleConfig,
(token, refreshToken, profile, done) => {
const googleId = profile.id;
const email = profile.emails[0].value;

User.findOrCreate({ where: { googleId }, defaults: { email } })
.then(([user]) => done(null, user))
.catch(done);
}
);

passport.use(strategy);

googleAuth.get('/', passport.authenticate('google', { scope: 'email' }));
googleAuth.get(
'/callback',
passport.authenticate('google', {
successRedirect: '/home',
failureRedirect: '/login'
})
);
}

Here I did employ an Express Router to have this be a self-sustained and modular component for handling everything Google OAuth related itself, including the redirects. You can replace this with any other Passport strategy of your choosing. This code simply exists to handle any login request opting to use Google to login, and the rest is handled in the main.js.

Let’s take a peek at what this schema.js file looks like, which will handle validating and our “native” user login and functionality for the API endpoint of the entire app:

const { Movie, User, Favorite } = require('../database/index');
const {
GraphQLObjectType,
GraphQLString,
GraphQLList,
GraphQLID,
GraphQLSchema,
GraphQLBoolean
} = require('graphql');

// Type Defs
const MovieType = new GraphQLObjectType({
name: 'Scene',
fields: () => ({
id: { type: GraphQLID },
title: { type: GraphQLString },
imdbLink: { type: GraphQLString },
})
});

const UserType = new GraphQLObjectType({
name: 'User',
fields: () => ({
id: { type: GraphQLID },
email: { type: GraphQLString },
password: { type: GraphQLString },
favorites: {
type: new GraphQLList(FavoriteType),
async resolve(parent, args) {
const userFavorites = await Favorite.findAll({
where: {
userId: parent.id
}
});
return userFavorites;
}
}
})
});

const FavoriteType = new GraphQLObjectType({
name: 'Favorite',
fields: () => ({
id: { type: GraphQLID },
userId: { type: GraphQLID },
movieId: { type: GraphQLID },
// Resolver to get movie associated with favorite
movie: {
type: MovieType,
async resolve(parent, args) {
return await Movie.findByPk(parent.movieId);
}
}
})
});

// Root query def
const RootQuery = new GraphQLObjectType({ ... })

// Mutations def
const Mutation = new GraphQLObjectType({
name: 'Mutation',
fields: {
loginUser: {
type: GraphQLBoolean,
args: {
email: { type: new GraphQLNonNull(GraphQLString) },
password: { type: new GraphQLNonNull(GraphQLString) }
},
async resolve(parent, args, request) {
const user = await User.findOne({ where: { email: args.email } });
if (!user) {
throw new Error(
`Could not find account associated with email: ${args.email}`
);
} else if (!user.correctPassword(args.password)) {
throw new Error(
`Incorrect password for account associated with: ${args.email}`
);
} else {
request.login(user, error => (error ? error : user));
return true;
}
}
},
addFavorite: {
type: FavoriteType,
args: {
sceneId: { type: new GraphQLNonNull(GraphQLID) }
},
async resolve(parent, args, request) {
if (!request.user) throw new Error('Only users can create favorites.');
const newFavorite = await Favorite.create({
sceneId: args.sceneId,
userId: request.user.id
});
return newFavorite;
}
}
});

module.exports = new GraphQLSchema({
query: RootQuery,
mutation: Mutation
});

All I have done here is define my schema types, queries, and mutations that handle the business logic behind my GraphqlHTTP server in main.js. Notice the loginUser mutation — here I simply provide the logic to handle communications to the database, and if all goes well, use passport’s “.login()” to pass it onto the passport middleware and attach it to the request object . It is worth noting we never imported passport functionality into our schema or defined “.login()” anywhere because we know this will be executed within main.js which DOES have access to passport, so that is where the magic will happen.

Speaking of the request object, this is finally where it comes into play! First we used it in our loginUser mutation to attach our user to it, meaning now all of our GraphQL resolvers have access to not just the request object, but to request.user as well since we have opted to provide the request object as our context!

Finally, we will get our first taste of user validation in the addFavorite mutation. I only want logged in users to be able to have favorites, and this is enforced by the line:

async resolve(parent, args, request) {
if (!request.user) throw new Error('Only users can create favorites.');

As we saw earlier, successful logins and the use of our session middleware for persistence will always (for as long as the session storage policy allows) provide the request object with the user property, and as such anything I want authorized or validated that is user related I can simply check the request.user object right from inside my resolver. Here is a simple implementation: if a non-logged in user tries to add a favorite, the resolver simply checks if there is a user associated with the request and if it doesn’t find one sends back an error. Otherwise, execute the logic behind adding the favorite for the user.

And that’s basically it!!! If you’re thinking to yourself, that was super easy and basically just like setting it up with a REST server, that’s because it really is! It is important to realize, at least in this particular case, that the resolvers have essentially taken the place of your routes, and as such all of your authentication logic simply just gets put right in the resolver just as you would with a route! The simplicity of that notion is what eluded me for so long, but hopefully you’ll not run into overthinking it like I did and breeze through the rest of your project.

Thanks for reading, happy coding!

--

--