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().
Related
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.
I am using Passport-Google-OAuth2.0 and Express-session to authenticate. Use connect-redis to store session,
// app.ts
const RedisStore = connectRedis(session)
export const app = express()
app.set('trust proxy', 1)
export const cookieOptions = {
maxAge: +COOKIE_MAX_AGE,
httpOnly: true,
secure: isProduction,
signed: true,
}
app.use(
session({
store: new RedisStore({ client: redis }),
secret: env.SESSION_SECRET,
resave: false,
saveUninitialized: true,
cookie: env.cookieOptions,
})
)
app.use(passport.initialize())
app.use(passport.session())
Log in works, but it cannot log out. This is my attempt:
// Works
router.get('/auth/google', passport.authenticate('google', { scope: ['profile'] }))
// Works
router.get(
'/auth/google/callback',
passport.authenticate('google', { failureRedirect: '/signin' }),
async function (_req, res, _next) {
res.redirect('/')
}
)
// Not working
router.get('/auth/signout', function (req, res) {
req.session.destroy(function (err) {
if (err) console.log(` >> sess.des:`, err)
req.logout()
res.clearCookie('connect.sid')
// res.clearCookie('connect.sid', cookieOptions) // This throws 500 error
res.redirect('/outed')
})
})
When I go to http://localhost:8000/auth/signout, it redirects to /outed. I expected it to revoke my authentication state and have to login again.
So, I go to http://localhost:8000/auth/google to log in again, but it keeps redirecting me back to authenticated successful route /.
How do I log out properly with Passport and Redis session?
connect-redis: https://github.com/tj/connect-redis
express-session: https://github.com/expressjs/session
passport-google-oauth2: https://github.com/jaredhanson/passport-google-oauth2
First off, just use req.logout() by itself. Don't call req.session.destroy() and then inside of that try to call req.logout() (as the code in your question currently shows).
Then, when you go to your /auth/google google route the very first time, it will walk through an oauth session and you will have to confirm with Google that you are permitting this site to use your Google login via oauth.
When you then logout of your app, it is just killing your app login. It doesn't kill the Google login in that browser. That still remains. So, if you then go back to /auth/google, it just seamlessly (and without your further approval or participation) logs you back in as the currently logged in Google user. Google sees that you've previously approved this app to use your Google login and sees that the current browser is still logged into Google so it just approves the login without asking you again.
If you actually want your app to be able to login as a different Google user (or any other service using a similar mechanism), then you have log this browser out of Google itself or log in to Google itself as a different user or within Google revoke that app's permission to use the Google login. That will truly start you back at ground zero where you can login as a different Google user.
FYI, if you use your Google login with stackoverflow, you can experiment with this and see the very same behavior.
To capture the issue - I am using youtube's data API OAuth for authenticating users and authorizing my app. to keep track of users and their tokens I set up a mongo dB to store sessions as well as app data (users metadata and tokens included). the problem I have is that once a user had authenticated the app, I can see the session data properly stored in the session store, including user id, but when I make consecutive calls to my backend - the user is not logged in.
I'll describe the flow of what I did and then add some code snippets to make it a bit more clear (hopefully):
The client initiates the OAuth flow by redirecting to the https://www.googleapis.com/auth/youtube with all required parameters. once the user had authorized the app - the redirection URI (specified in google API dashboard) redirect the result to my backend.
on my backend, I use express-session and passport js with the passport-youtube-v3 strategy. as mentioned, the login/signup part seems to be working fine. I can see the a new user is registered in my users' collection and the session stored includes that user id. I can also see the session is stored in a cookie on the client. To the best of my understanding - once a consecutive API call had been initiated - the session cookie should be included in the request and passport should authenticate the user. however, when I guard any endpoint with passport.authenticate('youtube') or a simple req.isAuthenticted() check - the request stops there and the next middleware or function is not invoked (meaning the user is not logged in I assume). if I remove these guards (for the sake of debugging) what I am getting on the request object seems like a whole new session altogether. it has a different expiry timestamp, missing user data (or the cookie property).
here is a bit of code to illustrate what I am doing, I am surely doing something wrong - I just can't figure out what...
server.js
const mongoStore = require("connect-mongo")(session);
const sessionStore = new mongoStore({
mongooseConnection: mongoose.connection,
collection: "sessions",
});
app.use(
session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
store: sessionStore,
cookie: {
maxAge: 1000 * 60 * 60 * 24, // 1 day
},
})
);
app.use(passport.initialize());
app.use(passport.session());
// set up routes ...
passport.js
passport.serializeUser((user, done) => {
done(null, user.id);
});
passport.deserializeUser((id, done) => {
console.log("deserializeUser", id);
userModel
.findById(id)
.then((user) => {
done(null, user);
})
.catch((err) => {
done(err);
});
});
passport.use(
new YoutubeV3Strategy(
{
clientID: process.env.CLIENT_ID,
clientSecret: process.env.CLIENT_SECRET,
callbackURL: "http://localhost:5000/auth/youtube/redirect",
scope: ["https://www.googleapis.com/auth/youtube"],
authorizationParams: {
access_type: "offline",
},
},
async function (accessToken, refreshToken, profile, done) {
// console.log("passport verify cb", profile);
let user = await getUser(profile.id);
// console.log("user", user);
if (!user) {
user = await addUser({
youtubeId: profile.id,
name: profile.displayName,
accessToken: accessToken,
refreshToken: refreshToken,
});
}
return done(null, user);
}
)
);
an example for a route called after user authorized the app and is registered in db (both in user collection and a session created in session store):
router.get("/someendpoint", (req, res) => {
console.log("isAuthenticated", req.isAuthenticated()); // false
console.log("session", req.session); // a session object that does not correspond to anything in my session store.
});
As mentioned, if I guard this endpoint with passport.authenticate('youtube') or a simple req.isAuthenticated() check the code doesn't get anywhere past that guard.
some additional info that helps you help me:
my client and server run on localhost but on different ports
the 'some endpoint' endpoint is on a different path than the endpoints I use for managing the OAuth flow.
Your help is much appreciated...
I've a login page (using a HTML form) that when a user enters a password (pre-determined) they can view a site. What I would like is, when the user is logged in, a cookie timer will start and last for 24 hours, when 24 hours expires, they will have to log back in when they view the site. I've done a fair bit of research into this but am struggling a small bit to understand due to my lack of experience with Node.js. I appreciate that using a pre-determined password is poor practice but for what I'm doing, it suits.
Below is the code I have in the server.js file. I had a normal login working prior to starting with cookies so it's just the cookie part I'm having trouble with. Obviously the code below is missing something like MaxAge but I dunno where to implement it.
Thanks
function checkAuth(req, res, next) {
if (!req.session.user_id) {
res.send('You are not authorized to view this page');
} else {
next();
}
}
app.get('/home', checkAuth, function (req, res) {
res.send('if you are viewing this page it means you are logged in');
});
app.post('/login', function (req, res) {
var post = req.body;
if (req.body.name == "login"){
req.session.user_id = "login";
res.redirect('/home');
} else {
res.send('Bad user/pass');
}
});
When you initialise your session, add the maxAge to the cookie.
app.use(session({
secret: 'your secret key',
resave: true,
saveUninitialized: false,
cookie: {
secure: false,
maxAge: 1440000
}
}));
This sets the maxAge of all the cookie. Post which you can check if the request is authenticated.
I have this issue where a session is created regardless if the user is logged in or not.
I would like it if a session is created only if the user logs in successfully. Otherwise, if a selenium bot hits...for example, the route route('/users/:username') my session collection would fill up with sessions that are not from real users. I am having my users stay logged in forever, so the session cookie is set to a year...which is even worse if the session does not belong to real user.
How can I have a session returned to the client only if the authenticate successfully? I tried different order of the routes and middle ware, but this is the only order that works correctly.
app.use(session({
secret: 'bob',
saveUninitialized: true,
resave: true,
name: 'curves',
cookie: { secure: false, httpOnly: false,
maxAge: 365 * 24 * 60 * 60 * 1000 },
store: new MongoStore(storeOptions)
}));
app.use(passport.initialize());
app.use(passport.session());
app.use('/', auth);
app.use('/api/v1', router);
// isLoggedIn WOULD NOT WORK IF I PLACED THIS BEFORE PASSPORT/SESSION MIDDLEWARE
router.route('/users/:username')
.get(isLoggedIn, api.getUsername);
auth.route('/auth/facebook')
.get(passport.authenticate('facebook', { scope : 'email' }));
auth.route('/auth/facebook/callback')
.get(function(req, res, next) {
passport.authenticate('facebook', function(err, userDoc, info) {
if (err) { return next(err); }
// I don't think !userDoc will ever happen because of mongo upsert
if (!userDoc) { return res.redirect('/login'); }
req.logIn(userDoc, function(err) {
if (err) { return next(err); }
return res.redirect('http://localhost:9000/users');
});
})(req, res, next);
});
function isLoggedIn(req, res, next) {
if (req.isAuthenticated()) { return next(); }
res.status(404).json({ error: "not logged in" });
}
Figured it out...I just needed to set saveUninitialized: false.
saveUninitialized
Forces a session that is "uninitialized" to be saved to the store. A session is uninitialized when it is new but not modified. Choosing false is useful for implementing login sessions, reducing server storage usage, or complying with laws that require permission before setting a cookie. Choosing false will also help with race conditions where a client makes multiple parallel requests without a session.
The default value is true, but using the default has been deprecated, as the default will change in the future. Please research into this setting and choose what is appropriate to your use-case.
Note if you are using Session in conjunction with PassportJS, Passport will add an empty Passport object to the session for use after a user is authenticated, which will be treated as a modification to the session, causing it to be saved.