Skip to content
2 min

Identity and authorization

Connect an existing authentication and authorization system through Transport, Endpoint, and Service Middleware.

The Univer Collaboration SDK does not define users, roles, or an ACL model. Applications establish trusted identity in Transport Middleware, then protect realtime Sessions and authoritative collaboration data with Endpoint and Service Middleware.

This page is a concrete application of Middleware and Events and focuses on integrating identity and authorization through Middleware.

Identity model

IdentifierProviderMeaning and lifecycle
userIDApplicationStable business identity and author of confirmed changesets
memberIDEndpointOnline member ID for one WebSocket Session; changes after reconnection
sid + reqIdClientChangeset submission identity that remains stable across retries

User profile fields submitted by the browser, memberID, and revisions cannot replace application authentication. Map the application's stable user key to context.userID.

Establish identity in Transport Middleware

The Transport reruns Middleware for every Collaboration HTTP request. An application can read a Cookie, Session, or Bearer token and attach the authenticated result to Context:

TypeScript
transport.use(async (context, next) => {
  const user = await auth.requireUser(context.incomingMessage);

  context.userID = user.id;
  context.customData.user = user;
  context.customData.tenantID = user.tenantID;
  context.customData.traceID = readTraceID(context.incomingMessage);

  await next();
});

On authentication failure, end the HTTP response without calling next():

TypeScript
transport.use(async (context, next) => {
  const user = await auth.findUser(context.incomingMessage);
  if (!user) {
    context.response.statusCode = 401;
    context.response.end("Authentication required");
    return;
  }

  context.userID = user.id;
  await next();
});

The SDK does not prescribe Cookie names, token formats, user tables, or login flows.

How identity enters a WebSocket Session

The WebSocket path does not trust user fields in client payloads. The Endpoint extends an authenticated HTTP Context into a realtime Session through a one-time Session Ticket:

Text
Session Ticket HTTP request
  → Transport Middleware authenticates
  → Endpoint stores { userID, customData }
  → returns opaque one-time ticket
  → WebSocket open consumes ticket
  → creates Session { userID, memberID, customData }

The ticket string does not contain userID or customData. memberID identifies only the current connection and is not an independent identity credential.

Authorize at the correct boundary

Behavior to protectMiddleware
Collaboration HTTP entryTransport use()
WebSocket connectionEndpoint connect
Joining a Unit RoomEndpoint joinUnit
Reading Snapshots, Blocks, or ChangesetsService readUnitData
Submitting content editsService submitChangeset
Creating a UnitService createUnit
Deleting UnitsService deleteUnits
Recovering UnitsService recoverUnits

Endpoint joinUnit only controls whether a Session can enter a realtime Room. Snapshots and missing changesets can be read directly over HTTP, so a JOIN check cannot replace readUnitData. Likewise, a read-only client UI is a product hint, not a replacement for server-side submitChangeset policy.

Protect read, JOIN, and edit

TypeScript
import { CollabError } from "@univerjs-pro/collaboration-service";

endpoint.use("joinUnit", async (context, next) => {
  const allowed = await acl.canRead(context.session.userID, context.unitID);
  if (!allowed) {
    throw new CollabError("PERMISSION_DENIED", "Cannot join this Unit");
  }
  await next();
});

service.use("readUnitData", async (context, next) => {
  const allowed = await acl.canRead(context.userID, context.request.unitID);
  if (!allowed) {
    throw new CollabError("PERMISSION_DENIED", "Unit is not accessible");
  }
  await next();
});

service.use("submitChangeset", async (context, next) => {
  const unitID = context.request.changeset.unitID;
  const allowed = await acl.canEdit(context.userID, unitID);
  if (!allowed) {
    throw new CollabError("PERMISSION_DENIED", "Unit is read-only");
  }
  await next();
});

Real applications typically install all three rules: read policy for HTTP and JOIN, and edit policy for submission.

Protect Unit lifecycle

The collaboration protocol opens existing Units. “Create document” is normally an application API that creates a product record and ACL before calling createUnitFromData() or createUnitFromSnapshot(). Protect the lifecycle through Service Middleware as well:

TypeScript
service.use("createUnit", async (context, next) => {
  if (!(await acl.canCreate(context.userID, context.request.snapshot.type))) {
    throw new CollabError("PERMISSION_DENIED", "Unit creation denied");
  }
  await next();
});

service.use("deleteUnits", async (context, next) => {
  for (const unitID of context.request.unitIDs) {
    if (!(await acl.canDelete(context.userID, unitID))) {
      throw new CollabError("PERMISSION_DENIED", `Cannot delete ${unitID}`);
    }
  }
  await next();
});

Protect recoverUnits separately. A user who once had delete permission does not necessarily retain recovery permission forever.

Reuse lookups through customData

Middleware in the same Service call can use customData to avoid duplicate queries:

TypeScript
service.use("submitChangeset", async (context, next) => {
  const unitID = context.request.changeset.unitID;
  context.customData.role ??= await acl.getRole(context.userID, unitID);

  if (context.customData.role === "viewer") {
    throw new CollabError("PERMISSION_DENIED", "Unit is read-only");
  }
  await next();
});

customData belongs only to the current call or Session and is not automatically persisted. Roles, ACLs, and tenant relationships still belong in application storage.

Extension modules need separate policy

History, Thread Comment, and Worktree have independent Service Middleware:

  • History list and changeset reads should check Unit read access;
  • Comment list should check read access, while add/reply/edit/delete apply Comment policy;
  • Worktree should separately protect visibility, draft edits, status changes, and merge;
  • a Worktree merge into trunk still enters the trunk Service Middleware.

Rules on the core Collaboration Service do not automatically protect these modules. Reuse the same application policy service rather than creating a separate ACL data model for each extension.

See the complete runnable Permissions example.