Before submitting
Affected area
Developer experience / tooling
Layer
API (apps/api), Database / Prisma, Frontend (apps/app)
Problem
A user recently automated document creation on the site by copying their connect.sid cookie out of browser devtools and pasting it into a script. It worked — which is the problem. We offer no supported way to script against the API, so the only path available to a motivated user is to extract and reuse their browser session credential.
That credential is a poor fit for automation:
- Full account authority. The session grants everything the user can do in the UI. A script that only needs to create documents can also delete content, change account settings, and read everything the user can read.
- Long-lived. The session cookie is configured with
maxAge: 365 * 24 * 60 * 60 * 1000 — one year (apps/api/src/index.ts). A cookie pasted into a script, committed to a repo, or shared with a collaborator stays valid for a very long time.
- No revocation surface. There is no UI listing active sessions, so a user who suspects a leaked cookie has no way to invalidate it short of asking us to clear rows from the session store.
- No attribution. Requests from a script are indistinguishable from requests from the browser in logs and rate limiting, so we can't scope, meter, or audit automated traffic.
- Encourages worse habits. Once the pattern spreads, cookies get shared between people and end up in shell history, CI config, and Slack.
This is not hypothetical demand — people want to bulk-create and manage content programmatically, and they'll keep reaching for the cookie until there's something better.
Proposed solution
Add first-class, scoped API tokens as the supported automation path.
Token issuance and storage
- New Prisma model (
ApiToken): id, userId, label, hashed secret, scopes, createdAt, lastUsedAt, expiresAt, revokedAt.
- Store only a hash of the secret (e.g. SHA-256 of a high-entropy random token); show the plaintext exactly once at creation.
- Prefix tokens (
doenet_pat_…) so they're greppable in secret scanners and recognizable in a paste.
Authentication
- Accept
Authorization: Bearer <token> as an alternative to the session cookie via a Passport strategy alongside the existing ones, resolving to the same UserInfoWithEmail shape so downstream handlers are unchanged.
- Reject bearer-token requests to auth/session endpoints (login, magic link, account deletion) — tokens are for content operations, not identity changes.
Scopes
- Start with a small, coarse set that covers real use:
content:read, content:write, assignments:read, assignments:write. A token limited to content:write is a dramatically smaller blast radius than a session cookie.
Lifecycle and visibility
- Account settings page to create, label, view (last-used timestamp, scopes), and revoke tokens.
- Default expiry (90 days?) with explicit opt-in to longer.
- Separate rate limits for token traffic, and tag tokened requests in logs.
Hardening the current path
- Consider
sameSite and shorter session lifetimes independently — a one-year session cookie is generous even for browser use.
- Optionally reject requests that present a session cookie with no matching
Origin/Referer from our own domains on write endpoints, which makes the copied-cookie approach stop working once a supported alternative exists. (Sequencing matters: ship tokens first, then tighten.)
Documentation
- A short "Automating Doenet" doc showing token creation and a
curl example, plus an explicit "never paste your session cookie into a script" note.
Alternatives considered
- OAuth 2.0 / app authorization. The right answer if third-party applications ever act on behalf of Doenet users, but heavyweight for the actual need here — a user scripting against their own account. Personal tokens can be a stepping stone; the scope model carries over.
- Do nothing / document the cookie approach. Cheapest, but blesses a credential with full authority, a one-year life, and no revocation.
- Service accounts. Useful for institutional/LMS integrations, but doesn't help an individual author automating their own content.
Additional context
Relevant code: session config in apps/api/src/index.ts, Passport strategies and SessionUser shapes in apps/api/src/auth/. Session storage uses @quixo3/prisma-session-store, so revocation tooling would need to reach that table too.
Before submitting
Affected area
Developer experience / tooling
Layer
API (apps/api), Database / Prisma, Frontend (apps/app)
Problem
A user recently automated document creation on the site by copying their
connect.sidcookie out of browser devtools and pasting it into a script. It worked — which is the problem. We offer no supported way to script against the API, so the only path available to a motivated user is to extract and reuse their browser session credential.That credential is a poor fit for automation:
maxAge: 365 * 24 * 60 * 60 * 1000— one year (apps/api/src/index.ts). A cookie pasted into a script, committed to a repo, or shared with a collaborator stays valid for a very long time.This is not hypothetical demand — people want to bulk-create and manage content programmatically, and they'll keep reaching for the cookie until there's something better.
Proposed solution
Add first-class, scoped API tokens as the supported automation path.
Token issuance and storage
ApiToken): id,userId, label, hashed secret, scopes,createdAt,lastUsedAt,expiresAt,revokedAt.doenet_pat_…) so they're greppable in secret scanners and recognizable in a paste.Authentication
Authorization: Bearer <token>as an alternative to the session cookie via a Passport strategy alongside the existing ones, resolving to the sameUserInfoWithEmailshape so downstream handlers are unchanged.Scopes
content:read,content:write,assignments:read,assignments:write. A token limited tocontent:writeis a dramatically smaller blast radius than a session cookie.Lifecycle and visibility
Hardening the current path
sameSiteand shorter session lifetimes independently — a one-year session cookie is generous even for browser use.Origin/Refererfrom our own domains on write endpoints, which makes the copied-cookie approach stop working once a supported alternative exists. (Sequencing matters: ship tokens first, then tighten.)Documentation
curlexample, plus an explicit "never paste your session cookie into a script" note.Alternatives considered
Additional context
Relevant code: session config in
apps/api/src/index.ts, Passport strategies andSessionUsershapes inapps/api/src/auth/. Session storage uses@quixo3/prisma-session-store, so revocation tooling would need to reach that table too.