🔓 Authentication
Permissions
The server supports fine grained permission control.
Each user has a role that has a specific set of permissions:
export type AccessRights = Readonly<{ editSiteMetadata: boolean, // Permission to edit Site metadata (Name of the site etc...) admin: boolean, // Permission for general administration tasks editOwnUsername: boolean, // Permission to edit own username editRoles: boolean, // Permission to create edit and delete Roles editSchemas: boolean, // Permission to create, edit, and delete Schemas createShare: boolean, // Permission to create Share Tokens readableCollections: CollectionLevelPermission, // Collection IDs of collections that are readable writableCollections: CollectionLevelPermission, // Collection IDs of collections that are writable readableEntities: EntityLevelPermission, // Entity IDs of entities that are readable writableEntities: EntityLevelPermission, // Entity IDs of entities that are writable readMedia: boolean, // Permission to read Media (See Images and Videos that are uploaded) deleteMedia: boolean, // Permisison to read Media uploadMedia: boolean, // Permission to upload new Media}>
Collection Level Permissions
This type defines permissions at the collection level. It can store a list of allowed collection IDs or use a wildcard (*
) to grant access to all collections.
export type CollectionLevelPermission = ReadonlySet<string> | '*'
Entity Level Permission
This type allows granular control over entity-level permissions within a collection. It can:
- Use a wildcard (
*
) to grant access to all entities. - Specify specific entity IDs using
specificEntities
. - Grant access to all entities within a particular collection using
entitiesFromCollection
.
export type EntityLevelPermission = Readonly<{ specificEntities: ReadonlySet<string>, // Entity IDs entitiesFromCollection: ReadonlySet<string>, // Collection IDs}> | '*'
When a user requests an access token, the server consults their role permissions and sets them accordingly. Additionally, if the browser has active share tokens, the granted permissions are combined from both the user’s role and the share tokens.
Token Types
The server employs three distinct token types:
Refresh Token
Stored in a refresh
cookie, this JSON Web Token (JWT) contains the logged-in user’s ID and role. It is called a “refresh token” because it’s used to obtain a new access token. The server maintains a record of valid tokens in Valkey. A refresh request via the /api/auth/refresh
endpoint triggers token rotation.
Share Token
Users can generate share tokens to grant others (limited) editing access without requiring an account (sharing via URI). Each share token is associated with specific access rights.
Share tokens can be activated by including them in a POST request to the /api/auth/refresh
endpoint, adding them to the shares
cookie (a comma-separated list of share tokens).
Access Token
Stored in an access
cookie, this JWT contains the current access rights. It’s set by sending a request to the /api/auth/refresh
endpoint. Access rights are determined by combining those from the logged-in user and active share tokens.
Unlike refresh tokens, access tokens have a short validity period and must be refreshed once they expire. They are used to verify permissions for actions requiring authorization (implementation pending). This approach avoids frequent permission checks for every request, streamlining server performance.
Sign-In Flow
- The Dashboard redirects to
/api/auth/signin
. - Users select a login provider (or are automatically redirected if only one is available) and are taken to
/api/auth/signin/start/<providerId>
. - Users complete the OAuth/OpenID flow at the chosen provider, which redirects them to
/api/auth/complete/<providerId>
with the authentication token in the URI parameter. - The server retrieves the user’s ID from the login provider and checks for an associated account:
- If an account exists, the user is logged in by setting the
refresh
cookie. - If no account exists, a new account is created, and the user is logged in with the
refresh
cookie.
- If an account exists, the user is logged in by setting the
- The user is redirected back to the Dashboard.
Login Providers
Currently we only support Discord OAuth2 as a login provider type. We plan to expand the list to a handfull of big names like Google and we also want to support generic OpenID.
To test or develop with Discord you need to register a developer application. In the developer application go to the OAuth2 Section and add the following redirects:
https://localhost:3000/api/auth/signin/complete/<ID>
and
http://localhost:3000/api/auth/signin/complete/<ID>
Make sure to set the port to the one your dev. server runs on (not the Dashboard’s port) and replace <ID>
with any string of characters and numbers for example discord
.
The next step is to register the login provider in the Server. Since the Server supports multiple login providers, they are configured through a JSON Array in the following format:
[{ "type": "<PROVIDER_TYPE>", "id": "<ID>", "client_id": "<CLIENT_ID>", "client_secret": "<CLIENT_SECRET>"}]
Use the template and replace the placeholders with the correct values. The only supported type is DiscordOAuthProvider
. Set the ID to the same one you set in the step before. The client ID and secret can be accessed on the OAuth2 page of the Discord application you registered.
Then strip all newlines from the template and set the LOGIN_PROVIDERS
environment variable in the apps/server/.env.local
file. It will look something like this:
export LOGIN_PROVIDERS='[{"type": "DiscordOAuthProvider","id": "discord","client_id":"HIDDEN","client_secret":"HIDDEN"}]'
Don’t forget to surrount the JSON String with single quotes ('
) and do not use any within the String.