GraphQL HTTP-only JWT Authentication with Next.js

Lately, I've been working on building one of the app challenges on devchallenges.io.

I decided to use Next.js with GraphQL as my stack. I was a little worried about how I would implement secure HTTP-only authentication but it turned out to be super simple! Let me show you how.

Starting off we'll use a basic graphql API route adapted from the next.js example

1import { ApolloServer, gql } from 'apollo-server-micro'
2
3const typeDefs = gql`
4 type Query {
5 me: User
6 }
7
8 type Mutation {
9 signup(username: String!, password: String!): User
10 }
11
12 type User {
13 username: String!
14 }
15`
16
17const resolvers = {
18 Query: {
19 me(_parent, _args, context) {
20 // what do we do here?
21 },
22 },
23
24 Mutation: {
25 signup(_parent, {username, password}, context) {
26 // ??
27 },
28 }
29
30}
31
32const apolloServer = new ApolloServer({ typeDefs, resolvers })
33
34export const config = {
35 api: {
36 bodyParser: false,
37 },
38}
39
40export default apolloServer.createHandler({ path: '/api/graphql' })

Here's where the fun begins.

We'll import jsonwebtoken and cookies (make sure you add them to your dependencies!):

1import jwt from "jsonwebtoken";
2import Cookies from "cookies";

Then we'll add a context within the apollo server where we'll create a cookie jar to set and get cookies within our resolves and parse our JWT token (if we have it).

1const verifyToken = (token) => {
2 if (!token) return null;
3 try {
4 return jwt.verify(token, process.env.SECRET!);
5 } catch {
6 return null;
7 }
8};
9
10
11const apolloServer = new ApolloServer({
12 typeDefs,
13 resolvers,
14 context: ({ req, res }) => {
15 const cookies = new Cookies(req, res);
16 const token = cookies.get("auth-token");
17 const user = verifyToken(token);
18 return {
19 cookies,
20 user,
21 };
22 },
23});

Now in our resolvers, we can set the cookie when a user signs up (and signs in, but I'll let you figure that out):

1const resolvers = {
2 // ...
3 Mutation: {
4 async signup(_parent, {username, password}, context) {
5 let hash = await bcrypt.hash(password, 10);
6 // bring your own db logic
7 let user = await db.createUser({username, password: hash})
8
9
10 let token = jwt.sign({ id: user.id }, process.env.SECRET!);
11 context.cookies.set("auth-token", token, {
12 httpOnly: true,
13 sameSite: "lax",
14 // here we put 6 hours, but you can put whatever you want (the shorter the safer, but also more annoying)
15 maxAge: 6 * 60 * 60,
16 secure: process.env.NODE_ENV === "production",
17 });
18 return user;
19 },
20 }
21}

Now, whenever a request is made to check our auth status, it's easy!

1const resolvers = {
2 Query: {
3 me(_parent, _args, context) {
4 // bring your own db logic
5 context.user?.id ? db.findUser(context.user.id) : null
6 },
7 },
8}

That should be enough to get you started 😄