DEV Community

loading...

GraphQL Server Not Setting JWT Cookie

doylecodes profile image Ryan Doyle ・3 min read

SOLUTION!

Here is the solution to the problem in this original post

Original Post

Hello smart people! I'm hoping someone could help me out with something I've been struggling with for a while.

I've worked through a WesBos course (Advanced React) and it was great, and it used JWT saved as cookies for authentication. I've been working on my own project and decided to do the same thing. Initially, I was using Apollo Server 2, and I could not figure out why my cookies were not saving so I thought, "Hey I did this with a GraphQL Yoga server, so I'll just do that instead." After trying a few things with the apollo-server-express server).

Long story short, I'm still stuck. I suppose I could ask on StackOverflow but it's not nice over there! Her is what I have going on...

index.js

const { Prisma } = require('prisma-binding');
const express = require('express');
const { ApolloServer } = require('apollo-server-express');
const { GraphQLServer } = require('graphql-yoga');
const { importSchema } = require('graphql-import');
const cookieParser = require('cookie-parser');
const jwt = require('jsonwebtoken');
require('dotenv').config();

const typeDefs = importSchema('./src/schema.graphql');
const Query = require('./src/resolvers/Query');
const Mutation = require('./src/resolvers/Mutation');

const db = new Prisma({
  typeDefs: './generated/prisma.graphql',
  endpoint: process.env.DB_ENDPOINT,
  secret: process.env.DB_SECRET
});

const server = new GraphQLServer({
  typeDefs,
  resolvers: {
    Mutation,
    Query
  },
  context: req => ({ ...req, db })
});

server.express.use(cookieParser());
server.express.use((req, res, next) => {
  const { token } = req.cookies;
  if (token) {
    const { userId } = jwt.verify(token, process.env.APP_SECRET);
    // add the user to future requests
    req.userId = userId;
  }
  next();
});

server.start({ //start the GraphQL server
  cors: { // only allow from frontend server (frontend_url)
    credentials: true,
    port: 4000,
    origin: ['http://localhost:3000'],
  },
}, postStart => { //callback once connection is created
  console.log(`🚀 Server now running on http://localhost:${postStart.port}`);
});
Enter fullscreen mode Exit fullscreen mode

My Mutation for logging in

async signin(parent, { email, password }, ctx, info) {
    const user = await ctx.db.query.user({ where: { email: email } });
    if (!user) {
      throw new Error(`No such user found for the email ${email}`);
    }
    const valid = await bcrypt.compare(password, user.password);
    if (!valid) {
      throw new Error(`password is not valid!`);
    }
    const token = jwt.sign({ userId: user.id }, process.env.USER_SECRET);

    ctx.response.cookie('token', token, {
      httpOnly: true,
      maxAge: 1000 * 60 * 60 * 24 * 31,
    });
    console.log(ctx.response);
    return user;
  },
Enter fullscreen mode Exit fullscreen mode

You can see right now that I have a console.log set up in my mutation to log the response. I can see that there is an actual response that states:

    body:
      { operationName: 'LOGIN_MUTATION',
        variables: [Object],
        query:
         'mutation LOGIN_MUTATION($email: String!, $password: String!) {\n  signin(email: $email, password: $password) {\n    id\n    name\n    email\n    __typename\n  }\n}\n' },
     _body: true,
     length: undefined },
  locals: [Object: null prototype] {},

  [Symbol(outHeadersKey)]:
   [Object: null prototype] {
     'x-powered-by': [ 'X-Powered-By', 'Express' ],
     'access-control-allow-origin': [ 'Access-Control-Allow-Origin', 'http://localhost:3000' ],
     vary: [ 'Vary', 'Origin' ],
     'access-control-allow-credentials': [ 'Access-Control-Allow-Credentials', 'true' ],
     'set-cookie':
      [ 'Set-Cookie',   'token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjanRreG5kZG9henlpMGIwMG8yamwwZzg1IiwiaWF0IjoxNTU0MDc0MDU1fQ.h617eZ1LV3yUqn01jpMaTZDTUlYQmRMxwZc1VKbSHns; Max-Age=2678400; Path=/; Expires=Wed, 01 May 2019 23:14:15 GMT; HttpOnly' ] } }
Enter fullscreen mode Exit fullscreen mode

So I see at the end this Set-Cookie, but no matter what I've tried, no cookie get saved :(

I even tried just replicating what I did in the course (although packages are slightly different) to have no luck.

So, smart people, what am I missing? I feel like I want to write a post on this after I learn what's up...

Update:

  • I know my env variables work, I have other mutations in the app that work fine. I also get other errors if I remove them from my .env file.
  • My thought is it’s something CORS related because of the server and client on different localhosts.

GitHub logo ryanmdoyle / thermoFisherSci

Part Inventory description tracker/editor/exporter. Allows input and exiting of part descriptions in multiple languages to export as HTML to be used in web applications.

ThermoFisher Scientific Part Descriptions

Part Inventory description tracker/editor/exporter. Allows input and exiting of part descriptions in multiple languages to export as HTML to be used in web applications.

You can start the application by:

  1. npm install from both frontend and backend folders.
  2. You'll need to create a Prisma account and deploy to prisma from backend.
  3. Create a .env file in backend with:
  • DB_ENDPOINT (set to your prisma endpoint)
  • DB_SECRET (make a random secret for prisma to use)
  • USER_SECRET (a random secret for the app to use)
  • COOKIE_SECRET (something random for cookie generation)
  • FRONTEND_URL=http:localhost:3000
  • APP_SECRET (something random for the app to use)
  1. Make .env in frontend
  • NODE_ENV=development
  1. npm run dev from both frontend (localhost: 3000) and backend (localhost:4000)
  2. See app from locahost:3000

Discussion (4)

pic
Editor guide
Collapse
wodcz profile image
Martin Janeček • Edited

Hi!

The credentials: true should be on the client-side, not on server-side.

Edit: it should be included on both front-end (credentials: 'include') and back-end (credentials: true) - apollographql.com/docs/react/recip...

Collapse
fcpauldiaz profile image
Pablo Díaz Márquez

I have the same problem ? Did you solved it ?

Collapse
doylecodes profile image
Ryan Doyle Author

I did! Basically, it all came down to needing to pass in various options that were CORS related to both my frontend and backend. My main issue was that I was using apollo-server-express with cors middleware, but didn;'t realize that apollo-server-express had some cors built in and both were conflicting. I ended up doing:

server.applyMiddleware({
  app,
  path: '/',
  cors: false, // disables the apollo-server-express cors to allow the cors middleware use
})

Then my cors middleware worked and the tokens/headers were all getting passed correctly.

You can see my codebase here

ryanmdoyle / thermoFisherSci

Part Inventory description tracker/editor/exporter. Allows input and exiting of part descriptions in multiple languages to export as HTML to be used in web applications.

ThermoFisher Scientific Part Descriptions

Part Inventory description tracker/editor/exporter. Allows input and exiting of part descriptions in multiple languages to export as HTML to be used in web applications.

You can start the application by:

  1. npm install from both frontend and backend folders.
  2. You'll need to create a Prisma account and deploy to prisma from backend.
  3. Create a .env file in backend with:
  • DB_ENDPOINT (set to your prisma endpoint)
  • DB_SECRET (make a random secret for prisma to use)
  • USER_SECRET (a random secret for the app to use)
  • COOKIE_SECRET (something random for cookie generation)
  • FRONTEND_URL=http:localhost:3000
  • APP_SECRET (something random for the app to use)
  1. Make .env in frontend
  • NODE_ENV=development
  1. npm run dev from both frontend (localhost: 3000) and backend (localhost:4000)
  2. See app from locahost:3000

.
Collapse
fcpauldiaz profile image
Pablo Díaz Márquez

Thank you Ryan :)