Stephan Du Toit
Stephan Du Toit

Reputation: 849

Set-Cookie in response from server but not stored in storage cookie

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: "[email protected]",
        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

Upvotes: 8

Views: 10094

Answers (4)

Mark Denes
Mark Denes

Reputation: 211

[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!

Upvotes: 1

JhonF
JhonF

Reputation: 1

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)

Upvotes: 0

ogelacinyc
ogelacinyc

Reputation: 1372

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

Upvotes: 2

reza erami
reza erami

Reputation: 158

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

Upvotes: 1

Related Questions