Localhost react app not receiving cookie from Heroku hosted node API - node.js

I am able to log in and out with POSTMAN through the heroku hosted node API.
In my react application, the API call with AXIOS (withcredentials:true) does not set the passport cookies, causing the session to not persist. Localhost react and Localhost server does not have this problem.
HEROKU SERVER SIDE, I have the following code:
app.use(cors({
origin: "http://localhost:3000",
credentials: true
}));
app.enable('trust proxy');
mongoose.connect(dbconfig.url, {
useUnifiedTopology: true,
useNewUrlParser: true,
useCreateIndex: true,
useFindAndModify: false });
// Parse URL-encoded bodies (as sent by HTML forms)
app.use(express.urlencoded({extended: true}));
app.use(cookieParser('street'));
app.use(session({
secret: 'katsura street',
resave: true,
saveUninitialized: true,
proxy: true,
store: new MongoStore({ mongooseConnection: mongoose.connection },)
}));
app.use(passport.initialize());
app.use(passport.session());
I have checked that the cookie parser is above the session,
the session is initialized before passport.
Is my cors affecting my local reactapp? instead of localhost, am I supposed to reference my local computer's external API?
REACT SIDE:
Axios.post(Config.ServerURL + 'auth/login',
{email: this.state.email, password: this.state.password},
{
withCredentials: true
}).then(result => {
console.log(result.data);
}).catch(error => {
//make sure message doesn't crash
if (error.response !== undefined) {
try {
console.log(error.response);
this.setState({message: error.response.data.info.message})
}catch(error) {
console.log(error);
}
}
});

After checking the headers and seeing that the cookies are sent but filtered out, I came to a article: heroku blog
After 2019, Chrome and other browsers have cross domain cookies disabled to prevent CSRF attacks.
In my use case, I can just turn it off with the following on the Node API server:
app.use(session({
secret: 'street',
resave: true,
saveUninitialized: true,
proxy: true,
cookie: {
sameSite:'none',
secure:true
},
store: new MongoStore({ mongooseConnection: mongoose.connection },)
}));
changing samesite to none, will allow chrome to set your cookies with cross domains.

In my case, it was just setting, in the session settings, cookie.sameSite to "none", and proxy to true.

Related

Cookie not being sent to AWS ECS server from ReactJS

I am current deploying a MERN Stack application and have successfully deployed the backend api to http://44.198.159.229/. I am now trying to connect it to my client server which is still running on localhost:3000. However, I am running into a cookie related issue. I am receiving the cookie on my frontend from the backend express server, but upon making a get request an authenticated route the frontend is not sending the cookie back. In the network tag in google chrome I see that the cookie is instead filtered out. I have done countless research and browsed various posts but cannot seem to find the solution for this. It works when I check the api route manually in my browser but does not upon sending an axios request. It also works when I'm deploying the backend on my local server but I imagine because they are both on the same domain.
Here is my express configuration on the backend.
const corsOptions = {
credentials: true,
origin: "http://localhost:3000",
optionsSuccessStatus: 200,
};
// Express backend for web application
const app = express();
app.set("trust proxy", true);
/////////////////////////////////////////////////////// Middleware //////////////////////////////////////////////////
app.use(cors(corsOptions));
app.use(cookieParser());
app.use(
session({
secret: "somethingsecretgoeshere",
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: false,
secure: false,
maxAge: 10 * 60 * 100000,
sameSite: 'none'
},
})
);
app.use(express.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
app.use(passport.initialize());
app.use(passport.session());
passportConfig(passport);
app.use("/api", auth_routes);
app.use("/api", major_requirement_routes);
app.use("/api", user_course_routes);
export default app;
Here is the route at which I am making the get request to see if a user is authenticated
router.get("/auth/check", (req, res) => {
console.log(req.user)
console.log(req.cookies)
if (req.user) {
User.findOne({netId: req.user}, function (err, docs) {
if (err) {
console.log(err);
} else {
res.json({
auth: true,
user: req.user,
courseList: docs.courseList,
semesterList: docs.semesterList,
major: docs.major,
creditsApplied: docs.creditsApplied,
emailAddress: docs.emailAddress,
});
}
});
} else {
res.json({auth: false, id: null});
}
});
Here is my axios config
import axios from "axios";
const backend_url = "http://44.198.159.229:5000/api"
// const backend_url = "http://localhost:5000/api"
export default axios.create({
withCredentials: true,
baseURL: backend_url,
});
Here is my axios get request on the frontend
axios
.get("auth/check", { withCredentials: true,credentials: 'include',
})
.then(({ data}) => {
console.log(data)
if (data.auth) {
setIsAuthenticated(true);
setUser(data.user);
setCourseList(data.courseList);
setIsLoading(false);
} else {
setIsAuthenticated(false);
setCourseList(undefined);
setUser(undefined);
setIsLoading(false);
}
})
.catch(() =>
console.log(
"Something went wrong while trying to fetch your auth status."
)
);
}, []);
Okay so after a lot of research and playing around for a few days I have found a solution. I had to use a SSL and redirect traffic to an https server via AWS load balancing and set sameSite: None httpOnly: true, secure: true. I hope this helps someone else. This is because cookies can only be sent to cross origin sites that are secure. I also had to change my local host to run on https instead of http

Why is the express session cookie not stored in the browser's cookie storage on production but is working on localhost/development? (express + react)

I have created a test app, my react app is deployed at vercel and my node express is deployed at render.com. I set the same domain on both to solve cross-site cookie problems (app.mydomain.online)(api.mydomain.online). Now no error is showing when I view the cookie in the header but still when I check the cookie storage it is still not stored or not being saved at the browser's cookie storage.
server is created via npm init.
react is created via npm create-react-app.
as of now this is my sample code.
server
const express = require("express");
const cors = require("cors");
const session = require('express-session');
const app = express();
require('dotenv').config();
const PORT = process.env.PORT;
app.use(express.json());
app.use(cors({
origin: 'https://app.myDomain.online',
methods: ["POST", "PUT", "GET", "OPTIONS", "HEAD"],
credentials: true
}));
const oneDay = 1000 * 60 * 60 * 24;
app.set('trust proxy', 1) // trust first proxy
app.use(session({
secret: 'keyboard cat',
resave: false,
saveUninitialized: true,
cookie: { secure: true, sameSite: 'none' }
}));
app.get('/createSession', (req, res) => {
req.session.user = 'user';
res.send('new session is created');
});
app.get('/', (req, res) => {
res.send('get sess')
});
app.get('/getSession', (req, res) => {
if(req.session.user){
res.send('active');
}else{
res.send('not active');
}
});
app.listen(PORT, () => {
console.log(`The server is running on port ${PORT}`);
});
react
import React from 'react'
import { useEffect } from 'react';
import Axios from 'axios';
function Test() {
useEffect(() => {
Axios.get(' https://api.myDomain.online/createSession',
{ withCredentials: true }
);
}, []);
return (
<div>Test</div>
)
}
export default Test;
From the documentation for express-session...
cookie.expires
Specifies the Date object to be the value for the Expires Set-Cookie attribute. By default, no expiration is set, and most clients will consider this a “non-persistent cookie” and will delete it on a condition like exiting a web browser application.
The docs go on to prefer the maxAge property to control this. Choose a time frame that makes sense for your application. For example, 1 week...
app.use(
session({
secret: "keyboard cat",
resave: false,
saveUninitialized: true,
cookie: { secure: true, sameSite: "none", maxAge: 7 * 24 * 60 * 60 * 1000 },
})
);
I have already fixed it a few days ago, I found out that the reason why the cookie was blocked was that the cookie has exactly the same domain as the server or probably has the same link address. As the domain of the server is api.myDomian.online, the cookie domain can't be api.myDomain.online. Not sure if that is the direct reason, but somewhat similar I think as the code works by setting a new domain to the cookie. I just removed the subdomain of the cookie like .myDomain.online and it works. here is my configuration. My backend is already deployed at aws when I test it but it could also work in render. I will try it out later on.
This is my new configuration
const oneDay = 1000 * 60 * 60 * 24;
const APP_SESSION = session({
secret: 'secrete',
resave: false,
saveUninitialized: true,
name: 'session',
cookie: {
secure: true,
sameSite: 'none'
maxAge: oneDay,
domain: '.domain.com'
}
});
Haven't tried to remove sameSite if it will still work.

How to setup Node to make different domains share cookies?

I have the problem that on production (Apache server/MERN stack) certain cookies are not accessible from the browser with document.cookie.
On localhost, both front-end and back-end are on the same domain, namely localhost, and just use different port numbers. Because they are working on the same domain, they share cookies.
On production, however, front-end and back-end operate on different (sub)domains. As a result, they don't have access to each other's cookies.
How can I make the cookies set by the back-end accessible for the front-end, also on production?
I thought this should be done with CORS, and with httpOnly: false and sameSite: 'none'. But the Node setup below doesn't work. In the browser I'm unable to access from the front-end the cookies set by the back-end.
var cors = require("cors");
const session = require("express-session");
const csurf = require("csurf");
const cookieParser = require("cookie-parser");
var corsOptions = {
origin: process.env.CORS_ORIGIN_URL.split(","), // the front-end is included here.
credentials: true,
exposedHeaders: ["set-cookie"],
};
app.use(cors(corsOptions));
let sessionSettings = {
secret: process.env.SESSION_SECRET,
key: process.env.SESSION_KEY,
store: sessionStore,
resave: true,
saveUninitialized: true,
cookie: {
secure: false,
},
};
app.use(session(sessionSettings));
const protect = csurf({
cookie: true,
httpOnly: false,
sameSite: 'none'
});
app.use(
cookieParser("test", {
sameSite: 'none',
httpOnly: false,
secure: false,
maxAge: 900000,
})
);
app.use((req, res, next) => {
protect(req, res, next);
});
app.use((req, res, next) => {
if (req.csrfToken) {
res.cookie(
"XSRF-TOKEN",
req.csrfToken(),
{
secure: true,
httpOnly: false,
sameSite: 'None'
}
);
}
next();
});
app.use(`${process.env.API_PATH}/csrf`, (req, res) => {
return res.status(200).json({
status: true,
csrfToken: req.csrfToken(),
});
});
...
Here you need to share the cookie with subdomains and main domain. You can do this by adding a domain field in res.cookie options. Now your main domain and subdomains can access this cookie.
res.cookie(
"XSRF-TOKEN",
req.csrfToken(),
{
secure: true,
httpOnly: false,
sameSite: 'None',
domain: 'mydomain.com'
}
);

How can I set the domain attribute in express-session based on the request origin?

I'm using express session. I set the domain domain: 'mydomain.com' so that the session cookie can be set between subdomains- like api.mydomain.com and staging.mydomain.com.
But this prevents the Set-Cookie header from setting the cookie when testing with a localhost frontend. I get Set-Cookie was blocked because its Domain attribute was invalid with regards to the current host url.
So I need to make the domain attribute change to localhost if the origin is localhost.
If I conditionally set the domain, we don't have access to req:
app.use(session({
secret: 'very secret 12345',
resave: true,
saveUninitialized: false,
store: new MongoStore({ mongooseConnection: mongoose.connection }),
cookie: {
domain:
req.get('origin').slice(0, 17) === 'http://localhost:' ? 'localhost' : 'mydomain.com',
secure: true,
httpOnly: true,
sameSite: none,
},
})
);
This returns ReferenceError: req is not defined.
So I tried calling session in a custom middleware to get access to req:
app.use((req, res, next) =>
session({
secret: 'very secret 12345',
resave: true,
saveUninitialized: false,
store: new MongoStore({ mongooseConnection: mongoose.connection }),
cookie: {
domain:
req.get('origin').slice(0, 17) === 'http://localhost:' ? 'localhost' : 'mydomain.com',
secure: true,
httpOnly: true,
sameSite: none,
},
})
);
But it doesn't work. It seems that with this, res, req, and next don't get passed in to the middleware function that session() returns. I also trying calling the function session() that returned -session({..options..})() , but that didn't work either.
How can I set the domain attribute based on the request origin?
I had to call the function and pass in req, res, and next
app.use((req, res, next) =>
session({
secret: 'very secret 12345', // to do, make environment variable for production
resave: true,
saveUninitialized: false,
store: new MongoStore({ mongooseConnection: mongoose.connection }),
cookie: {
domain:
req.get('origin').slice(0, 17) === 'http://localhost:' ? 'localhost' : 'mydomain.com',
secure: true,
httpOnly: true,
sameSite: none,
},
},
})(req, res, next)
);

Express Session : terminate session on tab/browser close

I need to terminate the user session or log user out when they close the browser or tab.
Following is the code implemented to maintain session:
app.use(session({
store: new RedisStore({
url: REDIS_CONNECTION_URL,
}),
secret: 'COOKIE_SECRET',
name: 'COOKIE_NAME',
resave: true,
saveUninitialized: false,
rolling: true,
cookie: {
path: '/',
httpOnly: true,
maxAge: 'COOKIE_TIMEOUT',
},
}));
I have tried setting cookie.expires to true but that doesn't help.
You have to handler onclose event of client user, then call a http request to destroy client's session on server side.
Client side:
$(window).unload(function () {
$.get('/session/destroy');
});
Server side:
app.get('/session/destroy', function(req, res) {
req.session.destroy();
res.status(200).send('ok');
});

Resources