The standard approach when using Passport.js is to have separate endpoints on your server for signup and login. The front-end sends data from the corresponding forms to these dedicated endpoints. After successful authentication, the user is redirected to some URL.
This doesn't feel very GraphQL like. You would rather want to have one signup and one login mutation that handle the complete authentication flow from sending the credentials to the server, authenticating the user and updating the client-side cache afterward. This is what we're going to do here.
The code in this article is based on a GraphQL API server we implemented in a previous introductory post. You don't need to read that to follow this article. But if you have trouble understanding or want the bigger picture you can have a look there.
As a starting point, the server already supports a query to fetch the logged in user's data and a mutation to log out. It's also set up to use Passport and express-session. You can find the code on GitHub and add the code snippets in this article as you follow along.
Adding the GraphQL local strategy for Passport
Passport usually requires separate endpoints for authentication. Thus handling signup and login from within GraphQL resolvers is not supported out of the box. For this reason, I created a small npm module called graphql-passport. This allows us to access Passport's functionality from the GraphQL context and provides us with a strategy to authenticate users with their credentials against a local database. Let's add it to our dependencies.
npm install graphql-passport
First, we need to initialize Passport with this strategy in api/index.js
.
import express from 'express';
import passport from 'passport';
import { GraphQLLocalStrategy } from 'graphql-passport';
import User from './User';
passport.use(
new GraphQLLocalStrategy((email, password, done) => {
const users = User.getUsers();
const matchingUser = users.find(user => email === user.email && password === user.password);
const error = matchingUser ? null : new Error('no matching user');
done(error, matchingUser);
}),
);
const app = express();
app.use(passport.initialize());
...
We don't use a real database here to allow us to focus on user authentication. We, therefore, use a mock database model to find a match for the provided email and password. If we find a match we pass the user to the done
callback. Otherwise, we create an error and pass it to done
.
Preparing the GraphQL context
Before we go on and implement the mutations we need to prepare the GraphQL context to make certain Passport functions accessible from the resolvers. We can use graphql-passport's buildContext
function for this purpose. The initialization of the Apollo server inside api/index.js
now looks like this.
import { GraphQLLocalStrategy, buildContext } from 'graphql-passport';
...
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req, res }) => buildContext({ req, res }),
});
...
buildContext
basically copies a couple of fields related to Passport like its authenticate
and login
functions from the request into the context and makes them usable from the resolvers. If you're interested you can find the source code here.
Implementing the login mutation
First, let's add the login mutation to the GraphQL type definitions. We require a user's email
and password
as input variables and return the matching user object on success. Add the following lines in api/typeDefs.js
.
const typeDefs = gql`
...
type AuthPayload {
user: User
}
type Mutation {
login(email: String!, password: String!): AuthPayload
...
}
`;
Now we need to implement the corresponding resolver. Open api/resolvers.js
and add the following lines.
const resolvers = {
...
Mutation: {
login: async (parent, { email, password }, context) => {
const { user } = await context.authenticate('graphql-local', { email, password });
context.login(user);
return { user }
},
...
}
};
export default resolvers;
First, we call the authenticate
function on the context. We pass it the name of the strategy we use (graphql-local) and the credentials which we can read from the mutation variables. In order to create a persistent user session Passport requires us to call the login
function after authenticating.
Now we can run npm start
to start the GraphQL server. Open your browser and point it to localhost:4000/graphql. When you run the login mutation below it should return the corresponding user object as defined in User.js
.
mutation {
login(email: "maurice@moss.com", password: "abcdefg") {
user {
id
firstName
lastName
email
}
}
}
Implementing the signup mutation
Finally, let's implement the signup mutation as well. We again start with the type definitions. This is fairly similar to the login mutation. We only expect the first and last name as additional variables.
const typeDefs = gql`
...
type Mutation {
signup(firstName: String!, lastName: String!, email: String!, password: String!): AuthPayload
...
}
`;
Since we need to add the new user to the list of existing ones we will need to access the User model from the resolvers. Therefore we need to add it to the GraphQL context. We can achieve this by passing the User model to buildContext inside api/index.js
.
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req, res }) => buildContext({ req, res, User }),
});
buildContext
will add all additional fields you pass to it to the context.
Now we can head to resolvers.js
. First, we need to check if a user with the provided email already exists. If not we add the user. Otherwise, we throw an error. We also want the user to be logged in directly after signing up. In order to create a persistent session and set the corresponding cookie, we need to call the login
function here as well.
import uuid from 'uuid/v4';
const resolvers = {
...
Mutation: {
signup: (parent, { firstName, lastName, email, password }, context) => {
const existingUsers = context.User.getUsers();
const userWithEmailAlreadyExists = !!existingUsers.find(user => user.email === email);
if (userWithEmailAlreadyExists) {
throw new Error('User with email already exists');
}
const newUser = {
id: uuid(),
firstName,
lastName,
email,
password,
};
context.User.addUser(newUser);
context.login(newUser);
return { user: newUser };
},
...
},
};
export default resolvers;
When you run the server with npm start
and open your browser at localhost:4000/graphql you should be able to execute the signup mutation below.
mutation {
signup(
firstName: "Jen",
lastName: "Barber",
email: "jen@barber.com",
password: "qwerty"
) {
user {
id
firstName
lastName
email
}
}
}
You can run the login mutation with the email and password you used in the signup mutation and see the newly created user as well. Be aware though that the new user will be gone once you restart the server (or in fact save a file in the api
directory since we use nodemon).
The currentUser
query below should return the same data when you send it after having logged in or signed up.
{
currentUser {
id
firstName
lastName
email
}
}
You can find the complete code here on GitHub.
I hope this article was helpful to you. If you liked it I'd be happy to see you on my newsletter. If you're interested in more ways to authenticate with GraphQL and Passport.js check out this post to learn how to implement a social login using Facebook.
Top comments (8)
First of all thanks for your post.
I think that I am having this issue: github.com/apollographql/apollo-se...
I am trying to run a User.findOne({ email }) inside the newGrapQLLocalStrategy, but for some reason, the User model is empty. Even if i try to import it in the scope.
passport.use(
new GraphQLLocalStrategy((email, password, next) => {
console.log(
🎫 ${JSON.stringify(User)} 🚔 👮♂
)User.findOne({ email })
.then(user => !user
? next(null, false, 'Invalid email or password')
: user.checkPassword(password) //bcrypt
.then(match => !match
? next(null, false, 'Invalid email or password')
: next(null, user)
)
)
.catch(error => next(error))
}),
);
Do I need to apply for the middleware passport.authenticate as it said in the Github issue?? how??
nevermind... it works, was an error with... oh, not I don't know what was it... I mean, it works in the playground, but for the console: console.log(🎫 ${JSON.stringify(User)} 🚔 👮♂)
returned:
🎫 undefined 🚔 👮♂
was a ghost? 👻
Thanks for your comments Josue! Not sure what the error was. I cannot really understand it from the code that you shared. It's indeed strange that the console.log says User is undefined but the next line User.findOne works. If you still have a problem would you mind sharing the code in a codesandbox? Would be much easier to debug ;-)
Oh, thanks for the offer... in fact for the same reason, the register doesn´t work ( i will be working on that the next 3 hours).
You can check the public repo here:
github
and the passport config
I am getting confused with the async function .save() from mongoose and the async login/logout with passport
you can check it in the user resolver
So far is working...
I have an other questions...
¿How do you know that the user is logged?. Can I check that in a resolver? where is that? in the context?.
With passport session, we had a collection for all those sessions and had access with something like this:
a simpel middleware.
Can I check for my user if is authenticated??
something like this:
context.req.isAuthenticated()
Yeap, it works, but it restart after changing anything (nodemon).
Btw, checking the context.req object we have something:
Can I store this in a database, like passport use to do with
passport.session()
Thanks a lot for the comment. Passport doesn't encrypt the password, it only provides a standardized way of getting a user according to a given set of credentials. If you only want to support password-based login for your users with GraphQL you could achieve the same functionality inside the resolvers without Passport. In your case you could do the following:
In general, I would advise against using JWT for session management. This is why we use express-session in this tutorial which saves a session ID to the cookie instead. We didn't really cover it in this post but you can find more details here. Passport integrates really well with express-session.
Another big advantage of Passport is that it supports a lot of other ways to authenticate. You can plug in more "Strategies" and easily implement login via Facebook, Twitter, GitHub, Auth0 and many more.
Hellow bro this library is amazing just a little question, about cors, in client side its necesary added credentials : 'include' to support session?
Thanks a lot! First of all, it depends on whether or not you're using cookies to store the session id.
express-session
as we use it in the article works with cookies. In that case, you need the credentials header.Depending on whether or not your frontend and backend are running on the same domain you might also use
same-origin
instead. Have a look here for all options.Hi Kettmann,
Could you plz provide an example with passport jwt implementation to graph ql. I am trying to implement it.
Thanks.