nodejs - Share session between express and socket.io (authentication with passport) - node.js

I am currently creating a webapplication using node.js.
When a user enters the side, a connection via socket.io is built up.
I want to block multiple connections. A user should just be able to connect one time, if he opens the page in another window, i want to display a message like "already connected, please close all other tabs", or something like that.
To do so, i create sessions for the users using "express-session". this sessions are shared with socket.io by using following code.
I am not sure, if i am doing it correctly. I have looked at some tutorials and they are all sharing the sessions in a similar way. But sometimes, i think, the session data is overridden, when i change and save a value in the socket.io-session.
var sessionStoreClient = Redis.createClient({
port: DBs.sessionStore_redis.port,
host: DBs.sessionStore_redis.host,
password: DBs.sessionStore_redis.password,
db: DBs.sessionStore_redis.database
});
var sessionStore = new RedisStore({
unset: "destroy",
client: sessionStoreClient
});
var sessionInstance = expressSession(
{
key: sessionsKey,
store: sessionStore,
secret: sessionsSecret,
unset: "destroy"
}
);
app.use(sessionInstance);
app.use(passport.initialize());
app.use(passport.session());
to share the sessions with socket.io, i am using following code
var passportSocketIo = function(app,sessionInstance, passport){
return function(socket, next){
var req = socket.request;
sessionInstance(req,req.res,
function () {
if (!req.session) {
return next(new Error("Not authorized"), false);
}
req.updateSessionValue = function (propertyName, propertyValue, callback) {
if(!req.session){
console.log("unauthorized session modification");
return;
}
req.session.reload(()=>{
req.session[propertyName] = propertyValue;
req.session.save();
if(callback)
callback(propertyName, propertyValue,req.session);
});
};
if(req.session.passport && req.session.passport.user) {
passport.deserializeUser(req.session.passport.user, req, function (err, user) {
req.user = user;
req.user.logged_in = true;
return next(null, true);
});
}else {
return next(null, true);
}
}
);
};
};
var nsp = io.of(Packages.NAMESPACES.LOBBY);
nsp .use(options.sessionMiddleware);
to edit the values of the sessions, inside of the socket.io-listeners, i use the function, which is set to the request in the code above.
_onConnectionReceived(socket) {
socket.request.updateSessionValue("isConnected",true);
}
and when the user disconnects from the socket:
_onDisconnect(socket) {
socket.request.updateSessionValue("isConnected",false);
}
For login and authentication I am currently using passport.js.
When the user logs out, he is redirected to the samepage, but as "guest"-user. For loggin out, I am using following code:
router.get('/logout',function(req, res,next){
req.logout();
req.session.destroy(
function (err) {
res.render('logout',{});
}
);
});
The problem there is, that after the session is destroyed and the user is redirected, the socket.io disconnect event is fired. Even after the reload inside of the "onDisconnected" event-listener, the session seems to exist, after the value is modified, the session seems to get recreated somehow. Also it feels like, that the session values are sometimes just overwritten - i have not exactly found out why.
Can anyone explain what i am doing wrong, or if there is a better way to share and modify the sessions? Is the "reload" function of the session really reloading its data from the sessionStore?
Another approach could be to just save the session/user ids in a hashset, and check if there is already a connection open (without saving it in the session it self), but i am curious, if my approach is correct, becaus i need the sharing and modifing of the sessions anyways.

Related

Not being able to retrieve data saved in cookie-session

I am trying to make a dynamic website in Cloud Functions for Firebase.
However, I am stuck at this point.
What I am trying to achieve is that I want to save particular form details and then push them to the DB if the user is logged in. If the user is not logged in I would like to save those forms details somewhere and then redirect the user to login. When he does login I save the form details under his userid.
Now I thought of using firebase-cookie-session for this. Since it's the only one allowed in firebase cloud functions.
However, my problem is that when the user is redirected to login. I no longer have access to the session in that request.
This is how I approached this:
I setup my express app:
var session = require('firebase-cookie-session');
app.use(session({
keys: ['cookie_secret'],
name: 'session',
sameSite:true,
proxy: true,
resave: true,
saveUninitialized: true,
maxAge: 360*12*24*36*36
}));
then in post I save data in session: -> console.log(req.session.packageDetails) works perfectly
homepageRouter.post("/save-form-details", function (req, res, next) {
const packageDetails = JSON.parse(JSON.stringify(req.body));
firebaseController.isUserSignedIn().then(signedinUser => {
if(signedinUser){
firebaseController.getCurrentUser().then(currentUser => {
firebaseController.putPackageToUser(currentUser, packageDetails).then(success => {
return success;
});
}).catch(errorMessage => {
console.log(errorMessage);
});
} else {
req.session.userSignedIn = false;
req.session.packageIncluded = true;
req.session.packageDetails = packageDetails;
console.log('Hello Hello Hello Hello Hello Hello');
console.log(req.session.packageDetails);
res.send({
userSignedIn: false,
redirectTo: '/signup-login'
});
}
console.log('User Status: ' + signedinUser);
console.log('Server Side: ' + JSON.parse(JSON.stringify(req.body)));
});
});
After the redirection from the above to below the session goes to undefined, No idea why.
signupLoginRouter.post("/login", function (req, res, next) {
const accountJSON = JSON.stringify(req.body);
let account = JSON.parse(accountJSON);
console.log('HEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEELP');
console.log(req.session.packageDetails);
}
I think I either setup something wrong or understood something wrong. If anyone can help. Thay would be appreciated

express-session: Why is redirect executed before my session info is set?

Yet another "I've just started learning node.js/express and now I stuck" person here. I've tried looking everywhere for answers but I've reached a dead end. This is my first Stackoverflow question, so please tell me if I have done something wrong or unconventional.
I'm trying to create a POST request that saves a User-object to an express-session (on MongoDB), and redirects you to a URL that handles your session information.
The problem is, that the user is redirected before the header is set, and I am given the following:
Error: Can't set headers after they are sent.
Here is my code. I know it's a lot.. Im sorry.
router.post()
// Handler for POST requests from root page (/)
router.post('/', function(req, res) {
console.log("Router: A POST request for: \"/\" has been called!");
var username = req.body.username;
var password = req.body.password;
// Connect to database
mongoose.connect(dbAddress);
var db = mongoose.connection;
// Report if error in connection
db.on('error', console.error.bind(console, 'Database: Connection error..'));
// Callback function on 'open'
db.once('open', function() {
console.log("Database: Connection successful!");
console.log("Database: User supplied username: " + username);
console.log("Database: User supplied password: " + password);
// Find User object with a userId of req's username
User.findOne({ 'userId' : username.toUpperCase() }, function(err, userObj) {
if (err)
return console.err(err);
// Disconnect when done retrieving user object
mongoose.disconnect();
if ( userObj ) {
console.log("Database: Returned password from MongoDB:");
console.log(userObj.password);
var db_password = userObj.password;
if (password === db_password) {
console.log("Database: User Authenticated");
// Set 'req.session.user' as cookie
req.session.user = userObj;
// Redirect to homepage, with new cookie set
res.redirect('/');
} else { // If passwords don't match
res.render('index', {
showError: true
});
}
} else { // If userObj is null
res.render('index', {
showError: true
});
}
});
});
});
Note the 'req.session.user = userObj' part. Here I am trying to set 'user' in the session to the user object retrieved from MongoDB. In the next line, I am redirecting the user back to the GET request handler for '/' which handles the user based on the session information.
For some reason, these aren't happening in order. The GET request handler doesn't find req.session.user.
I understand that node.js is all about asynchronisation, but from other examples I've found online, this is supposed to work. What am I missing here?
You could put your redirect inside a callback after the session is saved e.g:
...
// Set 'req.session.user' as cookie
req.session.user = userObj;
req.session.save(function(err) {
// session saved
res.redirect('/')
})
...
Hopefully this will make sure that the user is only redirected after the session is saved.
Note: Make sure you hash your password with something like Bcrypt or pbkdf2.

trying to use node.js and mysql to connect to different database per user - preferably using a pool

I'm fairly new to nodejs, knex, bookshelf etc and am currently developing a web app that would connect to a different mysql database based on which user is logged in.
The first connection works fine, I then log out and in the sign out code I do knex.destroy().
When I go to log back in I get an
Unhandled rejection Error: There is no pool defined on the current client
It seems that knex doesn't recreate a pool once it has been destroyed even if it gets reinitialized.
Has anyone tried this and know how to do it?
I have tried initialising without a database and adding one when the user logs in but the connection doesn't seem to then connect to the database. I've tried instantiating a new connection with the database without destroying the previous, which results in the user using the first user's database. Destroying the connection removes the MySQL connections and even establishes a new connection with the right database but apparently no pool.
From reading it looks like knex wasn't designed to do this but surely there must be a way to instantiate a new connection with a new pool?
I'm using passport-local
code snippets follow
passport.use(new LocalStrategy({
usernameField: 'email',
passwordField: 'password'
},
function(email, password, done) {
new userModel.User({email: email}).fetch().then(function(data) {
var user = data;
if(user === null) {
return done(null, false, {message: 'Invalid username or password'});
} else {
user = data.toJSON();
if(!bcrypt.compareSync(password, user.password)) {
return done(null, false, {message: 'Invalid username or password'});
} else {
ctrl = new DB();
ctrl.connect(user.db);
dbModel = require('../server/models/dbModel');
return done(null, user);
}
}
});
}));
DB.js
function DB(){
}
DB.prototype.connect = function (db){
if (db !== "crm" && db !== null){
db = "crm-" + db;
}
DB.knex = require('knex')({
client: 'mysql',
connection:{
host: 'localhost', // your host
user: MYSQL_USR, // your database user
password: MYSQL_PWD, // your database password
database: db,
charset: 'UTF8_GENERAL_CI' //,
// debug : true
}
});
DB.bookshelf = require('bookshelf')(DB.knex);
};
DB.prototype.destroy = function (){
DB.knex.destroy().then(console.log("destroyed"));
};
module.exports = DB;
please help!
let me know what other code you may need.
Has anyone ever done this and if so how? Please, I have not had or found any answers.
I'm not sure if this will help or not, but I got this stripped down version to work. I'm not really familiar with knex, but I created two very simple databases and I was able to connect to both and get simple output.
index.js
var DB = require('./DB');
function userConnect(db_name){
var db = new DB();
var knex = db.connect(db_name);
knex.select('color').from('test').then(function(result){
console.log(result);
knex.destroy();
});
}
userConnect('db_one');
userConnect('db_two');
DB.js
function DB(){
}
DB.prototype.connect = function (db){
return require('knex')({
client: 'mysql',
connection:{
host: MYSQL_HOST, // your host
user: MYSQL_USER, // your database user
password: MYSQL_PASSWORD, // your database password
database: db
}
});
};
module.exports = DB;
I'm not sure this is really what you want to do. What if you have two different users logged in at the same time. Both need to be able to access their connection. Having different databases means you probably should ether turn off pooling all together. In the config you can set pool: { min: 0, max: 0}. Or the other way would be you need to hold onto a database for each user currently logged-in and then once the logout or after some time-period you should release the database not destroy it.
You should really think hard before you use a different database for each user. This can make for a bunch of work down the road when you have to migrate 1000's of databases.

Authentication on Server side routes in Meteor

What is the best way (most secure and easiest) to authenticate a user for a server side route?
Software/Versions
I'm using the latest Iron Router 1.* and Meteor 1.* and to begin, I'm just using accounts-password.
Reference code
I have a simple server side route that renders a pdf to the screen:
both/routes.js
Router.route('/pdf-server', function() {
var filePath = process.env.PWD + "/server/.files/users/test.pdf";
console.log(filePath);
var fs = Npm.require('fs');
var data = fs.readFileSync(filePath);
this.response.write(data);
this.response.end();
}, {where: 'server'});
As an example, I'd like to do something close to what this SO answer suggested:
On the server:
var Secrets = new Meteor.Collection("secrets");
Meteor.methods({
getSecretKey: function () {
if (!this.userId)
// check if the user has privileges
throw Meteor.Error(403);
return Secrets.insert({_id: Random.id(), user: this.userId});
},
});
And then in client code:
testController.events({
'click button[name=get-pdf]': function () {
Meteor.call("getSecretKey", function (error, response) {
if (error) throw error;
if (response)
Router.go('/pdf-server');
});
}
});
But even if I somehow got this method working, I'd still be vulnerable to users just putting in a URL like '/pdf-server' unless the route itself somehow checked the Secrets collection right?
In the Route, I could get the request, and somehow get the header information?
Router.route('/pdf-server', function() {
var req = this.request;
var res = this.response;
}, {where: 'server'});
And from the client pass a token over the HTTP header, and then in the route check if the token is good from the Collection?
In addition to using url tokens as the other answer you could also use cookies:
Add in some packages that allow you to set cookies and read them server side:
meteor add mrt:cookies thepumpinglemma:cookies
Then you could have something that syncs the cookies up with your login status
Client Side
Tracker.autorun(function() {
//Update the cookie whenever they log in or out
Cookie.set("meteor_user_id", Meteor.userId());
Cookie.set("meteor_token", localStorage.getItem("Meteor.loginToken"));
});
Server Side
On the server side you just need to check this cookie is valid (with iron router)
Router.route('/somepath/:fileid', function() {
//Check the values in the cookies
var cookies = new Cookies( this.request ),
userId = cookies.get("meteor_user_id") || "",
token = cookies.get("meteor_token") || "";
//Check a valid user with this token exists
var user = Meteor.users.findOne({
_id: userId,
'services.resume.loginTokens.hashedToken' : Accounts._hashLoginToken(token)
});
//If they're not logged in tell them
if(!user) return this.response.end("Not allowed");
//Theyre logged in!
this.response.end("You're logged in!");
}, {where:'server'});
I think I have a secure and easy solution for doing this from within IronRouter.route(). The request must be made with a valid user ID and auth token in the header. I call this function from within Router.route(), which then gives me access to this.user, or responds with a 401 if the authentication fails:
// Verify the request is being made by an actively logged in user
// #context: IronRouter.Router.route()
authenticate = ->
// Get the auth info from header
userId = this.request.headers['x-user-id']
loginToken = this.request.headers['x-auth-token']
// Get the user from the database
if userId and loginToken
user = Meteor.users.findOne {'_id': userId, 'services.resume.loginTokens.token': loginToken}
// Return an error if the login token does not match any belonging to the user
if not user
respond.call this, {success: false, message: "You must be logged in to do this."}, 401
// Attach the user to the context so they can be accessed at this.user within route
this.user = user
// Respond to an HTTP request
// #context: IronRouter.Router.route()
respond = (body, statusCode=200, headers) ->
this.response.statusCode statusCode
this.response.setHeader 'Content-Type', 'text/json'
this.response.writeHead statusCode, headers
this.response.write JSON.stringify(body)
this.response.end()
And something like this from the client:
Meteor.startup ->
HTTP.get "http://yoursite.com/pdf-server",
headers:
'X-Auth-Token': Accounts._storedLoginToken()
'X-User-Id': Meteor.userId()
(error, result) -> // This callback triggered once http response received
console.log result
This code was heavily inspired by RestStop and RestStop2. It's part of a meteor package for writing REST APIs in Meteor 0.9.0+ (built on top of Iron Router). You can check out the complete source code here:
https://github.com/krose72205/meteor-restivus
Because server-side routes act as simple REST endpoints, they don't have access to user authentication data (e.g. they can't call Meteor.user()). Therefore you need to devise an alternative authentication scheme. The most straightforward way to accomplish this is with some form of key exchange as discussed here and here.
Example implementation:
server/app.js
// whenever the user logs in, update her apiKey
Accounts.onLogin(function(info) {
// generate a new apiKey
var apiKey = Random.id();
// add the apiKey to the user's document
Meteor.users.update(info.user._id, {$set: {apiKey: apiKey}});
});
// auto-publish the current user's apiKey
Meteor.publish(null, function() {
return Meteor.users.find(this.userId, {fields: {apiKey: 1}});
});
lib/routes.js
// example route using the apiKey
Router.route('/secret/:apiKey', {name: 'secret', where: 'server'})
.get(function() {
// fetch the user with this key
// note you may want to add an index on apiKey so this is fast
var user = Meteor.users.findOne({apiKey: this.params.apiKey});
if (user) {
// we have authenticated the user - do something useful here
this.response.statusCode = 200;
return this.response.end('ok');
} else {
// the key is invalid or not provided so return an error
this.response.statusCode = 403;
return this.response.end('not allowed');
}
});
client/app.html
<template name="myTemplate">
{{#with currentUser}}
secret
{{/with}}
</template>
Notes
Make /secret only accessible via HTTPS.
While it's very likely that the user requesting /secret is currently connected, there is no guarantee that she is. The user could have logged in, copied her key, closed the tab, and initiated the request sometime later.
This is a simple means of user authentication. I would explore more sophisticated mechanisms (see the links above) if the server-route reveals high-value data (SSNs, credit cards, etc.).
See this question for more details on sending static content from the server.
I truly believe using HTTP headers are the best solution to this problem because they're simple and don't require messing about with cookies or developing a new authentication scheme.
I loved #kahmali's answer, so I wrote it to work with WebApp and a simple XMLHttpRequest. This has been tested on Meteor 1.6.
Client
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
// Skipping ahead to the upload logic
const xhr = new XMLHttpRequest();
const form = new FormData();
// Add files
files.forEach((file) => {
form.append(file.name,
// So BusBoy sees as file instead of field, use Blob
new Blob([file.data], { type: 'text/plain' })); // w/e your mime type is
});
// XHR progress, load, error, and readystatechange event listeners here
// Open Connection
xhr.open('POST', '/path/to/upload', true);
// Meteor authentication details (must happen *after* xhr.open)
xhr.setRequestHeader('X-Auth-Token', Accounts._storedLoginToken());
xhr.setRequestHeader('X-User-Id', Meteor.userId());
// Send
xhr.send(form);
Server
import { Meteor } from 'meteor/meteor';
import { WebApp } from 'meteor/webapp';
import { Roles } from 'meteor/alanning:roles'; // optional
const BusBoy = require('connect-busboy');
const crypto = require('crypto'); // built-in Node library
WebApp.connectHandlers
.use(BusBoy())
.use('/path/to/upload', (req, res) => {
const user = req.headers['x-user-id'];
// We have to get a base64 digest of the sha256 hashed login token
// I'm not sure when Meteor changed to hashed tokens, but this is
// one of the major differences from #kahmali's answer
const hash = crypto.createHash('sha256');
hash.update(req.headers['x-auth-token']);
// Authentication (is user logged-in)
if (!Meteor.users.findOne({
_id: user,
'services.resume.loginTokens.hashedToken': hash.digest('base64'),
})) {
// User not logged in; 401 Unauthorized
res.writeHead(401);
res.end();
return;
}
// Authorization
if (!Roles.userIsInRole(user, 'whatever')) {
// User is not authorized; 403 Forbidden
res.writeHead(403);
res.end();
return;
}
if (req.busboy) {
// Handle file upload
res.writeHead(201); // eventually
res.end();
} else {
// Something went wrong
res.writeHead(500); // server error
res.end();
}
});
I hope this helps someone!
Since Meteor doesn't use session cookies, client must explicitly include some sort of user identification when making a HTTP request to a server route.
The easiest way to do it is to pass userId in the query string of the URL. Obviously, you also need to add a security token that will prove that the user is really who the claim they are. Obtaining this token can be done via a Meteor method.
Meteor by itself doesn't provide such mechanism, so you need some custom implementation. I wrote a Meteor package called mhagmajer:server-route which was thoroughly tested. You can learn more about it here: https://blog.hagmajer.com/server-side-routing-with-authentication-in-meteor-6625ed832a94

express.js not saving session data on nodeunit tests

Update at bottom!
My node.js server uses express.js to manage sessions. Upon login, I store some user information in req.session. I have a logout endpoint that simply deletes the user data from req.session before sending its response.
With every request the user makes, I use authentication middleware to make sure there is still user data in the session, so deleting user data in the session object should fail any subsequent authentication.
To test my server, I have a nodeunit test that logs in, calls a few endpoints, logs out, and then attempts to call another endpoint. I would expect the last endpoint to fail in authentication because I previously blew away user data. Instead, when I make the last call, my user data is still there. It's as if the logout call that deleted it was not written back into the session store.
Here's my app.js:
app.use(express.cookieParser('secretPassword'));
app.use(express.cookieSession({key: 'someKey'}));
...
app.get('/logout', accounts.logout);
app.get('/account', auth.authenticateSession, accounts.show);
auth.js:
exports.authenticateSession = function(req, res, next) {
if (!req.session.user) {
return res.json(401, {
error: 'Access denied. You must be logged in to make this request.'
});
}
...
}
accounts.js:logout:
exports.logout = function(req, res) {
req.session.user = null;
res.send('Logged out');
};
Unit tests:
step1_logIn : function(test) {
var postData = qs.stringify({
accountname: 'testAcct',
accountpassword: 'hello'
});
ct.postAndCall('/login', null, postData, function(resData, res) {
myCookie = res.headers['set-cookie'];
test.ok(res.statusCode === 200);
test.done();
});
},
step2_logout : function(test) {
ct.getAndCall('/logout', myCookie, function(data, res) {
test.ok(data === 'Logged out.');
test.ok(res.statusCode === 200);
test.done();
});
},
step3_ensureLoggedOut: function(test) {
ct.getAndCall('/account', myCookie, function(data, res) {
test.ok(res.statusCode === 401);
test.done();
});
}
When the tests run, execution goes through logout successfully, then into authenticateSession on the call to /account and at this point, req.session.user still exists! Why!?
Is my cookie store giving me stale data?
Is there a way to force express to save my session data manually, or do I just modify the req.session object and trust that it's going to save it?
Update:
It looks like this problem is directly related to the app middleware surrounding cookies. When I use app.use(express.session()) instead of app.use(express.cookieSession(...)), the session data is properly blown away and my tests pass.
I figured it out. Apparently express.cookieSession(...) is meant to be a set-once type of storage. Subsequent requests will have access to the session data that was initially set, but changing the req.session object won't save back new session data.
To fix the problem, I switched over to use express.session(...) which uses a server-side store for session vars.

Resources