I have an app (node, expressjs, express-session, redis) which, despite not setting maxAge
or expires
, seems to timeout logins after a while.
Via the admin panel, I want admins to be able to view current sessions (already working, by grabbing sess:*
from redis), and click a persist button on a session to make it last indefinitely.
I need a reliable way of going from the data stored against the sess:...
to a unique identifier that I can reference to the browser instance. Probably, storing something at login that is signed, and then saving that into a permanent db that is checked alongside the session check (and sets up a new authenticated session if needed).
EDIT: To clarify 'from a different browser' - this is for the admin function. So the goal is to have a list of current sessions (achieved already), with a "persist this login" button on each.
This would mean that the user in question then doesn't get logged out.
EDIT: This is my current code for session handling:
const express = require('express') // v4.17.1
const app = express()
const redis = require('redis') // v4.5.1
const expressSession = require('express-session') // v1.17.2
const RedisStore = require('connect-redis')(expressSession) // v3.4.2
const redisClient = redis.createClient({ legacyMode: true })
redisClient.connect()
const session = expressSession({
store: new RedisStore({ client: redisClient }),
saveUninitialized: true,
secret: ‘xxxxxxx’,
resave: true
})
app.use(session)
Answering first part:
How can I persist a login forever, from a different browser? ... I have an app (node, expressjs, express-session, redis) which, despite not setting maxAge or expires, seems to timeout logins after a while.
If you don't set express-session cookie.maxAge
or cookie.expires
then redis-connect
will set a default TTL of one day:
If the session cookie has a expires date, connect-redis will use it as the TTL. Otherwise, it will expire the session using the ttl option (default: 86400 seconds or one day). -- https://github.com/tj/connect-redis#ttl
You can disable that by setting redis-connect disableTTL option to true. It is not recommended you do this (see next part).
(Note, It could also be something completely different like your users are deleting your cookies when they close browser, or you have a SPA and session aren't tracking properly across refresh etc).
Answering second part:
Summarizing my understanding on what you want:
So what you seem to want to do is uniquely identify each user's devices and be able to dynamically toggle the session lifetime for the given device between some default and forever. It's device id that matters, session ids are kind of incidental here.
If you want to do this with express-session
I would suggest not trying to use disableTTL
, but just setting a really large cookie.maxAge
for these sessions (5 years is practically forever in web apps, and you can touch the session every time the user is active to reset TTL as well).
Below is proof of concept code to do that. Note code uses latest versions at time of writing - will probably break with other versions. You'll need to fill in the details:
const express = require('express');
const redis = require('redis');
const session = require('express-session'); // https://github.com/expressjs/session
const app = express();
const port = 3000;
const RedisStore = require('connect-redis')(session);
const redisClient = redis.createClient({
url: 'redis://localhost',
legacyMode: true,
});
redisClient.connect()
.then(() => { console.log('Redis client connected'); })
.catch(console.error);
const DAY_IN_MS = 60 * 60 * 24 * 1000;
const YEAR_IN_MS = 365 * DAY_IN_MS;
// Mock user DB.
const users = {
bob: {
id: 'bob',
forever_devices: ['foo_device', 'bah_device']
}
};
app.use(session({
store: new RedisStore({
client: redisClient,
// disableTTL: true, // Dont need this.
}),
secret: 'keyboards',
saveUninitialized: false,
resave: true,
cookie: {
maxAge: 3*DAY_IN_MS // Default 3 days.
},
}));
app.listen(port, () => console.log(`App listening on port ${port}!`));
app.use((req, res, next) => {
console.log(req.query, req.sessionID, req.session);
next();
});
// Mock login accepting a mock user id and device_id. Obviously insecure ..
app.get('/login', (req, res) => {
const { id, device_id } = req.query;
if(!(id in users)) {
return res.send('Denied').status(401);
}
req.session.loggedIn = true;
if(users[id].forever_devices?.includes(device_id)) {
req.session.cookie.maxAge = YEAR_IN_MS*1000;
}
res.send('OK');
});
app.get('/logout', (req, res) => {
req.session.destroy();
res.send('OK');
});
app.get('/', async function (req, res) {
if(req.session.loggedIn) {
redisClient.v4.ttl(`sess:${req.sessionID}`).then((d) => {
res.send(`Hi ${req.sessionID} your TTL is ${d}`);
});
} else {
res.send('Not logged in');
}
});
Testing with curl (you'll need to start redis on localhost with no credentials):
> curl -b cjar -c cjar 127.0.0.1:3000/login?id=bob
OK
> curl -b cjar -c cjar 127.0.0.1:3000/
Hi sukXf3ddjx3AMakVGIfChvlI-_KhaPqQ your TTL is 259196
> curl -b cjar -c cjar 127.0.0.1:3000/logout
OK
> curl -b cjar -c cjar "127.0.0.1:3000/login?id=bob&device_id=foo_device"
OK
> curl -b cjar -c cjar 127.0.0.1:3000/
Hi 81JZL-PxCRhNclktuYxHjTeHibaoNP9G your TTL is 31535999997
> curl -b cjar -c cjar 127.0.0.1:3000/logout
OK
> curl -b cjar -c cjar 127.0.0.1:3000/
Not logged in