8

I have an issue with my Graphql server and react front-end.

When submitting a "signin" mutation, the mutation is handled correctly and data received. The "Set-Cookie" is received in the response headers, but its not stored in the browser cookies.
I have tried proposed solutions from myriad of other discussions on Stack Overflow but to no avail.

enter image description here

enter image description here

Here is my Back-End code:

index.js

    const express = require("express");
    const mongoose = require("mongoose");
    const { ApolloServer, AuthenticationError } = require("apollo-server-express");
    const cors = require("cors");
    const cookieParser = require("cookie-parser");
    const jwt = require("jsonwebtoken");
    const resolvers = require("./graphql/resolvers");
    const typeDefs = require("./graphql/typeDefs");
    require("dotenv").config();

    const users = [
      {
        id: 1,
        name: "Test user",
        email: "your@email.com",
        password: "$2b$10$ahs7h0hNH8ffAVg6PwgovO3AVzn1izNFHn.su9gcJnUWUzb2Rcb2W" // = ssseeeecrreeet
      }
    ];

    mongoose
      .connect(process.env.MONGO_URI, {
        useNewUrlParser: true,
        useUnifiedTopology: true
      })
      .then(() => console.log("DB Connected"))
      .catch(err => console.error(err));

    const corsOptions = {
      credentials: true,
      origin: "http://localhost:3000"
    };
    const app = express();
    const port = 4000;
    app.use(cors(corsOptions));
    app.use(cookieParser());
    app.use(express.json());
    app.use(express.urlencoded({ extended: true }));

    const context = async request => {
      let authToken = null;
      let currentUser = null;
      const { headers } = request.req;
      try {
        authToken = headers.authorization || "";
        if (authToken) {
          currentUser = jwt.verify(authToken, process.env.SECRET_KEY);
        }
      } catch (error) {
        throw new AuthenticationError(
          "Authentication token is invalid, please log in"
        );
      }
      return { request, currentUser };
    };

    const server = new ApolloServer({
      typeDefs,
      resolvers,
      context
    });

    server.applyMiddleware({ app, path: "/graphql" });

    app.listen(port, () => console.log(`Server started: http://localhost:${port}`));

resolvers.js

module.exports = {
  Mutation: {
    signin: async (root, args, ctx) => {
      console.log(ctx.currentUser);
      // Make email lowercase
      const email = args.email.toLowerCase();
      // Check if User exists
      const userExist = await User.findOne({ email });
      if (!userExist) {
        throw new Error("User does not exist, please signup for new account");
      }
      // Check if passwords match
      const match = await bcrypt.compare(args.password, userExist.password);
      if (!match) {
        throw new Error("Invalid username or Password");
      }
      // Create a token and assign
      const token = jwt.sign(
        { email: userExist.email, id: userExist._id },
        process.env.SECRET_KEY,
        { expiresIn: "1day" }
      );
      // Assign to cookie
      ctx.request.res.cookie("token", token, {
        httpOnly: true,
        maxAge: 60 * 60 // 1 Hour
        // secure: true, //on HTTPS
        // domain: 'example.com', //set your domain
      });
      return userExist;
    }
  }
};

Then on the Client (React) side:

import React, { useContext, useReducer } from "react";
import ReactDOM from "react-dom";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";

import App from "./App";
import Splash from "./pages/Splash";
import Context from "./context";
import reducer from "./reducer";
import ProtectedRoute from "./ProtectedRoute";

import * as serviceWorker from "./serviceWorker";

import { ApolloProvider } from "react-apollo";
import { ApolloClient } from "apollo-client";
import { createHttpLink } from "apollo-link-http";
import { InMemoryCache } from "apollo-cache-inmemory";

const client = new ApolloClient({
  link: createHttpLink({
    uri: "http://localhost:4000/graphql",
    credentials: "include"
  }),
  cache: new InMemoryCache()
});

const Root = () => {
  const initialState = useContext(Context);
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <Router>
      <ApolloProvider client={client}>
        <Context.Provider value={{ state, dispatch }}>
          <Switch>
            <ProtectedRoute exact path="/" component={App} />
            <Route path="/login" component={Splash} />
          </Switch>
        </Context.Provider>
      </ApolloProvider>
    </Router>
  );
};

ReactDOM.render(<Root />, document.getElementById("root"));

Login.js component

// Imports Omitted

export default function SignIn() {

const onSubmit = async ({ email, password }) => {
    const variables = { email, password };
    const client = new GraphQLClient(BASE_URL);
    const data = await client.request(SIGNIN_MUTATION, variables);
    console.log(data);
  };


// return info omitted
Stephan Du Toit
  • 789
  • 1
  • 7
  • 19

4 Answers4

2

change SameSite:None to SameSite:Lax in your resolver.js

ctx.request.res.cookie("token", token, {
     httpOnly: true,
     maxAge: 60 * 60 // 1 Hour
     // secure: true, //on HTTPS
     // domain: 'example.com', //set your domain
     sameSite: 'lax',
    }

references:

https://web.dev/samesite-cookies-explained/#explicitly-state-cookie-usage-with-the-samesite-attribute

https://github.com/GoogleChromeLabs/samesite-examples/blob/master/javascript-nodejs.md

ogelacinyc
  • 1,272
  • 15
  • 30
1

having header in response header does not mean that you're allowed to use them whatever the api or webserver is, you should put set-cookie in Access-Control-Allow-Headers to let the browser use the given cookie

reza erami
  • 138
  • 6
  • Thanks... uhm how do i go about doing this? Is it done server side or client side? – Stephan Du Toit Jan 02 '20 at 19:25
  • yeah you should do it serverside set Access-Control-Allow-Origin to '*' – reza erami Jan 02 '20 at 19:37
  • const corsOptions = { // origin: "http://localhost:3000", origin: '*', credentials: true } i assume you meant like this. Not working – Stephan Du Toit Jan 02 '20 at 19:53
  • 1
    PS, I seem to only have this issue using graphql. If I use a standard axios request from the client and a simple /login route on the server, it works just fine. I would preferably like to stick to one API technology. – Stephan Du Toit Jan 02 '20 at 19:57
  • if you are calling an api from 'api.foo.com' and in response headers you see the set-cookie it will be set only in 'api.foo.com' domain in the browser but when you set credentials it will let the other page to access it too for example if you are calling that api from another address called 'bar.foo.com', with credentials set to true, you can access it, but, you can access it only if the set-cookie header is allowed in access control allow headers Access-Control-Allow-Headers = * – reza erami Jan 02 '20 at 20:01
1

[EDIT]: The missing path, and the "Access-Control-Allow-Headers" - "Origin, X-Requested-With, Content-Type, Accept" causes problems several times with Chrome (or newer browsers). It can HttpOnly and SameSite=Strict, but don't forget to add the missing attributes.

Further notes: It can caused by security mechanism:

Option A.) - The proxy way

1.) Make sure to use proxy in package.json of the frontend with the same endpoint of the backend.

Like :

"proxy": "http://localhost:8080/api/auth" 

if you run the backend on :8080.

2.) In your service file (or anywhere you would like to call the backend), relative paths will enough, so you don't need to specify the whole path, thanks to the proxied server url.

Like:

...

  return axios
    .post("/signin", {
      username,
      password,
    })

...

For furhter infos of Same-origin-policy: https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy

Option B.) - The CORS way

For Spring users: don't forget to set the @CrossOrigin origins and allowCredentials to true besides the mentioned header settings. like:

@CrossOrigin(origins = {"http://127.0.0.1:8089", "http://localhost:3001"}, allowCredentials = "true")

Hopefully, it will help to sb. Happy hacking!

Mark Denes
  • 141
  • 1
  • 6
0

This configurations working for me.

This configurations for cookie

cookie: {
    path: '/',
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: true,
    maxAge: 60000, //time exp
    domain: 'localhost' //or other domain
  }

Configuration for CORS / nodeJs

configCors = {
  origin: [`http://${process.env.SERVER_NAME}`, 'http://localhost:3000'],
  credentials: true
}

And finnaly, put as "include" apollo credentials I had the same problems, but I solved it with graphql credentials (Apollo)

JhonF
  • 1