Release notes v0.0.172

Since Compas v0.0.14 we have been working on a session management experience that works across the board. Rocking quite some features:

  • Allows localhost cookies to develop against a remote staging server
  • Completely backend managed session lifecycle, with custom rolling session behaviour
  • Secure defaults for production environments
  • Quick setup, all kinks worked out.

However, there are two major downsides;

  • Quite a few workarounds where added, like the compas proxy rewriting cookies, and things like keepPublicCookie for the frontend to check if a session may exist before attempting any call.
  • The cookies don't work in all environments. Where things like mobile apps need to open a webview, login and then hope that the cookies that where returned would be added to api calls from the app.

So we created a new session store accessible via 'fancy session tokens' in a JSON Web Token format. Which doesn't do any assumptions about how tokens are transported over the network. Shifting responsibility for secure transport, storage on the client and refreshing the session to the end user of Compas, you 😃

Session store

Migration to the new session store is quite the task. Let's start by removing the deprecated components:

  • Remove any reference to session imported from @compas/server. This is the Koa middleware that currently powers the full cookie management. There is no replacement in @compas/server.
  • Remove any reference newSessionStore imported from @compas/store. This component was used to persist sessions created by the session middleware in PostgreSQL.

The last session detail related to the old session management is ctx.session used in your route handlers. It is up to you if you want to replace it completely or if you want to use the new session management functions to be based on ctx.session. The next steps will describe current behaviours of ctx.session and which sessionStoreXxx calls relate to that behaviour. Please refer to the session docs for details on the functions mentioned below.

Creating a session

Previously creating a session could be done by setting ctx.session to a new object. This would automatically persist the session and set cookies in the response. This is now powered by sessionStoreCreate and the returned tokens need to manually be transported to the client. It is advised to return the tokens in the response body.

Reading the session

Reading the session was as easy as checking ctx.session. This is a bit more complex in the new system. The application needs to maintain the session id in the request context, because this is needed to for example update the session. By calling sessionStoreGet you get an object containing id, checksum and data. You could store this information on ctx._session for example as id and checksum are necessary for sessionStoreUpdate and sessionStoreInvalidate. But data is the one you need to populate ctx.session. We advise to use an Authorization: Bearer xxx header format for transporting the access token from the client to the server.

Updating a session

The old session management would automatically check if the ctx.session object did change and persist the new session data if necessary. The new sessionStoreUpdate call does exactly that, and thus can be called on every request with minimal performance impact.

Removing a session

Previously destroying a session would be done by setting ctx.session to null. This is now handled via sessionStoreInvalidate which revokes all tokens related to the session.

Custom max age

The previous session management had features to set a custom _maxAge on ctx.session. This is not supported anymore.

Sliding session window

The default settings for session management did set up a rolling session system. Meaning that sessions would automatically be renewed on incoming requests if necessary. This is not possible in the new system. Your api need to have some way of refreshing the session via for example a new endpoint, which internally calls sessionStoreRefreshTokens. And the api clients need to check the exp field on the access and refresh tokens to decide if they should call the refresh endpoint.

Cleaning up old sessions

Previously we exposed a .clean() function on the session store returned from newSessionStore. This would remove all session records PostgreSQL that where expired. The new system has the same feature via sessionStoreCleanupExpiredSessions.

Migration

The new session store is of course backed by Postgres, and needs the following migration:

CREATE TABLE "sessionStore"
(
  "id"        uuid PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(),
  "checksum"  varchar          NOT NULL,
  "data"      jsonb            NOT NULL,
  "revokedAt" timestamptz      NULL,
  "createdAt" timestamptz      NOT NULL DEFAULT now(),
  "updatedAt" timestamptz      NOT NULL DEFAULT now()
);

CREATE TABLE "sessionStoreToken"
(
  "id"           uuid PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(),
  "session"      uuid             NOT NULL,
  "expiresAt"    timestamptz      NOT NULL,
  "refreshToken" uuid             NULL,
  "revokedAt"    timestamptz      NULL,
  "createdAt"    timestamptz      NOT NULL,
  CONSTRAINT "sessionStoreTokenSessionFk" FOREIGN KEY ("session") REFERENCES "sessionStore" ("id") ON DELETE CASCADE,
  CONSTRAINT "sessionStoreTokenRefreshTokenFk" FOREIGN KEY ("refreshToken") REFERENCES "sessionStoreToken" ("id") ON DELETE CASCADE
);

CREATE INDEX "sessionStoreTokenSessionIdx" ON "sessionStoreToken" ("session");
CREATE INDEX "sessionStoreTokenRefreshTokenIdx" ON "sessionStoreToken" ("refreshToken");

In closing

Since this is a big change, we still support sessionMiddleware, newSessionStore, etc in v0.0.172. And most likely in v0.0.173 as well, but it will be removed in a future release.