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 apphttp://localhost:19006
// development & e2e web apphttp://localhost:4000
// needed for GraphiQL in developmenthttp://localhost:4001
// needed for GraphiQL in e2eserenity-desktop://app
// electron desktop app
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:
- https://github.com/serenity-kit/Serenity/blob/main/apps/app/utils/urqlClient/urqlClient.ts#L76-L78 (opens in a new tab)
- https://github.com/serenity-kit/Serenity/blob/main/apps/app/store/workspaceChainStore.ts#L222-L224 (opens in a new tab)
- https://github.com/serenity-kit/Serenity/blob/main/apps/app/utils/authentication/loginHelper.ts#L227-L229 (opens in a new tab)
On the server:
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.
Here is an example of a resolver that requires a valid user.
Depending on the specific mutation further checks regarding the device are performed e.g.
- if the create workspace invitation event is sent from the mainDevice of the user https://github.com/serenity-kit/Serenity/blob/main/apps/backend/src/graphql/mutations/workspace/createWorkspaceInvitation.ts#L47-L55 (opens in a new tab)
- validating the sessionTokenSignature in the login step
addDevice
https://github.com/serenity-kit/Serenity/blob/main/apps/backend/src/graphql/mutations/authentication/addDevice.ts#L86-L93 (opens in a new tab)
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
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.
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.
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.
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
.
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