npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@os-team/session

v1.0.45

Published

Reliable, feature-rich, easy-to-use session middleware for Express, developed based on the OWASP recommendations. Stores sessions in Redis. 100% test coverage.

Downloads

108

Readme

@os-team/session NPM version BundlePhobia

Reliable, feature-rich, easy-to-use session middleware for Express, developed based on the OWASP recommendations. Stores sessions in Redis. 100% test coverage.

Features

Usage

Install the package using the following command:

yarn add @os-team/session

It is assumed that the express and ioredis libraries are already installed.

If you use TypeScript, create the express.d.ts file with the following content:

import { Session } from '@os-team/session';

declare global {
  namespace Express {
    interface Request {
      session: Session;
    }
  }
}

Simple example

import IORedis from 'ioredis';
import express from 'express';
import session from '@os-team/session';

const redis = new IORedis({
  port: 6379,
  host: 'localhost',
});

const app = express();
app.use(session({ redis })); // Add the middleware
app.get('/', (req, res) => {
  res.send(`User ID: ${req.session.data.userId}`);
});

Creating a new session

When a user sign in or register in your app you need to create a new session for him. You can do it the following way:

// The sign in page
app.post('/sign-in', async (req, res) => {
  // Find the user
  const user = await db.findUser({ email: req.body.email });
  if (!user) {
    res.send('The user does not exist');
    return;
  }
  if (!PasswordUtil.compare(req.body.password, user.password)) {
    res.send('Password is incorrect');
    return;
  }

  // Create a new session
  await req.session.create({ userId: user.id }); // usedId is a required parameter

  res.redirect(302, '/account');
});

// The account page
app.get('/account', (req, res) => {
  // Redirect unauthenticated users to the sign in page
  if (!req.session.id) {
    res.redirect(302, '/sign-in');
    return;
  }

  // Use the session data
  console.log(req.session.id); // Session ID
  console.log(req.session.data.userId); // User ID
  console.log(req.session.data.createdAt); // Timestamp when the session was created
  console.log(req.session.data.regeneratedAt); // Timestamp when was the last time the session ID was regenerated (see renewalTimeout)
  console.log(req.session.data.lastSeenAt); // Timestamp when was the last time the user made a request to the server
  console.log(req.session.expiresIn); // Number of seconds after which the session will expire

  res.send('You are authenticated!');
});

Fell free to save additional information about a user.

await req.session.create({
  userId: user.id,
  ip: req.ip,
  userAgent: req.get('user-agent'),
});

The new session ID will be passed to the client using a cookie (the Set-Cookie header). In addition, to prevent caching the session ID the library sends the Cache-Control: no-store (for HTTP/1.1 clients) and Pragma: no-cache (for HTTP/1.0 clients) directives. See more here.

⚠️ Creating a new session is mandatory regardless of whether the user is authenticated (an existing session ID was passed) or not. Otherwise, an attacker can gain access to the user account by using a session fixation attack.

Paragraph 5.1 of the session fixation vulnerability paper reads as follows:

Web applications must ignore any session ID provided by the user's browser at login and must always generate a new session to which the user will log in if successfully authenticated.

Customizing the cookie name

The cookie name should be such that it is not clear what the purpose of this cookie is. The less descriptive the cookie name, the better.

By default, used the sid cookie name, but you can set your own by passing the cookieName option in the middleware:

app.use(
  session({
    redis,
    cookieName: 'id',
  })
);

Customizing the cookie options

By default, used the following cookie options:

  • httpOnly to forbid JS from accessing the cookie. It prevents the session ID stealing through XSS attacks.
  • secure to enforce a browser sends the cookie to the server only using the HTTPS scheme (only in the production environment). It prevents the disclosure of the session ID through man-in-the-middle attacks.
  • sameSite: 'strict' to enforce a browser sends the cookie only for same-site requests. It provides some protection against cross-site request forgery attacks.

You can set additional cookie options or change existing ones by passing cookieOptions:

app.use(
  session({
    redis,
    cookieOptions: {
      domain:
        process.env.NODE_ENV === 'production' ? 'domain.com' : 'localhost',
      path: '/path',
      secure: process.env.NODE_ENV === 'production',
      httpOnly: true,
      sameSite: 'lax',
    },
  })
);

Recommendations:

  • Do not set the domain attribute to restrict the cookie just to the origin server.
  • Set the path attribute as narrow as possible only for the path of your web application that uses the session ID.

See more in OWASP.

Passing the session ID using a custom header

If you are developing a mobile app, most likely you want to get the session ID using a custom header (e.g. X-Token). To do this, set the tokenHeaderName option:

app.use(
  session({
    redis,
    tokenHeaderName: 'X-Token',
  })
);

In this case, the expiration date of the new session will be passed using the X-Token-Expires-At header, but you can also customize it:

app.use(
  session({
    redis,
    tokenHeaderName: 'X-Token',
    tokenExpirationHeaderName: 'X-Token-Exp',
  })
);

Changing the length of the session ID

To prevent brute-force attacks the length of session IDs must be at least 128 bits. In addition, to prevent guessing attacks it is necessary to use a cryptography secure pseudo-random number generator (CSPRNG), which ensures that the random numbers coming from it are completely unpredictable.

To generate the session ID this library uses nanoid, which used A-Za-z0-9_- symbols (1 character is 6 bits). Nanoid uses the crypto.getRandomValues method, which generates cryptographically strong random values.

By default, the length of session IDs is 50 or 300 bits, but you can reduce or increase it by passing the length option:

app.use(
  session({
    redis,
    length: 30, // 180 bits
  })
);

⚠️ If your app already has active sessions, and you want to reduce the length of the session IDs you MUST also pass the maxLengthExistingIds option:

app.use(
  session({
    redis,
    length: 30,
    maxLengthExistingIds: 50, // The maximum length of session IDs already stored in Redis
  })
);

Otherwise, session IDs sent by users will not be detected, because the library validates the length of the session ID before checking for its existence.

By default, the maxLengthExistingIds is equal to the length, so if you reduce the length and your app has active sessions, set also the maxLengthExistingIds option.

The OWASP recommendation about validating the session ID.

From Redis keys section:

Very long keys are not a good idea. For instance a key of 1024 bytes is a bad idea not only memory-wise, but also because the lookup of the key in the dataset may require several costly key-comparisons.

The library also checks that the session ID does not contain a colon character before checking for its existence.

Limiting the max number of sessions per user

By default, the library stores no more than 100 sessions per user. If the number of active sessions exceeds the specified number, the oldest ones are deleted. This number should not be too large if you send a list of all user sessions to the client side.

You can change the maximum number of sessions per user by passing the maxSessionCountPerUser middleware option:

app.use(
  session({
    redis,
    maxSessionCountPerUser: 10,
  })
);

Updating the current session

In some cases, you may need to update the session data. For example, if you store a username in a session, and the user updates it on the settings page, you can also update it in the session:

app.post('/update-settings', async (req, res) => {
  // Redirect unauthenticated users to the sign in page
  if (!req.session.id) {
    res.redirect(302, '/sign-in');
    return;
  }

  // Update the session data
  await req.session.update({
    fullName: req.body.fullName,
  });

  res.send('Your name have been saved');
});

If you want to delete the existing field, you can do it the following way:

await req.session.update({
  fullName: undefined,
});

Note that you can not update system fields:

  • userId
  • createdAt
  • regeneratedAt
  • lastSeenAt

If you try to do this, these fields will not be saved.

⚠️ If you want to update the user role, you MUST also regenerate the session ID using the req.session.regenerateId method (see below).

Regenerating the session ID

If you update the user's privilege level (for example, the user has become an administrator), or change the user's password, you must regenerate the session ID to prevent session fixation attacks. Read more about this in OWASP.

Let's assume you want to update the user's password. After changing the password, regenerate the session ID as follows:

app.post('/update-password', async (req, res) => {
  // Redirect unauthenticated users to the sign in page
  if (!req.session.id) {
    res.redirect(302, '/sign-in');
    return;
  }

  // Find a user
  const user = await db.findUser({ id: req.session.id });

  // Check if the current password is correct
  if (!PasswordUtil.compare(req.body.password, user.password)) {
    res.send('Password is incorrect');
    return;
  }

  // Update the user's password in the database
  user.password = PasswordUtil.hash(req.body.newPassword);
  await user.save();

  // Regenerate the session ID
  await req.session.regenerateId();

  res.send('Your password has been changed');
});

The new session ID will be passed to the client using a cookie. You can also pass the new session ID using a custom header (see the tokenHeaderName option above).

The old session will be deleted immediately, but in some cases you can make the old session valid for some time, accommodating a safety interval, before the client is aware of the new session ID.

You can do this by passing the deleteAfterDelay argument to the regenerateId method like this:

await req.session.regenerateId(true); // deleteAfterDelay = true

By default, the old session will be valid for 60 seconds, but you can set your own number of seconds in the deletionTimeout middleware option:

app.use(
  session({
    redis,
    deletionTimeout: 20, // seconds
  })
);

⚠️ Do NOT use the deleteAfterDelay argument of the regenerateId method when updating the user's previlege level or changing the user's password. In this case, the old session MUST be deleted immediately.

Deleting the current session

If the user sign out, you must delete the current session as follows:

app.post('/sign-out', async (req, res) => {
  // Redirect unauthenticated users to the sign in page
  if (!req.session.id) {
    res.redirect(302, '/sign-in');
    return;
  }

  // Delete the current session
  await req.session.destroy();

  res.redirect(302, '/sign-in');
});

To reset the cookie in the user's browser, the library sends the following header:

Set-Cookie: sid=; Expires=Thu, 01 Jan 1970 00:00:00 GMT // + your cookie options

Deleting all sessions of the current user

The application should provide not only the sign out feature, but also the feature to sign out from all devices, if the user has any suspicions.

You can delete all sessions of the current user as follows:

app.post('/sign-out-from-all-devices', async (req, res) => {
  // Redirect unauthenticated users to the sign in page
  if (!req.session.id) {
    res.redirect(302, '/sign-in');
    return;
  }

  // Delete all sessions of the current user
  await req.session.destroyAll();

  res.redirect(302, '/sign-in');
});

Sessions associated with other users will not be affected.

You can also delete all sessions except the current one by passing the exceptCurrent argument to the destroyAll method like this:

await req.session.destroyAll(true); // exceptCurrent = true

Getting all sessions of the current user

The application can provide the user with a list of all their active sessions, as GitLab does.

You can get a list of all sessions of the current user as follows:

app.get('/active-sessions', async (req, res) => {
  // Redirect unauthenticated users to the sign in page
  if (!req.session.id) {
    res.redirect(302, '/sign-in');
    return;
  }

  // Get a list of all sessions of the current user
  const list = await req.session.list();

  // The list item contains all the session data, including you own
  console.log(list[0].userId); // User ID (always the current one)
  console.log(list[0].createdAt); // Timestamp when the session was created
  console.log(list[0].regeneratedAt); // Timestamp when was the last time the session ID was regenerated (see renewalTimeout)
  console.log(list[0].lastSeenAt); // Timestamp when was the last time the user made a request to the server
  console.log(list[0].id); // The unique ID that can be used to delete this session (not equal to the real session ID)
  console.log(list[0].current); // Whether the session is current
  console.log(list[0].ip); // Your own field that should be set when creating or updating the session
  console.log(list[0].userAgent); // Your own field that should be set when creating or updating the session

  res.json(list);
});

The list of sessions is sorted by creation date in descending order.

The session data includes the following additional fields:

  • id – used to delete the session.
  • current – indicates whether the session is current.

Using all of this data, you can show the user the following comprehensive information about each session:

  • Device type (by userAgent).
  • Browser name (by userAgent).
  • Browser version (by userAgent).
  • Operating system (by userAgent).
  • IP address (by ip).
  • The country and city where the user is located (by ip).
  • When the user is signed in (by createdAt).
  • When was the user last active (by lastSeenAt).
  • Whether the session is current (by current).

You can also provide the user with a feature to delete a specific session that makes them suspicious using the destroy method:

app.get('/delete-session', async (req, res) => {
  // Redirect unauthenticated users to the sign in page
  if (!req.session.id) {
    res.redirect(302, '/sign-in');
    return;
  }

  // Delete the specific session
  const list = await req.session.destroy(req.body.sessionId);

  res.send('The session has been deleted');
});

⚠️ The id is not equal to the real session ID for security reasons. It is generated based on the real session ID by the AES algorithm using Google's CryptoJS. This algorithm uses a secret passphrase that you MUST specify in the middleware option as follows:

app.use(
  session({
    redis,
    secret: 'secret',
  })
);

Configuring timeouts

To minimize the chance that an attacker will hijack the session ID and to reduce the time during which an attacker will use it, you should use as shortest timeouts as possible.

Absolute timeout

Absolute timeout is the maximum number of seconds a session can be active since the given session was initially created.

👨‍💻 It specifies the amount of time an attacker can use a hijacked session ID.

By default, it is 31540000 (1 year), but you can change this value:

app.use(
  session({
    redis,
    absoluteTimeout: 28800, // 8 hours
  })
);

Idle timeout

Idle timeout is the number of seconds a session will remain active in case there is no activity in the session.

👨‍💻 It specifies the amount of time during which a thief of a user's device (mobile phone, laptop, or other device) should access to the application on behalf of the victim user (or just steal its session ID).

By default, it is 2592000 (30 days), but you can change this value:

app.use(
  session({
    redis,
    idleTimeout: 300, // 5 minutes
  })
);

To disable set 0.

Renewal timeout

Renewal timeout is the number of seconds after which the session is automatically renewed in the middle of the user session.

👨‍💻 It specifies the time during which an attacker must gain access to the application on behalf of the victim user after stealing its session ID. In addition, the combination of idle and renewal timeouts significantly complicates the execution of a session fixation attack.

By default, it is 1800 (30 minutes), but you can change this value:

app.use(
  session({
    redis,
    renewalTimeout: 120, // 2 minutes
  })
);

To disable set 0.

When the session ID has been renewed, the old session ID will be valid for some time, accommodating a safety interval, before the client is aware of the new session ID. You can specify how long the old session ID will be valid using the deletionTimeout middleware option.

Read more about timeouts in OWASP.

Setting the prefix for all Redis keys

Sometimes it is necessary to store sessions of different apps in the same Redis store. In this case, each app must have a unique prefix for all Redis keys. You can specify the prefix of your app as follows:

app.use(
  session({
    redis,
    prefix: 'my-app',
  })
);

Now the Redis keys will be my-app:session:key and my-app:user:1:sessions instead of session:key and user:1:sessions.