npm package: @os-team/session
Description: Reliable, feature-rich, easy-to-use session middleware for Express, developed based on the OWASP recommendations. Stores sessions in Redis. 100% test coverage.
Latest version: 1.2.1

# @os-team/session [![NPM version](https://img.shields.io/npm/v/@os-team/session)](https://yarnpkg.com/package/@os-team/session) [![BundlePhobia](https://img.shields.io/bundlephobia/minzip/@os-team/session)](https://bundlephobia.com/result?p=@os-team/session)

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

## Features

- Has a method to destroy all user's sessions.
- Has a method to get a list of all user's session.
- Can send a new session ID and its expiration date not only using a cookie, but also using a custom header (e.g. `X-Token` and `X-Token-Expires-At`). It can be useful for mobile apps.
- Prevents [session fixation attacks](http://www.acrossecurity.com/papers/session_fixation.pdf).
- Prevents [brute-force attacks](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html#session-id-length).
- Prevents [guessing attacks](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html#session-id-entropy).
- Has not only [absolute timeout](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html#absolute-timeout), but also an [idle timeout](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html#idle-timeout) and a [renewal timeout](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html#renewal-timeout).
- Prevents to create too many sessions for a single user.

## Usage

Install the package using the following command:

```
yarn add @os-team/session
```

It is assumed that the [express](https://github.com/expressjs/express) and [ioredis](https://github.com/luin/ioredis) libraries are already installed.

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

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

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

### Simple example

```ts
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:

```ts
// 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.

```ts
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](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#preventing_caching).

⚠️ 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](http://www.acrossecurity.com/papers/session_fixation.pdf) 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:

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

#### Customizing the cookie options

By default, used the following [cookie options](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#attributes):

- `httpOnly` to forbid JS from accessing the cookie. It prevents the session ID stealing through [XSS attacks](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html).
- `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](https://developer.mozilla.org/en-US/docs/Glossary/MitM).
- `sameSite: 'strict'` to enforce a browser sends the cookie only for same-site requests. It provides some protection against [cross-site request forgery attacks](https://owasp.org/www-community/attacks/csrf).

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

```ts
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](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html#cookies).

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

```ts
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:

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

#### Changing the length of the session ID

To prevent [brute-force attacks](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html#session-id-length) the length of session IDs must be at least `128 bits`.
In addition, to prevent [guessing attacks](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html#session-id-entropy) 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](https://github.com/ianfabs/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:

```ts
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:

```ts
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](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html#manage-session-id-as-any-other-user-input).

From [Redis keys](https://redis.io/topics/data-types-intro) 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](https://stackoverflow.com/questions/22023538/is-it-possible-to-run-a-string-injection-attack-on-a-redis-query/26528595#26528595) 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:

```ts
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:

```ts
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:

```ts
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](http://www.acrossecurity.com/papers/session_fixation.pdf).
Read more about this in [OWASP](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html#renew-the-session-id-after-any-privilege-level-change).

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

```ts
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:

```ts
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:

```ts
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:

```ts
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:

```ts
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:

```ts
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](https://docs.gitlab.com/ee/user/profile/active_sessions.html#listing-all-active-sessions) does.

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

```ts
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:

```ts
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](https://code.google.com/archive/p/crypto-js/).
This algorithm uses a secret passphrase that you **MUST** specify in the middleware option as follows:

```ts
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:

```ts
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:

```ts
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](http://www.acrossecurity.com/papers/session_fixation.pdf).

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

```ts
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](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html#session-expiration).

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

```ts
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`.