Authentication
In a collaborative editing system, authentication is not just about knowing who a user is — it determines what documents they can access, what operations they can perform, and how their actions are attributed. Unlike a traditional web app where a session cookie gates access to pages, a sync server must authorize every individual message: each keystroke, cursor move, or file upload passes through a permission check.
Teleportal provides two approaches: a built-in JWT token system with IAM-like document access patterns, or a bring-your-own-auth hook for integrating with your existing identity provider.
Custom Authentication
Section titled “Custom Authentication”You can bypass the built-in token system entirely by implementing your own logic in the onUpgrade and checkPermission hooks. This is useful when you already have session management (cookies, OAuth tokens, API keys) and don’t want to introduce a second token format.
The onUpgrade Hook
Section titled “The onUpgrade Hook”The onUpgrade hook runs when a WebSocket connection is first established. It receives the HTTP request and returns a context object that is attached to the connection for its lifetime:
const handlers = getWebsocketHandlers({ onUpgrade: async (request) => { // Use your existing session/auth system const user = await verifySession(request); if (!user) throw new Response("Unauthorized", { status: 401 });
return { context: { userId: user.id, role: user.role } }; },});The checkPermission Hook
Section titled “The checkPermission Hook”The checkPermission hook runs before every message is processed. It receives the connection context (from onUpgrade), the target document or file, and the message itself:
const server = new Server({ storage: async (ctx) => documentStorage, checkPermission: async ({ context, documentId, fileId, message, rpcMethod }) => { // Derive read/write intent from the MESSAGE, not the `type` argument. // (For inbound client messages `type` is always "write" — see the caveat below.) const isWrite = message.type === "doc" && (message.payload.type === "sync-step-2" || message.payload.type === "update"); return await yourAuthService.canAccess(context.userId, documentId, isWrite ? "write" : "read"); },});This two-hook design separates identity verification (once, at connection time) from authorization (continuously, per message).
Built-in JWT Token System
Section titled “Built-in JWT Token System”For applications that need a self-contained permission model, Teleportal provides a JWT-based token manager with document-level access control patterns.
How It Works
Section titled “How It Works”The token lifecycle:
- Your application server creates a token using
TokenManager, encoding the user’s identity and document access rules - The client passes this token when connecting (via query parameter or Authorization header)
- The
onUpgradehook verifies the token signature, expiration, and claims - The
checkPermissionhook checks the token’s document access patterns against each incoming message
Token Structure
Section titled “Token Structure”Each token contains:
{ userId: string; // User identifier room: string; // Room/organization identifier documentAccess?: [ // Document access patterns (OPTIONAL — see gotcha below) { pattern: string; // Document pattern (`*` is the only wildcard) permissions: Permission[]; } ]; exp?: number; // Expiration time (Unix timestamp) — set + enforced by verifyToken iat?: number; // Issued at time (Unix timestamp) iss?: string; // Issuer — set + enforced aud?: string; // Audience (default: "teleportal") — set + enforced}The room field scopes the token to a single room or organization, preventing a token issued for one tenant from being used in another.
Permission Types
Section titled “Permission Types”read: View document content and receive awareness updateswrite: Modify document contentcomment: Add comments to documentssuggest: Make suggestions for document changesadmin: Full access to all operations (supersedes other permissions)
Creating Tokens
Section titled “Creating Tokens”import { createTokenManager } from "teleportal/token";
const tokenManager = createTokenManager({ secret: "your-secret-key-here", expiresIn: 3600, // 1 hour issuer: "my-collaborative-app",});
// Token with explicit access patternsconst token = await tokenManager.createToken("user-123", "org-456", [ { pattern: "shared/*", permissions: ["read", "comment"] }, { pattern: "user-123/*", permissions: ["read", "write", "admin"] },]);
// Admin token (full access to all documents in the room)const adminToken = await tokenManager.createAdminToken("admin-789", "org-456");Verifying Tokens and Checking Permissions
Section titled “Verifying Tokens and Checking Permissions”const result = await tokenManager.verifyToken(token);if (result.valid && result.payload) { // Check a specific permission against a document const canRead = tokenManager.hasDocumentPermission(result.payload, "user-123/document1", "read");
// Get all permissions the token grants for a document const permissions = tokenManager.getDocumentPermissions(result.payload, "user-123/document1");}Document Pattern Matching
Section titled “Document Pattern Matching”The permission system uses pattern matching to map tokens to documents, similar to IAM policy resources. This avoids needing to enumerate every document in the token.
* is the only wildcard — it matches any run of characters (including none). Every other character is matched literally. Patterns are compiled to an anchored RegExp with all regex metacharacters escaped, so a pattern like logs[prod]* matches the literal text logs[prod]… and is never treated as a regex character class. This escaping is a security boundary: neither patterns nor document names can inject regex syntax.
Exact Match
Section titled “Exact Match”pattern: "document1";// Matches: "document1" onlyPrefix Match
Section titled “Prefix Match”pattern: "user/*";// Matches: "user/doc1", "user/doc2", "user/project/doc3"Wildcard Match
Section titled “Wildcard Match”pattern: "*";// Matches: any document nameSuffix Match
Section titled “Suffix Match”pattern: "*.md";// Matches: "readme.md", "document.md"Patterns compose naturally with the permission system: a token can grant read to shared/* and admin to user-123/*, giving fine-grained control without per-document token issuance.
DocumentAccessBuilder
Section titled “DocumentAccessBuilder”For complex permission scenarios, the DocumentAccessBuilder provides a fluent API to construct DocumentAccess[] arrays:
import { DocumentAccessBuilder } from "teleportal/token";
// Basic allow/denyconst access = new DocumentAccessBuilder() .allow("user/*", ["read", "write"]) .deny("private/*") .build();
// Permission convenience methodsconst access = new DocumentAccessBuilder() .readOnly("public/*") .write("user/*") .fullAccess("admin/*") .admin("super-admin/*") .build();
// User-owned documents + custom patternsconst access = new DocumentAccessBuilder() .ownDocuments("user-123") .allow("shared/*", ["read", "write"]) .allow("projects/my-project/*", ["read", "write"]) .build();
// Complex patterns with exclusionsconst access = new DocumentAccessBuilder() .allowAll(["read", "write"]) .deny("private/*") .deny("*.secret") .ownDocuments("user-456", ["read", "write", "comment", "suggest", "admin"]) .allow("projects/important-project/*", ["read", "write", "comment", "suggest"]) .admin("system/*") .build();Server Integration
Section titled “Server Integration”Here is a complete example integrating the token manager with the WebSocket server:
import { getWebsocketHandlers } from "teleportal/websocket-server";import { Server } from "teleportal/server";import { createTokenManager } from "teleportal/token";
const tokenManager = createTokenManager({ secret: "your-secret-key", expiresIn: 3600,});
const server = new Server({ storage: async (ctx) => documentStorage, checkPermission: async ({ context, documentId, fileId, message, type }) => { const token = (context as any).token; if (!token) return false;
const result = await tokenManager.verifyToken(token); if (!result.valid || !result.payload) return false;
// Verify room scope if (result.payload.room !== context.room) return false;
// Check document permissions if (documentId) { const requiredPermission = message.type === "awareness" ? "read" : "write"; return tokenManager.hasDocumentPermission(result.payload, documentId, requiredPermission); }
return true; },});
const handlers = getWebsocketHandlers({ onUpgrade: async (request) => { const url = new URL(request.url); const authHeader = request.headers.get("authorization"); const token = url.searchParams.get("token") || (authHeader && /^bearer\s+/i.test(authHeader) ? authHeader.replace(/^bearer\s+/i, "") : null);
if (!token) { throw new Response("No token provided", { status: 401 }); }
const result = await tokenManager.verifyToken(token); if (!result.valid || !result.payload) { throw new Response("Invalid token", { status: 401 }); }
return { context: { userId: result.payload.userId, room: result.payload.room, token, }, }; },});Security Considerations
Section titled “Security Considerations”- Use strong secrets (≥256 bits): Generate cryptographically secure random secrets for signing tokens.
josedoes not enforce a minimum HMAC key length — that is on you. - Algorithm is pinned to HS256: Tokens are always signed and verified with HS256 (symmetric HMAC-SHA-256).
verifyTokenpinsalgorithms: ["HS256"], so HS384/HS512 tokens and unsecuredalg: "none"tokens are rejected. - Always attach a
documentAccesspolicy: A token minted without one fails open and grants room-wide admin access (see the danger callout above). UsecreateAdminTokenonly when you actually mean room-wide access. - Set appropriate expiration: Short-lived tokens limit the window of compromise. Prefer tokens that expire in minutes to hours, not days.
- Validate room access:
hasDocumentPermissionignorespayload.room— you must compare the token’sroomclaim to the connection’s room yourself. - Check permissions on every message: The
checkPermissionhook runs per message by design — do not cache authorization results, as tokens may have been revoked. Remember itstypeargument is always"write"; derive read/write intent from the message. - Use HTTPS/WSS: Always use secure connections in production to prevent token interception.
- Rotate secrets: Regularly rotate your JWT signing secrets.
Next Steps
Section titled “Next Steps”- Server - How the server uses authentication for permission enforcement
- Guides: Authentication - Step-by-step setup guide
- Attribution - How userId from authentication flows into authorship tracking