I have a server where users sign up by email. I want to allow connection in at most N devices, such as computer, phone and tablet. I want to discourage a user sharing credentials with many others, and so I want to logout all but the N most recent sessions when a user logs in.
I am using NodeJS, MongoDB, and Passport with a custom one-time password (otp) authentication strategy:
The user model file includes:
const mongoose = require('mongoose');
const UserSchema = new Schema({
// ...
});
UserSchema.methods.validateOtp = async function(otp) {
// ...
};
The users' routes file includes:
const express = require('express');
const router = express.Router();
const passport = require('passport');
router.post(
"/login",
passport.authenticate("user-otp", {
successRedirect: "/dashboard",
failureRedirect: "back",
})
);
passport.use('user-otp', new CustomStrategy(
async function(req, done) {
user = await User.findOne({req.body.email});
let check = await user.validateOtp(req.body.otp);
// more logic...
}
));
I found NodeJS logout all user sessions but I could not find the sessions collection in the database, even though I have two active sessions on it.
How can I log the user out of all but the N most recent sessions?
update
After the answer, I realize I left out code related to the session. The main script file includes:
const cookieParser = require('cookie-parser');
const passport = require('passport');
const session = require('cookie-session');
app.use(cookieParser("something secret"));
app.use(
session({
// cookie expiration: 90 days
maxAge: 90 * 24 * 60 * 60 * 1000,
secret: config.secret,
signed: true,
resave: true,
httpOnly: true, // Don't let browser javascript access cookies.
secure: true, // Only use cookies over https.
})
);
app.use(passport.initialize());
app.use(passport.session());
app.use('/', require('./routes/users'));
The module cookie-session stores data on the client and I don't think it can handle logging out all but the last N sessions, since there is no database on the server.
Are you sure you actually have a persistent session store currently? If you are not intentionally leaving out any middleware in your post then I suspect you do not.
The go-to for most development using express is express-session which needs to be added as its own middleware. In its default configuration, express-session will just store all sessions in memory though. Memory storage is not persistent through restarts and is not easy to interact with for any purpose other than storing session information. (like querying sessions by user to delete them)
I suspect what you will want to use is connect-mongodb-session as a session storage mechanism for express-session. This will store your sessions in mongodb in a 'sessions' collection. Here's some boilerplate to help you along.
Please excuse any minor bugs that may exist here, I am writing all of this code here without running any of it, so there could be small issues you need to correct.
const express = require('express');
const passport = require('passport');
const session = require('express-session');
const MongoDBStore = require('connect-mongodb-session')(session);
const app = express();
const router = express.Router();
// Initialize mongodb session storage
const store = new MongoDBStore({
uri: 'mongodb://localhost:27017/myDatabaseName',
// The 'expires' option specifies how long after the last time this session was used should the session be deleted.
// Effectively this logs out inactive users without really notifying the user. The next time they attempt to
// perform an authenticated action they will get an error. This is currently set to 1 hour (in milliseconds).
// What you ultimately want to set this to will be dependent on what your application actually does.
// Banks might use a 15 minute session, while something like social media might be a full month.
expires: 1000 * 60 * 60,
});
// Initialize and insert session middleware into the app using mongodb session storage
app.use(session({
secret: 'This is a secret that you should securely generate yourself',
cookie: {
// Specifies how long the user's browser should keep their cookie, probably should match session expires
maxAge: 1000 * 60 * 60
},
store: store,
// Boilerplate options, see:
// * https://www.npmjs.com/package/express-session#resave
// * https://www.npmjs.com/package/express-session#saveuninitialized
resave: true,
saveUninitialized: true
}));
// Probably should include any body parser middleware here
app.use(passport.initialize());
app.use(passport.session());
// Should init passport stuff here like your otp strategy
// Routes go here
So after you get cookies and sessions working, the next part is to have routes which are actually protected by your authentication. We're setting this up so that we know for sure that everything is working.
// Middleware to reject users who are not logged in
var isAuthenticated = function(req, res, next) {
if (req.user) {
return next();
}
// Do whatever you want to happen when the user is not logged in, could redirect them to login
// Here's an example of just rejecting them outright
return res.status(401).json({
error: 'Unauthorized'
});
}
// Middleware added to this route makes it protected
router.get('/mySecretRoute', isAuthenticated, (req, res) => {
return res.send('You can only see this if you are logged in!');
});
At this step you should check that if you are not logged in that you can't reach the secret route (should get error), and if you are logged in you can reach it (see the secret message). Logging out is the same as usual: req.logout() in your logout route. Assuming all is well now let's attack the actual issue, logging out everything except the 4 most recent sessions.
Now, for simplicity, I'm going to assume you are enforcing otp on every user. Because of this we can take advantage of the passport otp middleware you declared earlier. If you aren't then you may need do a bit more custom logic with passport.
// Connect to the database to access the `sessions` collection.
// No need to share the connection from the main script `app.js`,
// since you can have multiple connections open to mongodb.
const mongoose = require('mongoose');
const connectRetry = function() {
mongoose.connect('mongodb://localhost:27017/myDatabaseName', {
useUnifiedTopology: true,
useNewUrlParser: true,
useCreateIndex: true,
poolSize: 500,
}, (err) => {
if (err) {
console.error("Mongoose connection error:", err);
setTimeout(connectRetry, 5000);
}
});
}
connectRetry();
passport.use('user-otp', new CustomStrategy(
async function(req, done) {
user = await User.findOne({ req.body.email });
let check = await user.validateOtp(req.body.otp);
// Assuming your logic has decided this user can login
// Query for the sessions using raw mongodb since there's no mongoose model
// This will query for all sessions which have 'session.passport.user' set to the same userid as our current login
// It will ignore the current session id
// It will sort the results by most recently used
// It will skip the first 3 sessions it finds (since this session + 3 existing = 4 total valid sessions)
// It will return only the ids of the found session objects
let existingSessions = await mongoose.connection.db.collection('sessions').find({
'session.passport.user': user._id.toString(),
_id: {
$ne: req.session._id
}
}).sort({ expires: 1}).skip(3).project({ _id: 1 }).toArray();
// Note: .toArray() is necessary to convert the native Mongoose Cursor to an array.
if (existingSessions.length) {
// Anything we found is a session which should be destroyed
await mongoose.connection.db.collection('sessions').deleteMany({
_id: {
$in: existingSessions.map(({ _id }) => _id)
}
});
}
// Done with revoking old sessions, can do more logic or return done
}
));
Now if you login 4 times from different devices, or after clearing cookies each time, you should be able to query in your mongo console and see all 4 sessions. If you login a 5th time, you should see that there are still only 4 sessions and that the oldest was deleted.
Again I'll mention I haven't actually tried to execute any of the code I've written here so I may have missed small things or included typos. Please take a second and try to resolve any issues yourself, but if something still doesn't work let me know.
Tasks left to you:
Your mongo query performance will be sub-par if you do not add an index for session.passport.user to the sessions collection. You should add an index for that field, e.g. run db.sessions.createIndex({"session.passport.user": 1}) on the Mongo shell (see docs). (Note: although passport is a sub-document of the session field, you access it like a Javascript object: session.passport.)
You should probably also logout other sessions if a password reset is
executed.
You should delete the session from the collection when calling req.logout().
To be friendly to the user, you could add a message to the revoked sessions to display when the user tries to access the content from a previous device. The same goes for expired sessions. And you could delete those sessions to keep the collection small.
The module express-session stores a cookie in the user's browser even without logging in. To be compliant with GDPR in Europe, you should add a notice about cookies.
Implementing the change from cookie-session (stored in the client) to express-session will log out all previous users. To be friendly to the user, you should warn them ahead of time and make sure you make all the changes at once, instead of trying multiple times and them getting exasperated at having to log in multiple times.
Related
I am trying to make the login/logout functionality of two category, admin & employee.
And When used app.use(session()) session will be available to every routes. And that is great. But when I want to logout lets say admin using req.session.destroy(). It logs out but the entire session is gone including admin as well as the employee. And that's not i want. I want to destroy only admin related session for admin logout and employee related session for employee logout. So, how can I do this?
And I am new to authentication and authorization. Do let me know what's the best practices using sessions, or is it better to JWT or anything which will help me be better at it.
For this related question.
my app.js
// session
app.use(
session({
secret: process.env.SECRET,
resave: false,
saveUninitialized: false,
store,
cookie: {
maxAge: 20000,
secure: false,
},
})
);
app.use("/api/admin", adminRoutes);
app.use("/api/employee", employeeRoutes);
app.get("/api", (req, res) => {
res.send("Api is running");
});
and when the api/admin/login route is called this controller is called,
const adminLoginController = asyncHandler(async (req, res) => {
console.log("I ran");
const { pass } = req.body;
if (someDBCheckHere) {
req.session.adminAuthenticated = true;
req.session.admin = { pass: pass };
res.send("success");
} else {
res.status(401).send({ message: "Login Failed" });
console.log("failure");
}
});
I'm not really sure why you would destroy the session. Did you read it somewhere that tell you to do so?
So from the behavior, you can see the session is intended to live, not to be destroy :D
To "logout" a user, you just set set the cookie to an expire date
Please confirm whether my interpretation of your requirement is correct:
Your users can log on in two roles, with different passwords per role. And they might even be logged on in both roles simultaneously (either by giving two passwords, or because the admin role includes the employee role).
You could achieve this by having only one session, with attributes req.session.employeeAuthenticated and req.session.adminAuthenticated. After validating a password, you would set one (or both) of these attributes, and users could also "log out from the admin role", after which you would simply set req.session.adminAuthenticated = false but keep the session.
The first of the adminRoutes must then validate that the current user indeed has the admin role:
function(req, res, next) {
if (req.session.adminAuthenticated) next();
else res.status(403).end("Forbidden for non-admins");
}
(and likewise in employeeRoutes).
Only when the user logs out completely would you call req.session.destroy().
const express = require("express");
const app = express();
var i = new Number;
i=0;
app.get("/", function(req, res){
i++
console.log(i);
});
app.listen(8080);
I created a very small node js project. I have a problem. when I create a variable like above, it doesn't evaluate for each user separately. that is, when a user requests a get, I want it to be 1 each time.
Sample
my problem is that when a jack user enters a site, if he doesn't log out, someone who enters the site's home page from another device enters his account with jack.
how can I do that?
The simplest answer for your question is to simply declare and increment the variable inside the function passed to app.get, but I'm going to assume that you would like a situation where, for a given user's series of requests, the number will increment.
The simplest way to do this is using a server side session, which is provided by the express-session library. Additionally, in order for this to work, you need to call res.end() in order to send the cookie associated with the server session back to the user's browser. More information on sessions generally can be found here.
Below is code to replicate the intent of what you have there, but incrementing for each request from a unique browser instance (identified by the same cookie value associated with the server session):
const express = require('express');
const session = require('express-session');
const app = express();
app.use(session({
resave: false,
saveUninitialized: true,
secret: 'secret',
cookie: {
maxAge: 60000
}
}));
app.get('/', function(req, res){
if (!req.session.value) {
req.session.value = 0;
}
req.session.value++;
console.log(req.session.value);
res.end();
});
app.listen(8080);
I've only been working with express and react for a little while. The first app I worked was set up pretty well. The new one I am on is set up well too, but there is a little problem with session when I make a code change.
When I make a change to the code, save, and look at the app, I need to re-login EVERY TIME.
I've looked at a bunch of blogs and SO posts, and most seem to be trying to solve session problems in a prod environment with stores like Redis, etc.
It would seem to me, that express-session should be suitable to do this in a development environment. No?
This is what index.js looks like:
/*
Configure the Google strategy used by Passport.
OAuth 2.0-based strategies require a `verify` function which receives the
credential (`accessToken`) for accessing the Google API on the user's
behalf, along with the user's profile. The function must invoke `callback`
with a user object, which will be set at `req.user` in route handlers after
authentication. ie. callback(err, user)
*/
passport.use(new GoogleStrategy({
clientID: `${process.env.GOOGLE_AUTH_CLIENT_ID}`,
clientSecret: `${process.env.GOOGLE_AUTH_CLIENT_SECRET}`,
callbackURL: `${process.env.GOOGLE_AUTH_CALLBACK_URL}`
},
(accessToken, refreshToken, idToken, profile, callback) => {
const onSuccess = (res) => {
/*
res.data = {
refresh_jwt: refresh token,
api_jwt: access token
}
*/
callback(null, res.data);
}
const onError = (err) => {
callback(new Error('Google OAuth Failed'), null);
}
getOauthData('google', idToken.id_token, onSuccess, onError);
})
);
/*
Configure Passport authenticated session persistence.
In order to restore authentication state across HTTP requests, Passport needs
to serialize users into and deserialize users out of the session.
*/
passport.serializeUser((user, callback) => {
const sessionUser = {
refresh_jwt: user.refresh_jwt,
api_jwt: user.api_jwt
};
callback(null, sessionUser);
});
passport.deserializeUser((sessionUser, callback) => {
verifyIdentity(sessionUser.api_jwt, sessionUser.refresh_jwt, callback);
});
/* Start app */
var app = express();
app.disable('x-powered-by');
/* -- Application-level middleware -- */
/* Configure session */
app.set("trust proxy", 1);
var sess = {
secret: `${process.env.COOKIE_IDENT_SECRET}`,
name: "ident",
proxy: true,
resave: false,
saveUninitialized: true,
cookie: {
expires: 604800000, // 1 week
secure: true
}
}
app.use(session(sess));
/* Initialize Passport and restore authentication state, if any, from the session. */
app.use(passport.initialize());
app.use(passport.session());
From docs
Session data is not saved in the cookie itself, just the session ID. Session data is stored server-side.
The default server-side session storage, MemoryStore
So everytime you restart app, the memory is going to be wiped along with sessions stored. If you need persistent session, store it in some persistent store, db etc, using one of the Session Stores available under Compatible Session Stores in the same link.
An example from connect-mongo using mongodb as persistent storage:
const session = require('express-session');
const MongoStore = require('connect-mongo')(session);
app.use(session({
secret: 'foo',
store: new MongoStore(options)
}));
I am trying to access my session token from other routes after setting it in a route. I am currently unsuccessful. Following the relevant code of the three files.
server.js: It calls the routes thermostats, login and also sets session token.
var session = require('express-session');
var app = express();
app.use(session({secret: 'keyboard cat',cookie: { secure: true }}))
var router = express.Router();
var thermostats = require('./api/routes/thermostats')(router, app, session);
require('./api/routes/login')(router, app, session, thermostats);
login.js: When the user goes to localhost:3000/login/, the login token needs to be saved in the session
module.exports = function(router,app, session, thermostats){
router.get('/login/', function(req, res) {
list(req, res) //response of this function has session which needs to be saved.
console.log(res.session)
app.use(session(res.session)) //trying to save the res.session as session token
});
}
thermostat.js: Needs to access the session token before can display any information.
module.exports = function(router,app){
router.get('/thermostats/', function(req, res) {
console.log(req.session) //Set to default/null values and not the updated session values
});
}
It might be something small but I cannot figure out the issue. I would appreciate any help.
Thank you
Express-session should automatically save the session, based on the configuration.
Looking at the 'resave' config option in the express-session docs:
resave
Forces the session to be saved back to the session store, even if the session was never modified during the request. Depending
on your store this may be necessary, but it can also create race
conditions where a client makes two parallel requests to your server
and changes made to the session in one request may get overwritten
when the other request ends, even if it made no changes (this behavior
also depends on what store you're using).
This is by default, true, so it should already start working without you needing to add app.use(session(res.session).
Edit: You will be able to save to the session by adding fields to the req.session object:
router.get('/login/', function(req, res) {
getDataFromExternalApi(req, function(err, apiResponse) {
var data = apiResponse.data;
req.session.data = data;
// if resave option is true, this should automatically save to the session store after this request is done.
});
});
Generally, you shouldn't be using app.use in your request handlers. Those are generally reserved for setting up the server, as it defines what middleware express uses.
I set up session management in my node js/ express js website successfully. I stores session data in mongo db. I want the session to be valid for the users who log in for a couple of weeks. The code is as follows:
var cookieParser = require('cookie-parser');
var session = require('express-session');
var MongoStore = require('connect-mongo')(session);
app.use(cookieParser());
app.use(session({
store: new MongoStore({ mongoose_connection: db }),
secret: 'cookie_secret',
cookie: { maxAge: null }
}));
It works fine for normal users, but my problem is with web crawlers such as google bots and facebook bots. I still want them to crawl my website but I don't want their sessions to be stored in my mongo db. It's taking up lots of space and storage is increasing daily which costs me money.
How to selectively choose which sessions to be stored in the db. I can check for req.headers['user-agent'], but where to use it in my code? How to tell express-session not to store session sometimes?
You can use the session middleware conditionally, based on the User-Agent header. A simple example:
var sessionMiddleware = require('express-session')({
...configuration here...
});
app.use(function(req, res, next) {
var ua = req.get('user-agent');
// If the User-Agent header contains the string "Googlebot",
// skip the session middleware.
if (/Googlebot/.test(ua)) {
req.session = {}; // perhaps a bit too simple?
return next();
}
return sessionMiddleware(req, res, next);
});
It would depend on your actual use of req.session if the code above works, or if you need to mock req.session a bit better (for instance, if you use any of the req.session methods in your code,
you may need to mock those too).