Reputation: 849
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.
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
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
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
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://github.com/GoogleChromeLabs/samesite-examples/blob/master/javascript-nodejs.md
Upvotes: 2
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