Fine-Grained Tokens

Fine-Grained Tokens

Restrict tunnel projects, tunnel creation, tunnel discovery, and tunnel connections.


Fine-grained tokens let a backend issue credentials that are limited to specific tunnel projects and specific tunnel operations. They are designed for delegation: a trusted backend keeps its personal access token or application credential, then gives a short-lived, narrower token to a browser, device, worker, or tenant service.

Resource Boundaries

Resource boundaries restrict runtime access to project-scoped resources. Today the public contract has one resource family, resources.tunnels, which is shared by tunnel operations, WebTTY, and TURN relay authorization. These boundaries do not hide workspaces, projects, credentials, or billing objects from hosted APIs. Hosted API access is controlled by API permissions, documented in Tokens.

A token with no resources field can use every tunnel project available to its owner, subject to its runtime permissions. A token with resources.tunnels can only use matching projects. The nested resources.tunnels value can target workspaces, target projects, or apply globally when no target field is present.

The signed token and API shape is always:

{
  "resources": {
    "tunnels": {
      "projects": ["project-id"]
    }
  }
}

The examples below focus on the nested resources.tunnels object:

{
  "OR": [
    {
      "workspaces": ["workspace-id"]
    },
    {
      "projects": ["project-id"],
      "scopes": {
        "tunnels": {
          "list": true
        }
      }
    },
    {
      "scopes": {
        "tunnels": {
          "connect": {
            "params": {
              "path": {
                "regex": "^/api"
              }
            }
          }
        }
      }
    }
  ]
}

The Dashboard exposes four resource-boundary modes for stored credentials: all projects, selected workspaces, selected projects, and advanced JSON. The first three modes cover the common cases. Advanced JSON edits the nested resources.tunnels value and is used when boundaries need operation-level scopes or different rules per project.

AND and OR are explicit object operators. A resource object can be a leaf (workspaces, projects, scopes) or a logical branch (AND or OR). resources.tunnels is always encoded as one canonical JSON object; array roots are invalid.

Resource boundaries are enforced by managed engines. Community Edition deployments do not enforce this model.

Scopes

The current scope root is tunnels. It has three capabilities: create, connect, and list. At least one capability must be present when scopes is present. Each capability can be set to true, false, or to an object that limits accepted tunnel properties, connection parameters, or returned fields. Operation-level tunnel scopes are enforced on Basic, Pro, and Enterprise managed projects.

tunnels.create.filters restricts tunnel properties at creation time. If a filter forces a property, the engine applies that property when possible and rejects incompatible requests. tunnels.connect.filters restricts which existing tunnels can be reached. tunnels.connect.params.path restricts accepted HTTP request paths for HTTP tunnels. tunnels.list.select limits the fields returned during tunnel discovery.

{
  "projects": ["project-id"],
  "scopes": {
    "tunnels": {
      "create": {
        "filters": {
          "protocol": "http",
          "publish": true,
          "token_auth": true
        }
      },
      "connect": {
        "params": {
          "path": {
            "regex": "^/api"
          }
        }
      },
      "list": {
        "select": {
          "id": true,
          "name": true,
          "protocol": true
        }
      }
    }
  }
}

String filters accept exact strings, { "exact": "value" }, { "oneof": ["a", "b"] }, and { "regex": "^/api" }. Filter trees also support AND and OR. Label filters use the same matcher per label key.

Effective restrictions

Resource boundaries can exist in two places. Stored credentials can have resources.tunnels in the database. Short-lived auth or application tokens can also carry resources.tunnels in their signed claims.

When both are present, the engine applies both. A token derived from a restricted credential cannot escape the credential boundary. Project settings are also applied as runtime bounds, so the effective rule is the intersection of stored credential boundaries, token boundaries, and project settings.

For example, if a project requires public tunnels to use token auth or rstream Auth, a token cannot create an unauthenticated public tunnel. If a token limits HTTP paths to ^/api, requests outside that path are denied even if the tunnel itself is public.

Remote device example

A common backend flow is to issue a short-lived application token to a remote device. The device only needs to create a tunnel for one project. It does not need to read account resources, manage credentials, or open connections back to other tunnels.

import { createClientCredentialsToken } from "@rstreamlabs/rstream";
 
const { token } = createClientCredentialsToken(
  {
    clientId: process.env.RSTREAM_CLIENT_ID!,
    clientSecret: process.env.RSTREAM_CLIENT_SECRET!,
  },
  {
    expiresInSeconds: 60,
    claims: {
      permissions: ["tunnels.tunnels.create-delete"],
      resources: {
        tunnels: {
          projects: ["project-id"],
          scopes: {
            tunnels: {
              create: {
                filters: {
                  protocol: "http",
                  publish: true,
                },
              },
            },
          },
        },
      },
    },
  },
);

The resulting token can create matching HTTP tunnels for project-id. It cannot list projects through the hosted API unless API permissions are also granted, and it cannot connect to tunnels because no tunnels.streams.create-delete permission is present.

Typical backend resources

Create one tunnel

Use this when a device or worker should publish exactly one HTTP tunnel shape. The token can create matching tunnels only; it cannot list tunnels or connect to existing tunnels unless those API permissions and scopes are also present.

{
  "projects": ["project-id"],
  "scopes": {
    "tunnels": {
      "create": {
        "filters": {
          "name": { "exact": "device-123" },
          "protocol": "http",
          "publish": true,
          "token_auth": true,
          "labels": {
            "app": "video-platform",
            "device": "device-123",
            "user": "user-456"
          }
        }
      }
    }
  }
}

Connect to selected tunnels

Use this for browser viewers, tenant-scoped clients, or service consumers. The path restriction is checked on HTTP requests that enter the protected published tunnel.

{
  "projects": ["project-id"],
  "scopes": {
    "tunnels": {
      "connect": {
        "filters": {
          "status": "online",
          "protocol": "http",
          "publish": true,
          "labels": {
            "app": "video-platform",
            "device": "device-123",
            "user": "user-456"
          }
        },
        "params": {
          "path": { "regex": "^/ws$" }
        }
      }
    }
  }
}

Watch selected tunnels

Use this when a backend or dashboard needs live state without broad tunnel access. select limits the returned fields.

{
  "projects": ["project-id"],
  "scopes": {
    "tunnels": {
      "list": {
        "filters": {
          "protocol": "http",
          "publish": true,
          "labels": {
            "app": "video-platform",
            "user": "user-456"
          }
        },
        "select": {
          "id": true,
          "status": true,
          "name": true,
          "hostname": true,
          "labels": true
        }
      }
    }
  }
}

Combine target and operation rules

Use AND when every condition must be true. Use OR when any branch may allow access. This example allows listing tunnels in one workspace and connecting only to a narrow HTTP path in one project.

{
  "OR": [
    {
      "workspaces": ["workspace-id"],
      "scopes": {
        "tunnels": {
          "list": true
        }
      }
    },
    {
      "AND": [
        { "projects": ["project-id"] },
        {
          "scopes": {
            "tunnels": {
              "connect": {
                "params": {
                  "path": { "regex": "^/tenant-a/" }
                }
              }
            }
          }
        }
      ]
    }
  ]
}

API example

The Control plane API can mint a short-lived auth token with the same model. Passing permissions narrows API and tunnel permissions. Passing resources.tunnels narrows the tunnel projects and runtime scope.

curl -X POST "https://rstream.io/api/tokens" \
  -H "Authorization: Bearer <token-with-api.tokens.create>" \
  -H "Content-Type: application/json" \
  -d '{
    "permissions": ["tunnels.tunnels.create-delete"],
    "resources": {
      "tunnels": {
        "projects": ["project-id"],
        "scopes": {
          "tunnels": {
            "create": {
              "filters": {
                "protocol": "http",
                "publish": true,
                "token_auth": true
              }
            }
          }
        }
      }
    }
  }'

Use short expirations for delegated tokens. A one-minute token is usually enough for browser bootstrap, device handoff, and other flows where a backend remains responsible for authorization.