fetch: cookies not receiving cookies - node.js

I have a express server running on localhost:5000 and client running on port 3000.
After sending post request from client to login using fetch API browser is not setting cookie. I'm getting cookie in response header as 'set-cookie' but it isn't set in the browser at client side.
Here is my fetch request code:
return (
fetch(baseUrl + "users/login", {
method: "POST",
body: JSON.stringify(User),
headers: {
"Content-Type": "application/json",
},
credentials: "same-origin",
})
Server side code:
router
.route("/login")
.options(cors.corsWithOptions, (req, res) => {
res.sendStatus(200);
})
.post(cors.corsWithOptions, (req, res, next) => {
passport.authenticate("local", (err, user, info) => {
if (err) return next(err);
if (!user) {
res.statusCode = 401;
res.setHeader("Content-Type", "application/json");
res.json({ success: false, status: "Log In unsuccessful", err: info });
}
req.logIn(user, (err) => {
if (err) {
res.statusCode = 401;
res.setHeader("Content-Type", "application/json");
// res.header("Access-Control-Allow-Credentials", true);
res.json({
success: false,
status: "Login Unsuccessful!",
err: "Could not log in user!",
});
}
var token = authenticate.getToken({ _id: req.user._id });
res.cookie("jwt-token", token, {
signed: true,
path: "/",
httpOnly: true,
});
res.statusCode = 200;
res.setHeader("Content-Type", "application/json");
res.json({ success: true, status: "Login Successful!", token: token });
});
})(req, res, next);
});

How to handle Server-side session using Express-session
Firstly you will need the following packages
npm i express-session connect-mongodb-session or yarn add express-session connect-mongodb-session
Now that we have packages that we need to setup our mongoStore and express-session middleware:
//Code in server.js/index.js (Depending on your server entry point)
import expressSession from "express-session";
import MongoDBStore from "connect-mongodb-session";
import cors from "cors";
const mongoStore = MongoDBStore(expressSession);
const store = new mongoStore({
collection: "userSessions",
uri: process.env.mongoURI,
expires: 1000,
});
app.use(
expressSession({
name: "SESS_NAME",
secret: "SESS_SECRET",
store: store,
saveUninitialized: false,
resave: false,
cookie: {
sameSite: false,
secure: process.env.NODE_ENV === "production",
maxAge: 1000,
httpOnly: true,
},
})
);
Now the session middleware is ready but now you have to setup cors to accept your ReactApp so to pass down the cookie and have it set in there by server
//Still you index.js/server.js (Server entry point)
app.use(
cors({
origin: "http://localhost:3000",
methods: ["POST", "PUT", "GET", "OPTIONS", "HEAD"],
credentials: true,
})
);
Now our middlewares are all setup now lets look at your login route
router.post('/api/login', (req, res)=>{
//Do all your logic and now below is how you would send down the cooki
//Note that "user" is the retrieved user when you were validating in logic
// So now you want to add user info to cookie so to validate in future
const sessionUser = {
id: user._id,
username: user.username,
email: user.email,
};
//Saving the info req session and this will automatically save in your mongoDB as configured up in sever.js(Server entry point)
request.session.user = sessionUser;
//Now we send down the session cookie to client
response.send(request.session.sessionID);
})
Now our server is ready but now we have to fix how we make request in client so that this flow can work 100%:
Code below: React App/ whatever fron-tend that your using where you handling logging in
//So you will have all your form logic and validation and below
//You will have a function that will send request to server
const login = () => {
const data = new FormData();
data.append("username", username);
data.append("password", password);
axios.post("http://localhost:5000/api/user-login", data, {
withCredentials: true, // Now this is was the missing piece in the client side
});
};
Now with all this you have now server sessions cookies as httpOnly

Related

Express session always creating new session when making get/post request from react js

I'm making post or get request from react JS using Fetch api when requesting the session from the express-session always creating new session.
Backend
app.use(session({
secret: 'sec12',
resave: false,
saveUninitialized: false,
store: MongoStore.create({
mongoUrl: keys.mongodb.dbURI,
autoRemove: 'native',
ttl: 9 * 60 * 60,
mongoOptions: {
useNewUrlParser: true,
useUnifiedTopology: true
}
})
}));
cors
var corsOptions = {
origin: 'http://localhost:3000',
methods: ['GET', 'POST'],
optionsSuccessStatus: 200,
credentials:true,
preflightContinue: true,
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With', 'Accept']
}
app.use(cors(corsOptions))
and from the react JS
var requestOptions = {
method: 'POST',
headers: headers,
body: JSON.stringify(formData),
redirect: 'follow',
credentials: 'include',
};
fetch(url, requestOptions)
.then(response => {
if(response.ok){
return response.json()
}else{
return reject('Oppps... Something went wrong the backend server not found!')
}
})
.then(result => result?.error ? resolve(result) : resolve(result))
I Think, I figure it out.. just adding req.cookie once authenticated and parse it in every request.
put this script once the login is authenticated
res.cookie('access_token', token, { maxAge: 900000, httpOnly: true });
and then using cookieParser and parse this in every request
const cookieExtractor = req =>{
let token = null;
if(req && req.cookies){
token = req.cookies['access_token']
}
return token;
}
// MIDDLEWARE
passport.use(new JwtStrategy({jwtFromRequest : cookieExtractor, secretOrKey :
"secret01"}, async (payload, done)=>{
User.findById({_id : payload.sub},(err, user)=>{
if(err) {
return done(err,false);
}
if(user){
return done(null,user);
}else{
return done(null,false);
}
});
}));

Cookie not set, even though it is in response headers. Using express-session cookies

Problem:
Trying to set the cookie on login using express-session, but think I'm missing something obvious. The response to the login POST request includes Set-Cookie. I've also set the Access-Control-Allow-Origin and Access-Control-Allow-Headers to wildcards as shown here:
https://i.stack.imgur.com/XS0Zv.png
But we see that in the browser storage (tried with Firefox and Chrome) there is nothing. As shown here
I'm currently setting my express-session as follows (refer to end of post for full code. Adding snippet for easier read):
app.use(session({
genid: () => { return uuidv4(); },
store: new MongoStore({ mongooseConnection: mongoose.connection }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: true,
cookie: {
httpOnly: true,
secure: false,
sameSite: true,
}
})
);
Then after I've verified the user is getting logged in, I try to set the userId via:
req.session.userId = user.id;
Possibly Relevant Info
These sessions are successfully getting stored in Mongo as you can see here, which makes me believe that I'm at least generating the sessions correctly. Now I could be totally wrong here...
my backend is running on localhost:8000 via: app.listen(8000);
my client is running on http://localhost:3000/
trying not to use Apollo GraphQL for learning purposes
Things I've tried so far:
different combinatons of resave, saveUnitialized.
remove the cookie parameter.
stop setting userId
restarting browser and servers
Looked at relevant stack overflow posts
Please advise! Even ideas on how to debug this or what other things I can look at would be immensely helpful!
Relevant Code
app.js
const express = require('express');
const { graphqlHTTP } = require('express-graphql');
const mongoose = require('mongoose');
const session = require('express-session');
const MongoStore = require('connect-mongo')(session);
const {v4: uuidv4} = require('uuid');
const graphqlSchema = require('./graphql/schema/index');
const graphqlResolvers = require('./graphql/resolvers/index');
const app = express();
const path = '/graphql';
app.use(bodyParser.json());
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'POST,GET,OPTIONS');
res.setHeader('Access-Control-Allow-Headers', '*');
if (req.method === 'OPTIONS') {
return res.sendStatus(200);
}
next();
});
mongoose
.connect(`mongodb+srv://${process.env.MONGO_USER}:${process.env.MONGO_PASSWORD}#cluster0.ccz92.mongodb.net/${process.env.MONGO_DB}?retryWrites=true&w=majority`,
{ useNewUrlParser: true, useUnifiedTopology: true, useFindAndModify: false }
)
.then(() => {
app.use(session({
genid: () => { return uuidv4(); },
store: new MongoStore({ mongooseConnection: mongoose.connection }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: true,
cookie: {
httpOnly: true,
secure: false,
sameSite: true,
}
})
);
app.use(path, graphqlHTTP({
schema: graphqlSchema,
rootValue: graphqlResolvers,
graphiql: true,
}));
app.listen(8000);
})
.catch(err => {
console.log(err);
});
graphql/resolvers/auth.js
const argon2 = require('argon2');
const jwt = require('jsonwebtoken');
const User = require('../../models/user');
module.exports = {
createUser: async args => {
try {
const existingUser = await User.findOne({
email: args.userInput.email
});
if (existingUser) {
throw new Error('User exists already.');
}
const hashedPassword = await argon2.hash(
args.userInput.password,
12
);
const user = new User({
email: args.userInput.email,
password: hashedPassword,
loggedIn: true
});
const result = await user.save();
const token = jwt.sign(
{ userId: result.id, email: result.email },
process.env.JWT_KEY,
{ expiresIn: '1h' }
);
return {
userId: result.id,
token: token,
tokenExpiration: 1
};
} catch (err) {
console.log("error in resolvers/auth.js");
throw err;
}
},
login: async (args, req) => {
const { userId } = req.session;
if (userId) {
console.log("found req.session");
return User.findOne({ _id: userId });
}
console.log("looking for user with ", args.userInput.email);
const user = await User.findOne({ email: args.userInput.email });
console.log("found user");
if (!user) {
throw new Error("User does not exist!");
}
user.loggedIn = true;
user.save();
const isEqual = await argon2.verify(user.password, args.userInput.password);
if (!isEqual) {
throw new Error ("Password is incorrect!");
}
console.log("setting session.userId");
req.session.userId = user.id;
return { ...user._doc, password: null};
},
logout: async (args, req) => {
if (!req.isAuth) {
throw new Error('Unauthenticated');
}
try {
const result = await User.findOneAndUpdate(
{ _id: req.userId },
{ loggedIn: false },
{ new: true },
);
return { ...result._doc, password: null };
} catch (err) {
console.log("logout error", err);
throw(err);
}
},
};
So it turned out to be a CORS issue. I didn't realize that the port would mean a different origin. In this case my client is at 3000 and my server is at 8000.
Given the CORS nature, in the client I need to include credentials (cookies, authorization headers, or TLS client certificates) when I'm fetching:
fetch(config.url.API_URL, {
method: 'POST',
body: JSON.stringify(requestBody),
headers: {
'Content-Type': 'application/json'
},
credentials: "include",
})
This will tell the user agent to always send cookies.
Then serverside I need to set Access-Control-Allow-Credentials to be true as such:
res.setHeader('Access-Control-Allow-Credentials', true);
This will allow the browser to expose the response (which has the cookie) to the frontend Javascript code.
Since we are using credentials, we will need to specify Access-Control-Allow-Headers and Access-Control-Allow-Origin
res.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept')
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:3000');

Cookie is not set using express and passport

I spent a long time trying figure it out why it's not working.
I'm implementing a login page using react.
This page send the user and pass to backend (nodejs + express) using axios:
const login = useCallback(e => {
e.preventDefault()
fetch(process.env.REACT_APP_HOST + '/login', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: e.target.elements.username.value,
password: e.target.elements.password.value
})
})
.then(response => {
if (response.ok) {
return response.json()
} else if (response.status === 401) {
throw new Error('Invalid user or pass.')
} else {
throw new Error('An error ocurred')
}
...
})
}, [])
In backend, I have a route that receive those data and check on ldap system. So I generate a token using JWT and save it on token
const express = require('express'),
passport = require('passport'),
bodyParser = require('body-parser'),
jwt = require('jsonwebtoken'),
cors = require('cors')
cookieParser = require('cookie-parser')
LdapStrategy = require('passport-ldapauth');
let app = express();
app.use(cookieParser())
app.use(cors());
....
app.post('/login', function (req, res, next) {
passport.authenticate('ldapauth', { session: false }, function (err, user, info) {
const token = jwt.sign(user, env.authSecret)
res.cookie('cookie_token', token) //it doesn't set the cookie
if (err) {
return next(err)
}
if (!user) {
res.sendStatus(401)
} else {
return res.status(200).send({ firstName: user.givenName});
}
})(req, res, next);
});
The problem is that the token is empty, it's not being set.
Couple of things. In your react fetch post method you need to add
withCredentials: true,
beside the httpheader.
fetch(process.env.REACT_APP_HOST + '/login', {
method: 'POST',
withCredentials: true,
credentials: 'include',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: e.target.elements.username.value,
password: e.target.elements.password.value
})
})
After that in your nodejs part you are using cors but validating all origin. Thats not going to work with credentials. You need to use cors like this to validate specific origin and also turn credentials to true-
app.use(cors({credentials: true, origin: 'http://localhost:4200'}));
after that you can send your cookie by
res.cookie('cookie_token', token, { maxAge: 900000 })
This way the cookie will arrive and once the cookie is arrived in client side you can retrieve the cookie with document.cookie or with any other package like "js-cookie"

res.cookie not setting cookie in browser

I am currently trying to set up a Node/Express app with a React client to interact with it. I setup passport to handle authentication with JWT. When the user logs in, I validate the email/password. I then set the cookie:
res.cookie('jwt', token, { httpOnly: true, secure: false });
I see the token being passed back in the response header, but when I inspect my Chrome browser's cookie under Developer Tools > Application > Cookies, I see an empty cookie. What am I doing wrong and how do I send the jwt in the response header with subsequent requests?
server/App.js
const app = express()
app.use(bodyParser.urlencoded({extended: true}));
app.use(bodyParser.json());
app.use(cookieParser());
app.use(function(req, res, next) {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
next();
});
app.post('/login', (req, res) => {
passport.authenticate('local', { session: false }, (error, user) => {
if (error || !user) {
res.status(400).json({ error });
}
// Construct JWT payload
const payload = {
email: user.email,
expires: Date.now() + parseInt(process.env.JWT_EXPIRATION_MS),
};
// Assign payload to req.user
req.login(payload, {session: false}, (error) => {
if (error) {
res.status(400).send({ error });
}
// Generate a signed JWT
const token = jwt.sign(JSON.stringify(payload), process.env.JWT_SECRET);
// Assign JWT to cookie
res.cookie('jwt', token, { httpOnly: true, secure: false });
res.status(200).send({ email: user.email });
});
})(req, res);
});
client/LoginModal.js
handleLogin = async () => {
const { name, email, password } = this.state
try{
const res = await axios.post('http://localhost:8080/login', {
email: email,
password: password,
})
if(res.status == 200){
console.log("Logged in")
console.log(res)
}
} catch (err) {
console.log(err)
}
}
Edit: My current workaround is to send the token as part of the payload. My react client then grabs the token from the payload and stores it in the browser's cookie. Is there a way to avoid this workaround (see below for example)?
server
res.status(200).send({ email: user.email, jwt: token });
client
if(res.status == 200){
cookies.set('jwt', res.data.jwt)
cookies.set('email', res.data.email)
}
When making the axis.post() call, you'll have to pass {withCredentials: true, credentials: 'include'} as your second argument, only this way will your browser set the cookies.
You have the cookie set with the httpOnly flag enabled. Most modern browsers restrict read access to such cookies through developer tools. You can read more about it here.
If you'd like to see the contents of the cookie in your development environment, set httpOnly to false.
This solution I found works with both local development and production ( and alos LAN access, eg. when you access the website on your LAN IP address such as http://192.168.xxx.xxx:<port>):
// Set CORS options
const cors = require(`cors`)
const whitelist = [
'http://localhost:<application port>', // not https
'https://yourprod.ip.address.com' // must be https!
'http://<your local IP>:<port>', // optional, LAN access
// ...
]
const corsOptions = {
credentials: true,
origin: (origin, callback) => {
// `!origin` allows server-to-server requests (ie, localhost requests)
if(!origin || whitelist.indexOf(origin) !== -1) {
callback(null, true)
} else {
callback(new Error("Not allowed by CORS: "+ origin))
}
},
optionsSuccessStatus: 200
}
app.use(cors(corsOptions))
Then on the authentication endpoint:
// Set Cookie
const cookieContent = 'this is a cookie'
const cookieOptions = {
httpOnly: true, // safety, does not allow cookie to be read in the frontend javascript
maxAge: 24*3600*1, // cookie age in seconds
sameSite: 'Strict' // works for local development
}
if(process.env.NODE_ENV === 'production') {
// these options work on a https server
cookieOptions.secure = true
cookieOptions.sameSite= 'None'
}
res.cookie(
'cookie-tag',
refreshToken,
cookieOptions
)
res.json(cookieContent)
What worked for me is setting app.use(cors({ origin: true, credentials: true })) in cors package. Also setting withCredentials: true, credentials: 'include' while fetching from backend

node.js - Passport not persisting across browser requests, works with Postman

I am currently using the create-react-app boiler plate and have been attempting to add auth. I am using axios as my promise based HTTP libray with React.js. I have been using node with express, express-session, passport and passport-local on the backend.
Here is my server.js file with some exlusions:
const express = require('express');
const mysql = require('mysql');
const app = express();
const cors = require('cors');
const session = require('express-session');
const passport = require('passport');
const morgan = require('morgan');
const bodyParser = require('body-parser');
const cookieParser = require('cookie-parser');
const LocalStrategy = require('passport-local').Strategy;
// Express only serves static assets in production
if (process.env.NODE_ENV === 'production') {
app.use(express.static('client/build'));
}
app.set('port', (process.env.PORT || 3001));
app.use(cors({
credentials: true,
origin: 'http://localhost:3000'
}));
app.use(morgan('dev'));
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.use(cookieParser());
app.use(session({
secret: 'topsecretpassword',
resave: true,
saveUninitialized: false,
cookie: {
path: '/',
originalMaxAge: 1000 * 60 * 60 * 24,
httpOnly: true,
secure: false
}
}));
app.use(passport.initialize());
app.use(passport.session());
// Setup Database connection
const connection = mysql.createConnection({
host : 'localhost',
user : 'root',
password : '',
database : 'mvy_db'
});
passport.serializeUser(function(user, done) {
done(null, user.id);
});
passport.deserializeUser(function(user, done) {
connection.query('SELECT * FROM users WHERE id=?', user, function(err, userId) {
if (err) {
res.status(400).json({
error: 'Database Error',
id: userId[0]
});
}
done(err, userId[0]);
});
});
passport.use(new LocalStrategy({
usernameField: 'email',
passwordField: 'password',
},
function(email, password, done) {
connection.query('SELECT * FROM users WHERE email=?', email, function(err, user) {
if (err) {
return done(err);
}
if (!user.length) {
return done(null, false, { message: 'Incorrect email.' });
}
if (user[0].password !== password) {
return done(null, false, { message: 'Incorrect password.' });
}
return done(null, user[0]);
});
}
));
app.post('/signin', passport.authenticate('local'), function(req, res) {
console.log(req.session);
return res.send('login success!');
});
function isAuthenticated (req,res,next){
console.log(req.session);
if(req.session.passport.user)
return next();
else
return res.status(401).json({
error: 'User not authenticated'
})
}
app.get('/checkauth', isAuthenticated, function(req,res) {
res.status(200).json({
status: 'User Authenticated!'
});
})
app.get('/signout', function(req,res) {
req.session.destroy();
res.status(200).json({ success: 'successfully signed out' });
})
Using postman (and even on the browser), I am able to successfully login and the following is held in the req.session object :
cookie:
{ path: '/',
_expires: null,
originalMaxAge: 86400000,
httpOnly: true,
secure: false },
passport: { user: 1 } }
my login request using axios:
return axios.post(ROOT_URL + 'signin', {
email: e.target.email.value,
password: e.target.password.value
}).then((response) => {
if (response.status === 200) {
console.log(response);
}
})
My checkAuth request using axios (this is where I get a 500 error returned):
axios.get(ROOT_URL + 'checkauth', { withCredentials: true })
.then((response) => {
if (response.status === 200) {
return true;
} else {
return false;
}
});
The req.session object after checking authentication before the error message, note that the passport object doesn't exist anymore:
Session {
cookie:
{ path: '/',
_expires: null,
originalMaxAge: 86400000,
httpOnly: true,
secure: false } }
This is the error message I get on the console when I attempt to check that the user is authorized:
TypeError: Cannot read property 'user' of undefined
at isAuthenticated (/server.js:94:26)
I've been banging my head for hours, trying to resolve this issue. I thought it might have something to do with CORS, but after hours of playing around with it that doesn't seem to be the case. It's still plausible that it's a CORS issue, but what's really flustering me is that it works full well with Postman but not on my Chrome browser. Any help is appreciated!
Alright, so I found the solution to my problem. It appeared to be an issue with axios and the configuration of my get requests. For some reason, using the structure axios.get(URL) .then(response) doesn't work with the withCredentials property.
Instead, I had to send my request as:
axios(ROOT_URL + 'checkauth', {
method: 'get',
withCredentials: true
})
.then((response) => {
if (response.status === 200) {
return true;
} else {
return false;
}
});
Oh because I forgot that axious doesn’t send credentials by default I had to stick with jwt and completely removed session.
You can define an instance of axious which will allow you to make requests much more simply
const $axios = axios.create({
baseURL: 'https://some-domain.com/api/',
withCredentials: true
});

Resources