# How Workspace Token Scoping (Owner, Collaborator, Viewer) Enforces Access Control in OpenWork

> Learn how OpenWork's workspace token scoping (owner, collaborator, viewer) enforces granular access control. Server validates API requests based on embedded role claims.

- Repository: [Different AI/openwork](https://github.com/different-ai/openwork)
- Tags: architecture
- Published: 2026-08-22

---

**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](https://github.com/different-ai/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`](https://github.com/different-ai/openwork/blob/main/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`](https://github.com/different-ai/openwork/blob/main/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](https://github.com/different-ai/openwork/blob/dev/packages/types/src/openwork-context.ts)

```typescript
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.

```typescript
// 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`](https://github.com/different-ai/openwork/blob/main/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`](https://github.com/different-ai/openwork/blob/main/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](https://github.com/different-ai/openwork/blob/dev/dev/scripts/dev-headless-web.ts)

```typescript
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:

```typescript
// 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.

```typescript
// 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 `role` claim set to owner, collaborator, or viewer.
- The token structure is defined in [`packages/types/src/openwork-context.ts`](https://github.com/different-ai/openwork/blob/main/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.ts`](https://github.com/different-ai/openwork/blob/main/evals/specs/workspace-storm-auth-coherence.e2e.test.ts) verify 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`](https://github.com/different-ai/openwork/blob/main/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.