Cookie and Session

HTTP is a stateless protocol. But web applications often need to identify the sender of requests. To solve this problem, HTTP protocol defines a header Cookie. Web servers can use response header Set-Cookie to send small data to clients. Clients (e.g. web browsers) store the data according to protocol, and attach the cookie data in future requests. For security reason, browsers only attach cookies in the requests that are sent to the same domain. Web servers can use Domain and Path attributes to define the scope of the cookie.

By using ctx.cookies, we can easily and safely read/set cookies in controller.

class HomeController extends Controller {
async add() {
const ctx = this.ctx;
let count = ctx.cookies.get('count');
count = count ? Number(count) : 0;
ctx.cookies.set('count', ++count);
ctx.body = count;
}
async remove() {
const ctx = this.ctx;
ctx.cookies.set('count', null);
ctx.status = 204;
}
}

# ctx.cookies.set(key, value, options)

Modifying Cookie is done by setting Set-Cookie header in HTTP responses. Each Set-Cookie creates a key-value pair in client. Besides of setting the value of Cookie, HTTP protocol supports more attributes to control the transfer, storage and permission of Cookie.

In addition to these standard Cookie attributes, egg.js supports 3 more parameters:

When using Cookie, we need to have a clear idea of the purpose of the cookie, how long it needs to be stored in client, can it be accessed by JS, can it be modified by client.

By default, Cookie is signed but not encrypted, client can see raw content but cannot modify it (manually).

ctx.cookies.set(key, value, {
httpOnly: false,
signed: false,
});
ctx.cookies.set(key, value, {
httpOnly: true, // by default it's true
encrypt: true, // cookies are encrypted during network transmission
});

Note:

  1. Due to the uncertainty of client's implementation, to ensure Cookie can be stored successfully, it's recommended to encode cookie value in base64 or other codec.
  2. Due to the limitation of Cookie length on client side, do avoid using long Cookie. Generally speaking, no more than 4093 bytes. When Cookie value's length is greater than this value, egg.js prints a warning in log.

# ctx.cookies.get(key, options)

As HTTP Cookie is sent over header, we can use this method to easily retrieve the value of given key from Cookie. If options.signed and options.encrypt has been configured to sign and encrypt Cookie, the corresponding options also need to be used in get method.

If you want to get the cookie set by frontend or other system, you need to specify the parameter signed as false, avoid varify the cookie and not getting the vlaue.

ctx.cookies.get('frontend-cookie', {
signed: false,
});

Since we need to sign and encrypt Cookie, a secret key is required. In config/config.default.js:

module.exports = {
keys: 'key1,key2',
};

keys is defined as a string, several keys separated by commas. When egg.js processes Cookie:

If you need to update Cookie secret key and don't want to invalidate existing clients' Cookie, you can add new secret key at the front of keys. After some time, when existing Cookie has expired, delete the old secret keys.

# Session

In web applications, Cookie is usually used to identify users. So the concept of Session, which is built on top of Cookie, was created to specifically handle user identification.

Egg.js built-in supports Session through egg-session plugin. We can use ctx.session to read or modify current user session.

class HomeController extends Controller {
async fetchPosts() {
const ctx = this.ctx;
// get content from session
const userId = ctx.session.userId;
const posts = await ctx.service.post.fetch(userId);
// modify session value
ctx.session.visited = ctx.session.visited ? (ctx.session.visited + 1) : 1;
ctx.body = {
success: true,
posts,
};
}
}

It is very intuitive to use Session, simply get or set. To delete a session, set its value to null:

exports.deleteSession = function* (ctx) {
ctx.session = null;
};

Session is built on top of Cookie. By default, the content of Session is stored in a Cookie field as encrypted string. Every time a client sends requests to server, the cookie is attached in requests. Egg.js passes decrypted cookie to server code. The default configuration of Session is:

exports.session = {
key: 'EGG_SESS',
maxAge: 24 * 3600 * 1000, // 1 day
httpOnly: true,
encrypt: true,
};

The attributes except of key are all standard Cookie attributes. key is the key of the cookie that stores session content. With default config, the session cookie is encrypted, not accessible to JS, which ensures user cannot access or modify it.

# Store session in other storage

Session is stored in Cookie by default. If a session is too big, there are some troubles.

Egg.js supports to store Session in other places. To config it, you can simply set app.sessionStore.

// app.js
module.exports = app => {
app.sessionStore = {
// support promise / async
async get (key) {
// return value;
},
async set (key, value, maxAge) {
// set key to store
},
async destroy (key) {
// destroy key
},
};
};

The implementation of sessionStore can also be encapsulated into a plugin. For example, egg-session-redis stores Session in Redis. To apply it, import egg-redis and egg-session-redis plugin in your application.

// plugin.js
exports.redis = {
enable: true,
package: 'egg-redis',
};
exports.sessionRedis = {
enable: true,
package: 'egg-session-redis',
};

Note: once you choose to store Session in external storage, it means your system heavily depends on the external storage. Once it's down, Session feature won't work. So it's recommended to put only necessary information in Session, keep Session minimum and use the default Cookie storage if possible. Do not put per-user's data cache in Session.

# Session Practice

# Set session's expiration time

Session config has a attribute maxAge, which controls global expiration time of all sessions of the application.

We often can see a Remember Me option on a lot of websites' login page. If it's selected, Session of this logged in user can live longer. This kind of per-user session expiration time can be set through ctx.session.maxAge:

const ms = require('ms');
class UserController extends Controller {
async login() {
const ctx = this.ctx;
const { username, password, rememberMe } = ctx.request.body;
const user = await ctx.loginAndGetUser(username, password);

// set Session
ctx.session.user = user;
// if user selected `Remember Me`, set expiration time to 30 days
if (rememberMe) ctx.session.maxAge = ms('30d');
}
}

# Extend session's expiration time

By default, if user requests don't result in modification of Session, egg.js doesn't extend expiration time of the session. But in some scenarios, we hope that if users visit our site for a long time, then extend their session validity and not let the user exit the login state. The framework provides a renew configuration item to implement this feature. It will reset the session's validity period when it finds that the user's session is half the maximum validity period.

// config/config.default.js
module.exports = {
session: {
renew: true,
},
};