Search code examples
expressredisexpress-session

How can I persist a login forever, from a different browser?


Background:

I have an app (node, expressjs, express-session, redis) which, despite not setting maxAge or expires, seems to timeout logins after a while.

Intention:

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)

Solution

  • 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:

    • A user can have many login sessions, 1 per device and upto N active sessions (what N is or how it is imposed is beyond the scope).
    • Some of those sessions you want to dynamically make forever sessions using this hypothetical button on the hypothetical admin panel.

    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:

    1. Store the flag indicating which devices are forever devices in your user DB.
    2. Figure out how to uniquely identify each device the user logs in with (I would suggest just grouping device types and only allow one session on each type but I don't know enough about what your trying to do to give more specific advice).
    3. Update active session when the admin toggles the button on the admin panel or whatever.

    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