How Workspace Token Scoping (Owner, Collaborator, Viewer) Enforces Access Control in OpenWork
OpenWork issues signed JWT workspace tokens that embed owner, collaborator, or viewer role claims, and the server validates these scopes on every API request to enforce granular, tiered access control.
Workspace token scoping is the primary authorization mechanism in the OpenWork repository. Each workspace session operates under a cryptographically signed JWT that carries a specific role—owner, collaborator, or viewer—which determines exactly which REST endpoints and resources the bearer can access. According to the OpenWork source code, this role-based approach allows the server to distinguish between administrative actions, collaborative editing, and read-only observation without relying on external identity providers.
The Three Workspace Token Scopes
OpenWork defines three discrete permission tiers in the JWT payload. The server checks the role claim against endpoint-specific requirements before processing any request.
Owner Scope
The owner scope grants unrestricted administrative control over the workspace. Owners can create and delete workspaces, rename projects, manage member invitations, issue MCP (Model Context Protocol) tokens, and execute admin-level operations such as publishing plugins. During local development, the owner bearer is persisted in tmp/dev-head-headless-web.json to survive server restarts.
Collaborator Scope
The collaborator scope permits read and write operations within the workspace—adding files, running agents, and editing settings—but explicitly blocks administrative actions. Collaborators cannot transfer ownership, delete the workspace, manage other members, or mint MCP tokens. This scope is designed for team members who need to contribute content without accessing security-critical controls.
Viewer Scope
The viewer scope provides read-only access. Bearers can open the workspace, inspect file contents, and trigger limited agent runs in a restricted mode, but any attempt to modify persisted state is rejected. This scope suits stakeholders or auditors who require visibility without the ability to alter project data.
Token Structure and Claims
The workspace token structure is defined in packages/types/src/openwork-context.ts, where the JWT payload schema enforces the role enumeration. The token includes the workspace ID as the subject (sub) and the access tier as the role claim.
Source: packages/types/src/openwork-context.ts
import { z } from 'zod';
export const WorkspaceTokenSchema = z.object({
sub: z.string(), // Workspace UUID
role: z.enum(["owner", "collaborator", "viewer"]),
exp: z.number(), // Unix timestamp
iat: z.number(), // Issued at
});
export type WorkspaceToken = z.infer<typeof WorkspaceTokenSchema>;
When the server generates a token, it signs the payload with its private key using jsonwebtoken. Clients cannot forge elevated privileges because they lack the signing secret, ensuring that the role claim is tamper-proof.
Server-Side Access Control Enforcement
The OpenWork server enforces scoping through middleware that intercepts every request to a workspace-related route (typically prefixed with /workspace/). The verifyWorkspaceToken middleware extracts the Bearer token from the Authorization header, verifies the cryptographic signature, and validates that the embedded role satisfies the endpoint's minimum requirement.
// Conceptual implementation based on packages/server/src/middleware/
import jwt from 'jsonwebtoken';
type Role = 'owner' | 'collaborator' | 'viewer';
const ROLE_HIERARCHY: Record<Role, Role[]> = {
owner: ['owner'],
collaborator: ['owner', 'collaborator'],
viewer: ['owner', 'collaborator', 'viewer']
};
export function verifyWorkspaceToken(requiredRole: Role) {
return (req: Request, res: Response, next: NextFunction) => {
const authHeader = req.headers.authorization;
const token = authHeader?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Unauthorized' });
}
try {
const payload = jwt.verify(token, process.env.OPENWORK_JWT_SECRET!) as WorkspaceToken;
const allowedRoles = ROLE_HIERARCHY[requiredRole];
if (!allowedRoles.includes(payload.role)) {
return res.status(403).json({ error: 'Forbidden: insufficient scope' });
}
req.workspace = { id: payload.sub, role: payload.role };
next();
} catch (err) {
return res.status(401).json({ error: 'Invalid token' });
}
};
}
Routes in packages/server/src/api/workspace.ts apply this middleware with specific role requirements. For example, the DELETE /workspace/:id endpoint requires verifyWorkspaceToken('owner'), while PATCH /workspace/:id/files/* accepts 'owner' or 'collaborator'.
Practical Implementation Examples
Generating Owner Tokens in Development
The development script dev/scripts/dev-headless-web.ts demonstrates how the server mints an owner-scoped token when initializing a headless workspace session. This token is injected into the server configuration to grant the UI full administrative access during local development.
Source: dev/scripts/dev-headless-web.ts
import jwt from 'jsonwebtoken';
const workspaceId = 'ws-dev-123';
const ownerToken = jwt.sign(
{
sub: workspaceId,
role: 'owner',
exp: Math.floor(Date.now() / 1000) + (24 * 60 * 60) // 24 hours
},
process.env.OPENWORK_JWT_SECRET!
);
// Persisted for server relaunches
const mergedConfig = {
workspaces: [{ path: workspaceRoot }],
bearer: ownerToken
};
Protecting Administrative Endpoints
Server routes explicitly declare their scope requirements. The following pattern from the workspace API demonstrates how owner-only operations are protected:
// packages/server/src/api/workspace.ts
import { Router } from 'express';
import { verifyWorkspaceToken } from '../middleware/verifyWorkspaceToken';
const router = Router();
// Owner only: Delete workspace
router.delete(
'/workspace/:id',
verifyWorkspaceToken('owner'),
async (req, res) => {
await deleteWorkspace(req.params.id);
res.status(204).send();
}
);
// Owner or Collaborator: Update files
router.patch(
'/workspace/:id/files/*',
verifyWorkspaceToken('collaborator'),
async (req, res) => {
await updateFile(req.params.id, req.body);
res.json({ success: true });
}
);
Downgrading Tokens for Client Distribution
When the server issues tokens to downstream clients or browser sessions, it can derive a viewer token from the same workspace context, stripping away administrative claims. This ensures that even if the client token is compromised, the attacker gains only read-only access.
// Issuing a viewer token for public sharing
const viewerToken = jwt.sign(
{
sub: workspaceId,
role: 'viewer',
exp: Math.floor(Date.now() / 1000) + 3600 // 1 hour
},
process.env.OPENWORK_JWT_SECRET!
);
Summary
- Workspace token scoping relies on a signed JWT containing a
roleclaim set to owner, collaborator, or viewer. - The token structure is defined in
packages/types/src/openwork-context.ts, ensuring type safety across the monorepo. - Server middleware (
verifyWorkspaceToken) validates the signature and enforces role hierarchies before processing requests to/workspace/*endpoints. - Owner tokens enable full administrative control, including workspace deletion and MCP token minting.
- Collaborator tokens permit content modifications but block ownership management.
- Viewer tokens provide read-only access, suitable for auditing and public sharing.
- End-to-end tests in
evals/specs/workspace-storm-auth-coherence.e2e.test.tsverify that the scoping logic prevents unauthorized privilege escalation.
Frequently Asked Questions
How does OpenWork prevent privilege escalation via token manipulation?
OpenWork signs all workspace tokens using a server-side secret (OPENWORK_JWT_SECRET) via the jsonwebtoken library. Because the private key never leaves the server, clients cannot forge or modify the role claim to elevate their permissions. The server verifies the signature on every request, and any tampered token fails cryptographic validation, resulting in a 401 Unauthorized response.
Can a collaborator upgrade their own token to owner?
No. Token scoping is strictly server-side. A collaborator cannot self-promote because the upgrade requires the server to issue a new JWT with an owner role, which only an existing owner can authorize through the membership management endpoints protected by verifyWorkspaceToken('owner').
What happens when a token with insufficient scope attempts a restricted action?
The server's role-based middleware returns a 403 Forbidden status with a minimal error message. For example, if a viewer token is used to call DELETE /workspace/:id, the middleware detects that viewer is not in the ROLE_HIERARCHY['owner'] array and terminates the request before it reaches the route handler.
Where are workspace tokens stored during local development?
During development, owner tokens are persisted in tmp/dev-headless-web.json as part of the server configuration object. This allows the OpenWork UI to maintain administrative access across server restarts without requiring re-authentication. Production deployments typically store these tokens in secure environment variables or encrypted session stores rather than the filesystem.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →