I am learning Express.js, trying to create an api and connect it to React frontend. In express, I'm using express-session to create a session. For authentication, I use passport.js.
Here is the part of my app.js file:
const session = require('express-session');
const mongoDbStore = require('connect-mongodb-session')(session);
const store = new mongoDbStore({
uri: 'mongodb://localhost:27017/DB_NAME',
collection: 'UserSessions'
});
const app = express();
app.use(cors())
app.use(express.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
const sessionConfig = {
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: true,
store,
cookie: {
expires: Date.now() + 1000 * 60 * 60 * 24 * 7,
maxAge: 1000 * 60 * 60 * 24 * 7,
httpOnly: true
}
}
app.use(session(sessionConfig));
app.use(passport.initialize());
app.use(passport.session());
passport.use(new LocalStrategy(User.authenticate()));
passport.serializeUser(User.serializeUser());
passport.deserializeUser(User.deserializeUser())
app.listen(8080, () => {
console.log('listening on port 8080')
});
My issue is that when I send a request to my backend from my React app, I don't receive session id as a cookie. On my frontend, I use axios, to send requests:
import axios from 'axios';
export default axios.create({
baseURL: "http://localhost:8080/",
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
Why I don't receive a cookie from backend on my frontend and how can I fix this?
Thank you!
Because your cookie is a httpOnly cookie so in order to send the cookie you can do by fetch method:
const req = await fetch(URL,{
method : "POST",
credentials : "include", // to send HTTP only cookies
headers: {
"Contetnt-Type" : "application/json"
},
body : JSON.stringigy({name : "Bob"})
}):
const result = await req.json();
By axios you can also add withCredential properties:
axios.get(BASE_URL + '/todos', { withCredentials: true });
and also in backend consider this parametrs:
const corsOptions = {
optionsSuccessStatus: 200,
credentials: true,
}
app.use(cors(corsOptions))
Related
I've got the frontend and backend on different servers. when I first deployed the services I've got "SameSite: none" because of different origins, then when I set it to none, it required me to set "Secure: true" as well, after setting that I'm unable to see the Set-cookie header on server's response and on production the cookie is just not recieved.
here's main.ts with the express-session middleware:
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
const sessionSecret = app.get(AppConfigService).getConfig().session.secret;
const frontDomain = app.get(AppConfigService).getConfig().front.domain;
const port = app.get(AppConfigService).getConfig().app.port;
app.setGlobalPrefix('api');
app.use(
session({
name: 's.id',
secret: sessionSecret,
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 360000, // 1hour in seconds
secure: process.env.NODE_ENV !== 'production' ? false : true,
},
store: new PrismaSessionStore(new PrismaClient(), {
checkPeriod: 2 * 60 * 1000, //ms
dbRecordIdIsSessionId: true,
dbRecordIdFunction: undefined,
}),
}),
);
app.enableCors({
origin: [frontDomain, `${frontDomain}/`],
credentials: true,
});
app.use(passport.initialize());
app.use(passport.session());
await app.listen(port);
}
bootstrap();
try this, it works for me
const app = await NestFactory.create<NestExpressApplication>(AppModule{cors: true,});
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 make express-session work on a cross-site setup (Cannot set a cookie from the backend to the frontend)?
I created a test app as I am having a hard time finding ways to set cookies to the front end. When I try to set sameSite: 'none' and cookie: true, I cannot find the cookie in the header. But when I set the cookie to false and remove the sameSite, The cookie was in the header but it was blocked and i am getting an error such as I should set the sameSite=none and secure to true. The website is deployed at render.com.
The link of the react looks like this
https://react-websiteName-2d0w.onrender.com
The link of the server looks like this
https://api-websiteName-hj7g.onrender.com
//server
const express = require("express");
const cors = require("cors");
const sessions = require('express-session');
const app = express();
const PORT = 3001;
app.use(express.json());
app.use(cors({
origin: 'https://react-websiteName-2d0w.onrender.com',
methods: ["POST", "PUT", "GET", "OPTIONS", "HEAD"],
credentials: true
}));
const oneDay = 1000 * 60 * 60 * 24;
app.use(sessions({
secret: "secret",
saveUninitialized: true,
cookie: {
httpOnly: true,
secure: true,
maxAge: oneDay,
sameSite:'none'
},
resave: false
}));
app.post('/createSession', (req, res) => {
req.session.user = req.body.user;
res.send('new session is created');
});
app.listen(PORT, () => {
console.log(`The server is running on port ${PORT}`);
});
//react app
import { useEffect } from 'react';
import Axios from 'axios';
import './App.css';
import axios from 'axios';
const App = () => {
axios.defaults.withCredentials = true;
useEffect(() => {
Axios.post('https://api-websiteName-hj7g.onrender.com/createSession',
{ user: "user" }
);
}, []);
return (
<div>
<h1>Check if there is a cookie</h1>
</div>
);
}
export default App;
I am trying to make express-session work on my project when deployed with different domian.
Are there better alternatives or methods that could work in render.com instead of express-session?
I am trying to identify the user that is on my application via sessionId, not actual info on the user account itself. However, what I am noticing is that the sessionId changes everytime the user performs an action on the page. As shown below. My goal would be to have the same sessionID from the point they open the webpage until they close it.
const app = require('express')();
const https = require('https');
const fs = require('fs');
const session = require('express-session');
function getDateTimestamp(){
var today = new Date();
var date = today.getFullYear()+'_'+(today.getMonth()+1)+'_'+today.getDate();
return date;
}
app.use(session({
resave: false,
saveUninitialized: true,
secret: 'whatever',
cookie: {
maxAge: 60*60*1000,
sameSite: true
}
}))
app.get('/', (req, res) => {
res.writeHead(200, {'Content-Type': 'text/html'});
var readStream = fs.createReadStream('index.html','utf8');
readStream.pipe(res);
});
app.post('/:fieldName/:flag/:time/:dashboard/:identifier/:user', (req, res) => {
console.log('POST message received', req.params);
if (req.params && req.params.fieldName) {
fs.appendFileSync(`./changeLog_${getDateTimestamp()}.csv`, `${req.params.fieldName},${req.params.flag},${req.params.time},${req.params.dashboard},${req.params.identifier},${req.params.user},${req.sessionID}\n`);
return res.send('OK')
}
res.status(400).end()
});
Client Side
function onParameterChange (parameterChangeEvent) {
parameterChangeEvent.getParameterAsync().then(function (param) {
parameterIndicator = 'Parameter'
const details = {
method: 'POST',
credentials: 'include'
//body: JSON.stringify(data),
// headers: {
// 'Content-Type': 'application/json'
// }
};
fetch(`url/${param.name}/${parameterIndicator}/${getDateTimestamp()}/${dashboardName}/${param.name}/${worksheetData}`, details).then((res) => {console.log(res);});
});
}
Here is my output showing a different session for the same user.
Just to illustrate my comment above, I actually have ran a quick test with a simple setup, and toggling saveUninitialized actually seems to make the difference:
// app.js
const express = require('express')
const app = express()
const session = require('express-session')
// Run the file as "node app false" or "node app true" to toggle saveUninitialized.
const saveUninitialized = process.argv[2] == "true" ? true : false
app.use(session({
resave: false,
saveUninitialized,
secret: 'whatever',
cookie: {
maxAge: 60 * 60 * 1000,
sameSite: true
}
}))
app.get("/", (req, res) => {
res.status(200).send(req.sessionID)
})
app.listen(3000, () => {
console.log('server started on http://localhost:3000')
})
// Response body
node app false
// 1st request: OTnFJD-r1MdiEc_8KNwzNES84Z0z1kp2
// 2nd request: 5UVVGng_G72Vmb5qvTdglCn9o9A4N-F6
// 3rd request: 9aGsAwnHh1p1sgINa1fMBXl-oRKcaQjM
node app true
// 1st request: StUrtHOKBFLSvl5qoFai6OQCm7TY87U-
// 2nd request: StUrtHOKBFLSvl5qoFai6OQCm7TY87U-
// 3rd request: StUrtHOKBFLSvl5qoFai6OQCm7TY87U-
But maybe there is more to it than that with your setup.
I've a Node.js backend service and a React frontend. It was working till today when I had again an issue related to the CORS. It works fine in my local env but when I deploy this to App Engine the CORS issue is still there. What's is missing here?
Here my code:
Node.JS Backend Service:
const app = express();
/* MIDDLEWARE USER: set up cors to allow us to accept requests from our client */
app.use(
cors({
origin: process.env.CLIENT_URL || 'http://localhost:3001', // allow to server to accept request from different origin
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
credentials: true, // allow session cookie from browser to pass through
}),
);
I'm using passport to obtain credentials from Google and pass to the server
/* MIDDLEWARE USE: use Session Middleware */
const MAX_AGE = process.env.MAX_AGE || 60 * 60 * 1000;
const SECRET = process.env.SECRET || 'Our Secret';
const DEFAULT_ENV = process.env.NODE_ENV || 'development';
app.use(session({
cookie: {
maxAge: MAX_AGE,
secure: DEFAULT_ENV === 'production',
// secure: true,
httpOnly: true,
},
secret: SECRET,
resave: false,
saveUninitialized: false,
// store: new FileStore(fileStoreOptions),
store: new FirestoreStore({
dataset: new Firestore({
kind: 'express-sessions',
}),
}),
}));
/* MIDDLEWARE USE: use Passport Middleware */
app.use(passport.initialize());
app.use(passport.session());
Then I use react & redux in my frontend and here the code to obtain credentials from my endpoint.
/* RETRIEVE INFO FROM OAUTH AS SOON USER CLICK ON LOGIN WITH GOOGLE */
export const loginWithGoogle = () => {
return (dispatch) => {
dispatch({type: FETCH_START});
axios.post('/auth/login/oauth/success').then(({data}) => {
// console.log('userSignInFromGoogle: ', data);
if (data) {
const {originalMaxAge} = data.session.cookie;
const expireDate = (new Date()).getTime() + originalMaxAge;
localStorage.setItem('token', JSON.stringify(data.result.accessToken));
localStorage.setItem('token_expires_in', JSON.stringify(expireDate));
axios.defaults.headers.common['Authorization'] = 'Bearer ' +
data.result.accessToken;
dispatch({type: FETCH_SUCCESS});
// dispatch({type: USER_DATA, payload: data.result});
dispatch({type: USER_TOKEN_SET, payload: data.result.accessToken});
} else {
dispatch({type: FETCH_ERROR, payload: data.error});
}
}).catch(function(error) {
dispatch({type: FETCH_ERROR, payload: error.message});
// console.log('Error****:', error.message);
});
};
};
/* FUNCTION TO FETCH DATA FROM THE AUTHENTICATED USER */
export const getAuthenticatedUser = () => {
return (dispatch) => {
dispatch({type: FETCH_START});
isTokenExpired();
axios.post('auth/me',
).then(({data}) => {
// console.log('userSignIn: ', data);
if (data.result) {
dispatch({type: FETCH_SUCCESS});
dispatch({type: USER_DATA, payload: data.result});
} else {
dispatch({type: FETCH_ERROR, payload: data.error});
}
}).catch(function(error) {
dispatch({type: FETCH_ERROR, payload: error.message});
// console.log('Error****:', error.message);
if (error) {
dispatch({type: SIGNOUT_USER_SUCCESS});
localStorage.removeItem('token');
localStorage.removeItem('token_expires_in');
}
});
};
};
Here where I define the endpoint for axios:
import axios from 'axios';
/* TODO: Change In production with this */
export default axios.create({
withCredentials: true,
baseURL: `backend-url`,//YOUR_API_URL HERE
headers: {
'Content-Type': 'application/json',
},
});
After some test to figure out what the problem could be here, I finally tested this code on various browser and then only Chrome showed this issue not passing the token from the backend server to the other server. In the end I modified the code snippet related to the session store, adding the "sameSite" property to the cookie. Chrome, in the latest version, requires this property to be specified, otherwise it blocks cookies from server to server.
/* MIDDLEWARE USE: use Session Middleware */
const MAX_AGE = process.env.MAX_AGE || 60 * 60 * 1000;
const SECRET = process.env.SECRET || 'Our Secret';
const DEFAULT_ENV = process.env.NODE_ENV || 'development';
app.use(session({
cookie: {
maxAge: MAX_AGE,
secure: DEFAULT_ENV === 'production',
httpOnly: true,
/*TODO: Fix for chrome*/
sameSite: 'none',
},
secret: SECRET,
resave: false,
saveUninitialized: false,
// store: new FileStore(fileStoreOptions),
store: new FirestoreStore({
dataset: new Firestore({
kind: 'express-sessions',
}),
}),
}));