Client Sessions

How to hold client sessions across multiple reconnects

In Chapter 7 - Access control in the Writing services guide, we learn how to set a client’s access token. This token will be stored by Resgate for as long as the client’s WebSocket connection remains. If the connection is lost, even for a second, Resgate will discard the token, forcing the client to reauthenticate itself when it reconnects.

While this is positive from a security perspective, it is rather inconvenient. Let’s learn how to create client sessions that can last over multiple reconnects across multiple Resgates.

Session responsibility

Resgate and the RES protocol has no concept of sessions or reconnects. It only knows of connections, identified by the connection ID (cid)1 generated for any new connections. When a client is, willingly or unwillingly, disconnected from Resgate, all information tied to that connection will discarded. This includes:

  • Connection ID
  • Subscriptions
  • Access token

Session handling is instead handled by a microservice in collaboration with the webserver and the client. This can be done through different means, such as by using session cookies, JWT tokens, or by

Note

Because Resgate holds no session information, a client is free to reconnect to any other Resgate to resume its session.

Example

We will base this guide on the Client Session example, which lets a client log in and reconnect or reload without losing its session. To try out the example, visit the link and follow the instructions.

The example uses a method that does not involve headers, but instead relies on a short-lived relogin key that may be used as credentials to resume a session. While it is just one of many possible solutions, it will show how client sessions can be handled with Resgate based API’s.

Tip

To learn how to use header cookies to create sessions, look at the JWT Authentication example.

Session data

The service, sessionService.js in the Client Session example, will create a new session when the client logs in. The session is just a data object stored in memory or persisted in a database by the service. It may look like this:

{
	sid: "a3JsHylu6iLPbeCnqK9wBDb5XZ8jz4Ua",
	cid: "bi17lut8smgihf37loa0",
	user: { id: 42, username: "foo", name: "Foo Bar" role: "guest" },
	created: "2006-01-02T15:04:05Z",
}

Where sid would be a unique session ID generated by the service, and cid would be the connection ID of the client logging in, provided by Resgate.

Relogin key

When a client reconnects, it needs a way to authenticate itself to tell the service that this new connection is the same client trying to resume an already existing session. One way to do this is to have the service generate a one-time-use key that works as credentials for a single relogin.

In the example service, this key is called a relogin key, but you may call it whatever you want.

Service
The key is passed back to the client in the login response:

nats.subscribe('auth.session.login', function(req, reply) {
	/* ... validate user credentials ... */
	/* ... create a new session ... */
	/* ... send access token for the connection ... */
	/* ... generate reloginKey using crypto.randomBytes ... */
	nats.publish(reply, JSON.stringify({ result: { reloginKey }}));
});

Client
The client stores the key for later use, maybe in the browser’s localStorage. In code it could look like this:

client.authenticate('session', 'login', { username, password }).then(result => {
	localStorage.setItem("reloginKey", result.reloginKey);
});

Tip

Instead of using a simple string token as key, the service could issue a signed token, such as a JSON Web Token (JWT).

This way, the JWT may contain enough information to log in the user without the service having to store references of issued keys.

Relogin method

A separate auth method may be used for relogin, where the only required parameter is the relogin key. Using the key, the service will look up which session it belongs to, and send an access token to the new client connection.

Once the key is used, it should be discarded by the service, and a new key should be generated and sent back to the client.

Service
On the service, it could look like this:

nats.subscribe('auth.session.relogin', (req, reply) => {
	/* ... validate the key in the request params ... */
	/* ... lookup the session belonging to the key ... */
	/* ... send access token for the new connection ... */
	/* ... discard old key and generate a new one ... */
	nats.publish(reply, JSON.stringify({ result: { reloginKey }}));
});

Client
The client would try to relogin as soon as a connection is established. This is done in ResClient’s setOnConnect callback. The old reloginKey is used up and should be replaced by the new key. In code could look like this:

function relogin() {
	let reloginKey = localStorage.getItem('reloginKey');
	if (!reloginKey) return;

	return client.authenticate('session', 'relogin', { reloginKey }).then(result => {
		localStorage.setItem("reloginKey", result.reloginKey);
	}).catch(err => { /* ... */ });
}

client.setOnConnect(relogin);

Session and key expiration

A separate logout auth method may be added to give the user the choice to log out, letting the service dispose of the session.

If the user decides to close the client without logging out, we still might want to dispose of the session after a set period of time. In addition, since the relogin key works are credentials, we might not want it to be valid forever either. Both of these can be solved by requiring the client to periodically refresh its relogin key.

Service
In the example, this happends in the relogin auth method, where an expire timer is reset on each call. The code can be found in the issueReloginKey function:

function issueReloginKey(session) {
	clearTimeout(session.expireId);
	session.expireId = setTimeout(() => disposeSession(session), EXPIRE_DURATION);

	/* ... generate reloginKey and store it on the session ... */
}

Client
The client uses a setTimeout to periodically refresh its relogin key.

let reloginTimer = null;
function startReloginTimer() {
	clearTimeout(reloginTimer);
	reloginTimer = setTimeout(relogin, RELOGIN_DURATION);
}

The startReloginTimer function is then called on every successful login or relogin.

Increased security

The example’s weakest link is the relogin key. If stolen, it may be used to hijack the session. Let’s look at two measures one may take:

Additional header validation

The relogin key may be secured additionally by using a header session cookie issued by the web server that serves the client.

By storing a header cookie’s session ID value in the sessionService’s session object, one can validate that any relogin is done by a client having the same session ID in the header cookie.

Dispose session on stolen key

By making a small adjustment to the client’s updateUserInfo function, we can cause a session to be disposed in case a malicious user stole our relogin key and used it. This is done by letting the client make an extra attempt to relogin once logged out.

If the client was forcefully logged out due to a session hijack, the relogin key would now end up being used twice, causing the entire session to be disposed by the service.

function updateUserInfo(user) {
	// No user ID means we are not logged in
	if (!user.id) {
		relogin();
		/* ... */
	}
	/* ... */
}

Conclusion

While Resgate does not handle sessions, only connections, the microservice may work together with the webserver and the client to create and maintain secure client sessions.