I have a VueJS project that uses axios to call a server on another domain. On this server, I need to save a few values in the session so they don't need to be looked up on every request.
The server is NodeJS and runs on Heroku and I'm using Redis for memory storage. I can successfully save data to the session, but on every new request, the system creates a new session with a new ID so I can't access the values saved during the previous request.
EDIT
After updating the code based on a number of suggestions, I can see the following error in the Network console on the session cookie:
Preflight Invalid Allow Credentials
EDIT 2
I was able to resolve the Preflight Invalid Allow Credentials by adding "credentials: true" to the corsOptions. This resolves the error I was seeing in network on the session, but I am still getting a new session ID for every request.
Code on the server:
const express = require('express');
const app = express();
const cors = require('cors');
var corsWhitelist = ['http://127.0.0.1:8080','http://127.0.0.1:8081']
var corsOptions = {
origin: function (origin, callback) {
if (corsWhitelist.indexOf(origin) !== -1) {
callback(null, true)
} else {
callback(new Error('Not allowed by CORS - '+origin))
}
},
credentials: true
}
let REDIS_URL = process.env.REDIS_URL;
var Redis = require('ioredis');
const session = require('express-session');
const cookieParser = require('cookie-parser');
const RedisStore = require('connect-redis')(session);
const sessionClient = new Redis(REDIS_URL)
sessionClient.on('error', function (err) {
console.log('could not establish a connection with redis. ' + err);
});
sessionClient.on('connect', function (err) {
console.log('connected to redis successfully');
});
app.set('trust proxy', 1)
app.use(cookieParser());
app.use(session({
store: new RedisStore({ client: sessionClient }),
secret: 'someSecret',
resave: false,
saveUninitialized: true,
cookie: {
secure: false,
httpOnly: false,
maxAge: 1000 * 60 * 10
}
}))
app.use(cors(corsOptions));
app.options('*', cors(corsOptions))
// Add headers
app.use(function (req, res, next) {
if (corsWhitelist.indexOf(req.headers.origin) !== -1) {
res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
res.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE');
res.setHeader('Access-Control-Allow-Credentials', 'true');
}
next();
});
const getUser = async function(req, res, next) {
if (!req.session.user) {
req.session.user = "test#example.com"
req.session.save()
}
next()
}
app.get('/session', getUser, (req, res) => {
// get the session id
console.log('session id:', req.session.id)
// the session will be automatically stored in Redis with the key prefix 'sess:'
const sessionKey = `sess:${req.session.id}`;
// let's see what is in there
client.get(sessionKey, (err, data) => {
console.log('session data in redis:', data)
})
res.status(200).send('OK');
})
Method on VueJS:
getSession: async function () {
axios({
url: 'https://server.example.com/session',
withCredentials: true,
}).then(res => {
console.log(res)
})
},
There were a number of changes required to make it work:
The preflight settings were being set twice, so in the code below, I needed to remove the second line:
app.use(cors(corsOptions));
app.options('*', cors(corsOptions)) //delete this
The headers I was trying to set under "// Add headers" didn't make it to the preflight request, so instead I needed to add "credentials: true" to the corsOptions and remove the code under "// Add headers":
var corsOptions = {
origin: function (origin, callback) {
if (corsWhitelist.indexOf(origin) !== -1) {
callback(null, true)
} else {
callback(new Error('Not allowed by CORS - '+origin))
}
},
credentials: true
}
Last but not least, the cookie settings in the session definition weren't working for a cross-domain request. Specifically, "sameSite: 'none'" and "secure: true" were necessary. Result looks like this:
app.use(session({
store: new RedisStore({ client: client }),
secret: 'someSecret',
resave: false,
saveUninitialized: true,
cookie: {
secure: true,
httpOnly: false,
sameSite: 'none',
maxAge: 1000 * 60 * 10
}
}))
Related
I'm trying to integrate passport-google-oauth20 in my MERN application and although everything works fine in development on my local host but in production, it keeps throwing this error;
No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
I have searched and gone through multiple stack overflow posts and tried some of the answers and suggestions but nothing seems to work. This my index.js and the codes commented out are some of the solutions and CORS settings I've tried. I also tried putting the URL directly instead of an environment variable, but that didn't work either.
const express = require("express");
const cookieSession = require("cookie-session");
const passport = require("passport");
const cors = require("cors");
const helmet = require("helmet");
const connectDB = require("./config/db");
const authRoute = require("./routes/auth.route");
const userRoute = require("./routes/user.route");
const adminRoute = require("./routes/admin.route");
const transactionRoute = require("./routes/transaction.route");
//Passport setup
require("./passport");
const path = require("path");
// require("dotenv").config();
const app = express();
require("dotenv").config({
path: "./config/config.env",
});
//Connect to Database
connectDB();
//Use bodyParser
app.use(express.json());
app.use(
helmet({
contentSecurityPolicy: false,
frameguard: true,
})
);
app.use(
cookieSession({
name: "session",
keys: ["ccurves"],
maxAge: 24 * 60 * 60 * 100,
})
);
app.use(passport.initialize());
app.use(passport.session());
//Config for only development
// if (process.env.NODE_ENV === "development") {
// app.use(
// cors({
// origin: process.env.CLIENT_URL,
// methods: "GET,POST,PUT,DELETE",
// credentials: true,
// })
// );
// }
// const corsOptions = {
// origin: [`${process.env.CLIENT_URL}`],
// methods: "GET,HEAD,PUT,OPTIONS,POST,DELETE",
// allowedHeaders: [
// "Access-Control-Allow-Headers",
// "Origin",
// "X-Requested-With",
// "Content-Type",
// "Accept",
// "Authorization",
// "token",
// "Access-Control-Request-Method",
// "Access-Control-Request-Headers",
// "Access-Control-Allow-Credentials",
// ],
// credentials: true,
// preflightContinue: false,
// optionsSuccessStatus: 204,
// };
// app.use(cors(corsOptions));
app.use(
cors({
origin: process.env.CLIENT_URL,
methods: "GET,POST,PUT,DELETE",
credentials: true,
})
);
// app.use((req, res, next) => {
// res.header("Access-Control-Allow-Origin", `${process.env.CLIENT_URL}`);
// res.header("Access-Control-Allow-Methods", "POST, PUT, GET, OPTIONS, DELETE");
// res.header("Access-Control-Allow-Credentials", true);
// res.header(
// "Access-Control-Allow-Headers",
// "Access-Control-Allow-Headers, Origin, X-Requested-With, Content-Type, Accept, Authorization, token, Access-Control-Request-Method, Access-Control-Request-Headers, Access-Control-Allow-Credentials, Access-Control-Allow-Origin"
// );
// next();
// });
app.use("/api/auth", authRoute);
app.use("/api/user", userRoute);
app.use("/api/admin", adminRoute);
app.use("/api/transaction", transactionRoute);
const port = process.env.PORT;
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
And in my react frontend I'm fetching the request from the API like this:
const getUser = () => {
fetch(`${process.env.REACT_APP_API_URL}/auth/login/success`, {
method: "GET",
credentials: "include",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"Access-Control-Allow-Credentials": true,
},
})
.then((response) => {
if (response.status === 200) return response.json();
throw new Error("authentication has been failed!");
})
.then((resObject) => {
authenticate(resObject, () => {
isAuth && navigate("/");
});
})
.catch((err) => {
console.log(err);
});
};
I also tried using axios to send the request,
axios
.get(`${process.env.REACT_APP_API_URL}/auth/login/success`, {
withCredentials: true,
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
})
.then((res) => {
console.log(res);
authenticate(res.data, () => {
isAuth && navigate("/");
});
})
.catch((err) => {
console.log(err);
// setError(err.response.data.errors);
});
And it works fine and perfectly on my local host, all other routes are working and even the other authentication method works fine. Why is this particular route been blocked by CORS? When I open the API URL ${process.env.REACT_APP_API_URL}/auth/login/success directly in the browser I can see the json data is been sent fine. What is going on? Please what am I doing wrong?
#Kaneki21 Solution actually worked for me but he has deleted his answer so I'm reposting it. All credits go to him, so this cors configuration solved the error:
const corsOptions = {
origin: [`${process.env.CLIENT_URL}`],
methods: "GET,HEAD,PUT,OPTIONS,POST,DELETE",
allowedHeaders: [
"Access-Control-Allow-Headers",
"Origin",
"X-Requested-With",
"Content-Type",
"Accept",
"Authorization",
"token",
"Access-Control-Request-Method",
"Access-Control-Request-Headers",
"Access-Control-Allow-Credentials",
],
credentials: true,
preflightContinue: false,
optionsSuccessStatus: 204,
};
app.use(cors(corsOptions));
But I ran into another issue, Although the request was sent now, I noticed cookies weren't included in the header even after I added withCredentials: true. Inspecting the request in the chrome network tab I could see the cookies was been filtered out. I solved that by switching from cookie-session to express-session and using this config:
app.set("trust proxy", 1); // trust first proxy
app.use(
session({
secret: process.env.JWT_SECRET,
resave: false,
saveUninitialized: true,
cookie: {
secure: true,
sameSite: "none",
},
})
);
Notice the sameSite: "none" in the cookie configuration, that enables cross-sites request and it requires secure to be true. Previously cookie-session wasn't setting the sameSite attribute even when I included it in the cookie config and browsers set Lax as a default so hence the switch to express-session. You can check out these sites to understand more site1 site2
I hope this helps someone out cause I know this gave me quite a bit of a headache
I have been developing a simple login system and trying to deploy it on a production server. The express session works perfectly fine in localhost but when it comes to the production server or cross-origin it sends a new session in every request.
Server Codes:
const express = require('express');
const app = express();
const path = require('path');
const mysql = require('mysql');
const session = require('express-session');
const MySQLStore = require('express-mysql-session')(session);
const bcrypt = require('bcryptjs');
require('dotenv').config();
const cors=require("cors");
const bodyparser = require("body-parser")
app.use(express.json())
app.use(bodyparser.urlencoded({extended: true}))
app.use(express.static(path.join(__dirname, 'public')));
app.use(function (req, res, next) {
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:3000');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE');
res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type');
res.setHeader('Access-Control-Allow-Credentials', true);
next();
});
var db = mysql.createConnection({
host: process.env.DATABASE_HOST,
user: process.env.DATABASE_USER,
password: process.env.DATABASE_PASS,
database: process.env.DATABASE_NAME
});
db.connect((err) => {
if(err) {
console.log('error when connecting to db: ', err);
throw err;
}
});
const sessionStore = new MySQLStore({
expiration : (365 * 60 * 60 * 24 * 1000),
endConnectionOnClose: false,
}, db);
app.use(session({
key: 'fsasfsfafawfrhykuytjdafapsovapjv32fq',
secret: 'abc2idnoin2^*(doaiwu',
store: sessionStore,
resave: false,
saveUninitialized: false,
cookie: {
maxAge: (365 * 86400 * 1000),
httpOnly: false,
secure: false,
sameSite: 'none'
}
}));
app.post('/isLoggedIn', (req, res)=>{
// req.session.destroy();
if(req.session.userID || req.session.userID == 0) {
let cols = [req.session.userID];
db.query('SELECT * FROM user WHERE user_id = ? LIMIT 1',cols, (err, data, fields) => {
if(data && data.length === 1) {
res.json({
success: true,
first_name: data[0].first_name,
email: data[0].email,
type:data[0].type,
});
return true;
}else {
res.json({
success: false,
});
}
});
}else {
res.json({
success: false
})
}
});
API request:
let res = await fetch("https://hotel-network-manager1.herokuapp.com/isLoggedIn", {
method: "POST",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
credentials: 'include',
mode:'cors'
});
I have read many posts and documentation but couldn't solve the issue. You can even check the server running in Heroku in the above URL of the fetch API. When using this same server running on the localhost it works perfectly fine which means this has something to do with CORS. Does anyone has any idea what I'm missing here.
I'm using the passport.js local strategy.
I was testing it under proxy setting, localhost.
Things worked fine until I prepare to deploy.
I changed the API address to include dotenv and set CORS settings on the server-side.
When trying to login, CORS works fine, OPTIONS and the POST get 200 ok. The client receives the success data. cookie saved in client.
But when auth checking process runs right after Redux "isLoggedin" state is updated(useEffect), req.session doesn't
t have the passport object. So deserializeUser not be called. The session contains other cookie info except for Passport.
This one is only on Firefox(not Chrome): Page will be redirected if the login auth succeeded(it checks right after login redux state changed), but since it's failed, the user stays on the login page still. But if I try to login on the same page again, the cookie start to show the passport object.(in other words, it shows from 2nd attempt). But it doesn't persist because the Redux login state has been changed to true at the first login attempt already.(so Auth checking doesn't occur.)
client:
Axios.post(
`${process.env.REACT_APP_API_URI}/api/users/login`,
loginData,
{ withCredentials: true, }
).then((res) => res.data){
//save isLoggedin to true in Redux
}
// auth check logic starts right after changing isLoggedin. Axios Get to authenticateCheck
server.js
app.use(helmet());
// app.use(express.static('public'));
app.use("/uploads", express.static("uploads"));
// Passport configuration.
require("./utils/passport");
// connect to mongoDB
mongoose
.connect(db.mongoURI, {
useNewUrlParser: true,
useUnifiedTopology: true,
useCreateIndex: true,
useFindAndModify: false,
})
.then(() => console.log("mongoDB is connected."))
.catch((err) => console.log(err));
// CORS Middleware
const corsOptions = {
origin: "http://localhost:8000",
optionsSuccessStatus: 200, // some legacy browsers (IE11, various SmartTVs) choke on 204
credentials: true,
methods: ["POST", "GET", "DELETE", "PUT", "PATCH", "OPTIONS"],
allowedHeaders:
"Origin, X-Requested-With, X-AUTHENTICATION, X-IP, Content-Type, Accept, x-access-token",
};
// app.use(cors(corsOptions));
app.options(/\.*/, cors(corsOptions), function (req, res) {
return res.sendStatus(200);
});
app.all("*", cors(corsOptions), function (req, res, next) {
next();
});
// to get json data
// support parsing of application/json type post data
app.use(express.json());
app.use((req, res, next) => {
req.requestTime = new Date().toISOString();
next();
});
//support parsing of application/x-www-form-urlencoded post data
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser());
// db session store
const sessionStore = new MongoStore({
mongoUrl: db.mongoURI,
collection: "sessions",
});
// tell app to use cookie
app.use(
session({
secret: process.env.SESSION_SECRET_KEY,
resave: false,
saveUninitialized: false,
store: sessionStore,
cookie: {
httpOnly: true,
secure: false,
sameSite:"none",
maxAge: 24 * 60 * 60 * 1000, // 24 hours
//keys: [process.env.COOKIE_ENCRYPTION_KEY]
},
name: "pm-user",
})
);
// tell passport to make use of cookies to handle authentication
app.use(passport.initialize());
app.use(passport.session());
app.use(compression());
app.use(flash());
app.use((req, res, next) => {
console.log("req.session:", req.session);
// console.log('/////// req: ///////', req);
console.log("////// req.user: ", req.user, " //////");
next();
});
//---------------- END OF MIDDLEWARE ---------------------//
authController:
exports.authenticateCheck = (req, res, next) => {
console.log("req.isAuthenticated():", req.isAuthenticated());
if (req.isAuthenticated()) {
return next();
} else {
return res.json({
isAuth: false,
error: true,
});
}
};
It would be a really big help if you can advise me where to look to fix it.
Thanks.
I found the solution finally.
It was because the session was renewed every time when a new request starts other than a login request.
The solution was, I had to add { withCredentials: true } to every Axios option in my frontend.
So I am trying to build a simple login/authorization tool utilizing node.js, passport.js and angular2. My current issue is that while a user can login, the session information doesn't appear to be passed to the front end server or the front end server doesn't pass the passport information back.
When logging in the user appears to get all the way to the portion where the res.send is called, and at that point serialize has been called and req.sessions.passport.user has been set; however, when the user tries to go to an authorized page, while the cookie is there, the passport is missing. While Deserialized is also never called, the middleware is called/appears called. When the middleware gets to the deserializer there is no passport/user attached thus deserialize is never called.
At this point I am wondering if it might be a CORS issue or something with angular2, but I have been working on this for several days and appear to be doing it the suggested way. I have also tried rebuilding it and setting up CORS in multiple ways along with the middleware and I am running out of ideas. I am also using express session but that appears to be working because the cookie I create in that exists.
Session Data at the end of auth but before responding to the site
Session {
cookie:
{ path: '/',
_expires: null,
originalMaxAge: null,
httpOnly: true,
secure: false },
passport:
{ user:
anonymous {
username: 'test',
hash: '4024ca40c4372e029459a1d2d52a25b2fc4642f980f6cc948cc4b35f6350adde',
} } }
Session Data after making further requests
Session {
cookie:
{ path: '/',
_expires: null,
originalMaxAge: null,
httpOnly: true,
secure: false } }
Relevant Code:
Passport.js
passport.serializeUser((user, done) => {
done(null, user);
});
passport.deserializeUser((users, done) => {
var id=users.username;
db.one('select * from users where username = $1', id)
.then((user) => {
done(null, user);
})
.catch((err) => { done(err,null); });
});
local.js
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const init = require('./passport');
var promise = require('bluebird');
var options = {
// Initialization Options
promiseLib: promise
};
var hashclient = require('hashapi-lib-node');
const crypto = require('crypto');
var hash = crypto.createHash('sha256');
var pgp = require('pg-promise')(options);
var connectionString = 'postgresql://...';
var db = pgp(connectionString);
const optionsPassport = {};
init();
passport.use(new LocalStrategy(optionsPassport, (username, password, done) => {
db.one('select * from users where username = $1', username)
.then((user) => {
hash.update(password);
var encryptedPassword=hash.digest('hex');
hash = crypto.createHash('sha256');
if (!user) return done(null, false, { message: 'Incorrect username.' });
if (encryptedPassword!=user.password) {
return done(null, false, { message: 'Incorrect information.' });
} else {
return done(null, user);
}
})
.catch((err) => { return done(err); });
}));
helpers.js
function loginRequired(req, res, next) {
if (!req.user) return res.status(401).json({status: 'Please log in'});
return next();
}
Router.js example
const users = require('express').Router();
const auth = require('./auth');
const update = require('./update');
const password = require('./password');
const authHelpers = require('./helpers');
const passport = require('./local');
users.post('/update', authHelpers.loginRequired, update);
users.get('/:userId', authHelpers.loginRequired, single);
users.post('/create', create);
users.post('/auth', passport.authenticate('local'), auth);
app.js
var passport = require('passport');
app.use(cookieParser())
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(session({
secret: 'X+a1+TKXwd26mkiUUwqzqQ==',
resave:true,
saveUninitialized:true,
cookie:{secure:false}
}));
app.use(passport.initialize());
app.use(passport.session());
app.use(function (req, res, next) {
var allowedOrigins = ['http://localhost:3000']
res.header('Access-Control-Allow-Origin', allowedOrigins);
res.header( 'Access-Control-Allow-Headers', 'withCredentials, Access-Control-Allow-Headers, Origin, X-Requested-With, X-AUTHENTICATION, X-IP, Content-Type, Accept, Access-Control-Request-Method, Access-Control-Request-Headers');
res.header( 'Access-Control-Allow-Methods', 'GET, OPTIONS, HEAD, POST, PUT, DELETE');
res.header( 'Access-Control-Allow-Credentials', true);
next();
});
var routes = require('./routes');
app.use('/', routes);
front end http service
getData (url:string, data:any): Observable<any> {
var headers = new Headers({ 'Content-Type': 'application/json', withCredentials: true });
var options = new RequestOptions({ headers: headers });
return this.http.get(url,options)
.map((res: Response): data => res.json())
.catch(this.handleError);
}
The issue was on the front end I was not setting withCredentials to true in the correct location
var options = new RequestOptions({ headers: headers, withCredentials: true });
I have a suite of programs that are all under the same company and I am trying to develop a single login / authentication service that can persist through all of the programs. The idea is very micro-service oriented in which we will have one service to handle authentication and persist it as long as someone is in one of the programs. The issue is I need my other services to be able to access the same cookies across all of the domains and be able to send those cookies to the auth service for session verification. Please correct me if this is not the proper way to set up micro-services with a login/auth service.
For my front end (Angularjs):
service.login = function (obj, callback) {
$http.post(loginService + "login", obj, {
withCredentials: true
}).success(function (data) {
callback(data);
})
.error(function (data, status, headers) {
console.log(status);
});
};
For my server (Node, Express, Mongo):
var options = {
pfx: fs.readFileSync('company.pfx'),
passphrase: 'pass',
ca: [fs.readFileSync('gd1.crt'), fs.readFileSync('gd2.crt'), fs.readFileSync('gd3.crt')],
spdy: {
protocols: ['h2', 'spdy/3.1', 'http/1.1'],
plain: false,
'x-forwarded-for': true,
connection: {
windowSize: 1024 * 1024, // Server's window size
// **optional** if true - server will send 3.1 frames on 3.0 *plain* spdy
autoSpdy31: false
}
}
};
var server = spdy.createServer(options, app);
app.use(helmet());
app.use(cookieParser());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use("/static", express.static('static'));
app.use("/logins", express.static('logins'));
app.set('trust proxy', 1) // trust first proxy for production with a proxy server
app.use(session({
resave: false,
saveUninitialized: true,
genid: function (req) {
return uuid.v4() // use UUIDs for session IDs
},
name: "myToken",
secret: 'mysecret',
cookie: { secure: false, maxAge: (45 * 60000) }, // set secure to true when in production
store: new mongoStore({ url: 'mongodb://' + base + 'sessions' })
}));
app.use(function (req, res, next) {
res.header('Access-Control-Allow-Credentials', true);
res.header('Access-Control-Allow-Origin', req.headers.origin);//req.headers.origin);
res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,PATCH');
res.header('Access-Control-Allow-Headers', 'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version');
next();
});
Requesting:
app.get('/', function (req, res) {
var sess = req.session, token = req.cookies.myToken;
res.send('Hello World!');
});
To test this I have a virtual machine running on my system with the application deployed and then I am also running my localhost:/ application. From my understanding my cookies should remain the same between the two calls with the same session if I have CORS set up properly. Any help or suggestions?
Have you tried
res.header('Access-Control-Allow-Origin', '*.domain') ?
Basically a wildcard matching any subdomain under your main domain.