Cryptography
Client Server (Authentication & Authorization)

Client/Server (Authentication & Authorization)

CORS Setup

For the production environment the allowed list of origins is limited to the following domains:

  • https://www.serenityapp.page
  • serenity-desktop://app // electron desktop app

For staging and development the allowed list of origins is:

  • https://www.serenity.li // staging web app
  • http://localhost:19006 // development & e2e web app
  • http://localhost:4000 // needed for GraphiQL in development
  • http://localhost:4001 // needed for GraphiQL in e2e
  • serenity-desktop://app // electron desktop app

see https://github.com/serenity-kit/Serenity/blob/main/apps/backend/src/createServer.ts#L108-L120 (opens in a new tab)

Server Authentication

The authentication process using OPAQUE results in a sessionKey which is associated to a user and a specific device. See user-registration. In addition to that a sessionToken is derived using a HKDF from the sessionKey. The sessionKey is never sent over the network.

On the client:

On the server:

https://github.com/serenity-kit/Serenity/blob/main/packages/common/src/deriveSessionAuthorization/deriveSessionAuthorization.ts#L15-L19 (opens in a new tab)

This information is stored in the Session table. https://github.com/serenity-kit/Serenity/blob/main/apps/backend/prisma/schema.prisma#L141-L149 (opens in a new tab)

Every GraphQL request (which use HTTPS as transport layer) sent to the server has to contain a Authorization header which is constructed like: ${sessionToken}|${datetime}|${sessionDatetimeSubkey}.

The sessionToken is stable per session and used to identify the session. The datetime is the current timestamp as a ISO 8601 string and the sessionDatetimeSubkey is a HMAC of the datetime using the sessionKey as a secret.

The server then checks if the session is valid and provides the session and user to the backend resolver. The validation derives the sessionDatetimeSubkey and validates that it is correct and verifies that the datetime is not older than 3 hours or 3 hours in the future. This doesn't avoid reply attacks, but it limits the time window of a leaked Authorization header to maximum 6 hours based on the server clock.

In addition the expiredAt field of the session is checked to make sure the session is still valid. This is explained in more detail a bit further down on this page.

https://github.com/serenity-kit/Serenity/blob/main/apps/backend/src/createServer.ts#L39-L70 (opens in a new tab)

https://github.com/serenity-kit/Serenity/blob/main/apps/backend/src/database/authentication/getSessionIncludingUserBySessionAuthorization.ts#L45 (opens in a new tab)

https://github.com/serenity-kit/Serenity/blob/main/apps/backend/src/database/authentication/getSessionIncludingUserBySessionAuthorization.ts#L14-L43 (opens in a new tab)

Here is an example of a resolver that requires a valid user.

https://github.com/serenity-kit/Serenity/blob/main/apps/backend/src/graphql/mutations/comment/createComment.ts#L45-L47 (opens in a new tab)

Depending on the specific mutation further checks regarding the device are performed e.g.

All sessions have an expiredAt set which is determined by the device type and always a bit larger than the expiration date of the device itself in case it's defined to avoid possible time differences between the client and the server. For device without an expiration data e.g. mobile and desktop device a value of 1000 years is chosen.

  • web: 31 days (device expiration is set to 30 days)
  • temporary-web: 25 hours (device expiration is set to 24 hours)
  • mobile | desktop: 1000 years

https://github.com/serenity-kit/Serenity/blob/main/apps/backend/src/database/authentication/createSessionAndDevice.ts#L57-L82 (opens in a new tab)

While a session key is connected to a device this is not enforced everywhere since certain interactions require a signature from the mainDevice.

Session & Device revocation

Removing a device should also revoke the associated session, but removing the DB entry.

https://github.com/serenity-kit/Serenity/blob/main/apps/backend/src/database/device/deleteDevice.ts (opens in a new tab)

Websocket Connections

For the staging and production environments a wss connection is required. The websocket connection is authenticated by sending an authorization token as a query parameter and validated before establishing the connection.

`${host}/${docId}?sessionKey=${authorizationToken}`;

Note: the query parameter is still called sessionKey even though it's not the sessionKey from the OPAQUE flow. The naming will be changed in the future.

https://github.com/serenity-kit/Serenity/blob/main/apps/backend/src/createServer.ts#L227-L263 (opens in a new tab)

Authenticated Users

For authenticated users the authorization token is constructed like the one for HTTP requests ${sessionToken}|${datetime}|${sessionDatetimeSubkey} and validated the same way on the server.

Document Share Links

For document share links a unique authorization token is generated for each document share link and stored in the DocumentShareLink table.

https://github.com/serenity-kit/Serenity/blob/main/apps/backend/prisma/schema.prisma#L342 (opens in a new tab)

When opening a document share link first the document share link is retrieved based on a token which is part of the share URL. The websocketSessionKey is contained in this response and to establish the websocket connection the client has to send the websocketSessionKey as a query parameter for the sessionKey.

https://github.com/serenity-kit/Serenity/blob/main/apps/app/navigation/screens/sharePageScreen/sharePageScreenMachine.ts#L113-L118 (opens in a new tab)

Possible Improvement

Encrypting the transport layer

In our threat model we assume a HTTPS connection to the server is secure and can't be intercepted. This assumption could be reduces (not eliminated) by encrypting each request to a server and response back to the client with the sessionKey.

For example the client could use the sessionKey to encrypt & MAC the request body. In addition a UTC timestamp could be added and make sure neither the client nor the server accept a request older than 4 hours or in the future and therefor prevent replay attacks.

Session validation for GraphQL requests

Instead of validating the user in almost every resolver, it should be done by default and only a white-list of specific mutations and queries should be allowed without a valid user.

Incomplete list of mutations and queries that should be allowed without a valid user:

  • startRegistration
  • finishRegistration
  • workspaceInvitation