16 KiB
status, system, created, owners
| status | system | created | owners | |
|---|---|---|---|---|
| active | auth | 2026-07-24 |
|
Opt-in Authentication & Multi-User Segregation Requirements
Overview
Kandev ships as a single-user local tool with zero authentication: the backend binds 0.0.0.0 by default and anyone who can reach the port owns the instance and every agent credential on it. Teams who share a Kandev server (VPS, homelab, office box) need user accounts and privacy between users — without burdening the local single-user install with a login screen it never asked for.
Requirements
REQ-AUTH-AUTH-001: Opt-in Authentication & Multi-User Segregation
Intent: Kandev ships as a single-user local tool with zero authentication: the backend binds 0.0.0.0 by default and anyone who can reach the port owns the instance and every agent credential on it. Teams who share a Kandev server (VPS, homelab, office box) need user accounts and privacy between users — without burdening the local single-user install with a login screen it never asked for.
Acceptance criteria
- AC-AUTH-AUTH-001.1: Opt-in. Authentication is OFF by default. A disabled instance behaves byte-identically to pre-auth Kandev — no login, no auth UI.
- AC-AUTH-AUTH-001.2: Enablement is a runtime feature toggle. Authentication is turned on/off through the
features.authruntime flag ("Authentication & users" inSettings > System > Feature Toggles, orKANDEV_FEATURES_AUTH=true) — a restart-required flag like the other feature toggles, with no separate Authentication settings page. Turning it on (and restarting) enters setup mode: the first visitor completes a wizard (email + password) and becomes the admin. The wizard promotes the existing single-user profile — settings, workspaces, and secrets carry over to the admin account. The effective mode is derived:flag off → disabled,flag on + no admin → setup,flag on + admin exists → enabled. - AC-AUTH-AUTH-001.3: Accounts. Local email + password (argon2id). The identity schema is provider-based so OIDC/SSO can be added later without migration. Roles:
admin(user management, auth settings, destructive system operations, feature toggles) andmember. Users can be disabled (revokes all sessions and tokens); the last active admin cannot be demoted or disabled. - AC-AUTH-AUTH-001.4: Sessions & tokens. Browser sessions are opaque cookies with the base session-cookie name
kandev_session(effective name is request-host derived — see the port-scoping bullet below), HttpOnly, SameSite=Lax, sliding 30-day expiry, DB-backed and revocable fromSettings > Account. Programmatic clients (external MCP, scripts, CI) use personal access tokens (kandev_pat_…, shown once, revocable). - AC-AUTH-AUTH-001.5: Cookie name is port-scoped when the instance is served on a non-default port. Cookies ignore port when deciding scope, so two auth-enabled instances on one host (same IP, different ports) would otherwise overwrite each other's
kandev_sessionand yank every instance to/loginon the next request. The backend derives the session cookie name from the request host:kandev_session_<port>on a ported host, plainkandev_sessionon a default-port host (seefix-multi-instance-cookie-isolation). An explicitauth.cookieNameconfig value is used verbatim. The legacy unprefixed cookie is deliberately not read (one re-login after upgrade keeps the cross-instance conflict dead). - AC-AUTH-AUTH-001.6: Invites. Admins mint tokenized invite URLs (
/invite?token=…, optional pinned email, member/admin role, 7-day default expiry, single use). No email server required. Admins can also create accounts directly. - AC-AUTH-AUTH-001.7: Segregation: per-user workspaces. Every workspace has one owner. Users see and touch only their own workspaces — tasks, sessions, repositories, workflows, plans, walkthroughs, terminals, VS Code, port previews, git snapshots, session turns/context, secrets, third-party integration settings (GitHub/GitLab/Jira/Linear/Sentry/Slack/Azure), automations, and WebSocket events included. Cross-user access returns 404 — knowing an ID from another user's workspace does not grant access. Admins do NOT see other users' workspaces (hard privacy; admin is a management role, not a visibility role). Pre-auth data is claimed by the admin at setup.
- AC-AUTH-AUTH-001.8: Task lifecycle mutations are ownership-checked at the service layer. Changing a task's state, moving it between workflows/steps, updating a task repository's base branch, and archiving or deleting a task (including the cascade that walks its subtree) all refuse a foreign task before any read or write reaches the repository. This holds on every transport — HTTP, WebSocket and MCP — because the check lives below them, and it holds for long-running deletes, which detach from request cancellation but keep the caller identity.
Migrated source detail
Why
Kandev ships as a single-user local tool with zero authentication: the backend
binds 0.0.0.0 by default and anyone who can reach the port owns the instance
and every agent credential on it. Teams who share a Kandev server (VPS, homelab,
office box) need user accounts and privacy between users — without burdening
the local single-user install with a login screen it never asked for.
What
- Opt-in. Authentication is OFF by default. A disabled instance behaves byte-identically to pre-auth Kandev — no login, no auth UI.
- Enablement is a runtime feature toggle. Authentication is turned on/off
through the
features.authruntime flag ("Authentication & users" inSettings > System > Feature Toggles, orKANDEV_FEATURES_AUTH=true) — a restart-required flag like the other feature toggles, with no separate Authentication settings page. Turning it on (and restarting) enters setup mode: the first visitor completes a wizard (email + password) and becomes the admin. The wizard promotes the existing single-user profile — settings, workspaces, and secrets carry over to the admin account. The effective mode is derived:flag off → disabled,flag on + no admin → setup,flag on + admin exists → enabled. - Accounts. Local email + password (argon2id). The identity schema is
provider-based so OIDC/SSO can be added later without migration. Roles:
admin(user management, auth settings, destructive system operations, feature toggles) andmember. Users can be disabled (revokes all sessions and tokens); the last active admin cannot be demoted or disabled. - Sessions & tokens. Browser sessions are opaque cookies with the base
session-cookie name
kandev_session(effective name is request-host derived — see the port-scoping bullet below), HttpOnly, SameSite=Lax, sliding 30-day expiry, DB-backed and revocable fromSettings > Account. Programmatic clients (external MCP, scripts, CI) use personal access tokens (kandev_pat_…, shown once, revocable). - Cookie name is port-scoped when the instance is served on a non-default
port. Cookies ignore port when deciding scope, so two auth-enabled
instances on one host (same IP, different ports) would otherwise overwrite
each other's
kandev_sessionand yank every instance to/loginon the next request. The backend derives the session cookie name from the request host:kandev_session_<port>on a ported host, plainkandev_sessionon a default-port host (seefix-multi-instance-cookie-isolation). An explicitauth.cookieNameconfig value is used verbatim. The legacy unprefixed cookie is deliberately not read (one re-login after upgrade keeps the cross-instance conflict dead). - Invites. Admins mint tokenized invite URLs (
/invite?token=…, optional pinned email, member/admin role, 7-day default expiry, single use). No email server required. Admins can also create accounts directly. - Segregation: per-user workspaces. Every workspace has one owner. Users see and touch only their own workspaces — tasks, sessions, repositories, workflows, plans, walkthroughs, terminals, VS Code, port previews, git snapshots, session turns/context, secrets, third-party integration settings (GitHub/GitLab/Jira/Linear/Sentry/Slack/Azure), automations, and WebSocket events included. Cross-user access returns 404 — knowing an ID from another user's workspace does not grant access. Admins do NOT see other users' workspaces (hard privacy; admin is a management role, not a visibility role). Pre-auth data is claimed by the admin at setup.
- Task lifecycle mutations are ownership-checked at the service layer. Changing a task's state, moving it between workflows/steps, updating a task repository's base branch, and archiving or deleting a task (including the cascade that walks its subtree) all refuse a foreign task before any read or write reaches the repository. This holds on every transport — HTTP, WebSocket and MCP — because the check lives below them, and it holds for long-running deletes, which detach from request cancellation but keep the caller identity.
- The WebSocket backstop reads
idon top-leveltask.*actions. In addition totask_id/session_id/task_environment_id, an action namedtask.<verb>(exactly two segments —task.state,task.move,task.get, …) has its payloadidtreated as a task ID. Deeper namespaces (task.plan.*,task.review.*) are untouched: theiridnames a plan, revision or finding, not a task. - Shared surfaces (instance-global by design). Executors, executor profiles, environment definitions, agent profiles/agents, editors, prompts, sprites, voice, notification providers, and system pages remain shared across the instance; mutation of system settings and feature toggles is admin-only when auth is enabled. Note: a shared agent profile means all users share the agent's provider credentials — see the OS-level limit below.
- Public endpoints.
/health, the SPA shell/static assets, the boot payload (/api/v1/app-state— returns only{features, auth}for anonymous visitors),/api/v1/features, credential endpoints (login/setup/invite-accept), and self-authenticating webhooks (automationX-Webhook-Secret, office channel HMAC). The GitHub credential broker (/api/v1/github/credentials/resolve, GET readiness + POST resolve) is likewise public: containers and remote executors hold no session cookie or PAT by design, and the opaque, task-scoped lease in the request body — hashed at rest, TTL'd, scope-matched on redeem — is the self-authenticating credential. The GitHub App webhook (/api/v1/github/app/registrations/{id}/webhook) is public for the same reason as the other webhooks: its own HMAC (X-Hub-Signature-256) is verified by the handler. - Plugin webhooks (
/api/plugins/{id}/webhooks/{key}) are NOT self-authenticating by default. The global middleware cannot read the plugin registry, so it structurally defers GET/POST requests for the path;plugins.Controllerenforces the actual gate: a webhook requires a real caller identity (session/PAT, or the synthetic identity while auth is disabled) unless its manifest declareswebhooks[].public: true. An anonymous caller sees 401 for an unknown plugin ID, an undeclared key, and a declared-non-public key alike, so a caller with no identity cannot enumerate installed plugins. - Session challenges are distinct from provider authentication failures.
The browser clears its Kandev identity and opens
/loginonly when a 401 is a Kandev session challenge. A third-party integration may also return 401 when GitHub, GitLab, Jira, or Linear rejects its own credential; that failure remains on the current page and is rendered by the integration surface. - Disable. An admin can turn auth off again (unless env-forced). Ownership data is retained; everyone reaching the instance has full access again.
API surface
/api/v1/auth/*: setup, login, logout, me, password, sessions,
tokens, invites (+ preview, accept), settings. /api/v1/users
(admin CRUD: list, create, role/status). WS: cookie on same-origin upgrade or
?token=<PAT>; subscriptions and RPC actions are scoped to the caller.
Failure modes
- Wrong credentials and unknown emails return the same generic 401; login is rate-limited (10 attempts / 5 min per IP+email).
- Foreign workspaces/tasks read as 404 — existence is not leaked.
- A server bound to non-loopback interfaces with auth disabled logs a prominent startup warning.
- Sessions/PATs of disabled users fail closed immediately.
- Third-party provider authentication failures do not clear the authenticated Kandev user, replace the current route, or trigger a login redirect.
Scenarios
- GIVEN two users A and B and a task owned by B, WHEN A sends
task.stateortask.moveover the WebSocket naming that task byid, THEN the call is refused, B's task DTO is not serialized into the response, and no state, position or workflow write reaches the repository. - GIVEN the same pair, WHEN A sends
DELETE /api/v1/tasks/{B's task}orPOST /api/v1/tasks/{B's task}/archive, THEN the response is 404 and no task row, subtree row, or workspace-group membership is touched — while the owner's own delete still runs to completion if the owner's browser disconnects mid-request. - GIVEN an authenticated Kandev browser session, WHEN a protected API
request receives a Kandev session challenge, THEN the browser clears the
stale Kandev identity and navigates to
/login. - GIVEN two auth-enabled Kandev instances A (port 8080) and B (port
8081) on the same host, WHEN the user logs into B, THEN A's session
keeps working and A's next page load authenticates with A's own
kandev_session_8080cookie rather than showing the Sign in page. - GIVEN an authenticated Kandev browser session and an expired or invalid third-party integration credential, WHEN GitHub, GitLab, Jira, or Linear data loading returns 401 without a Kandev session challenge, THEN the browser stays on the integration route and displays the provider loading error.
Known v1 limits
- Filesystem / agent-credential isolation is NOT enforced. Worktrees and
repos live under one
~/.kandevtree readable by the OS user running the backend, and agent CLI logins (gh auth,claude login, provider API keys) authenticate as that OS user — so all app-users share the same on-disk agent credentials. Application-layer isolation (DB + the checks above) is the boundary between users' kandev data; it does NOT sandbox the filesystem or per-user agent auth. For hard isolation of agent credentials, run separate kandev instances per user or use per-user OS accounts / sandboxed executors (future work). - No workspace sharing/membership — one owner per workspace.
- No first-party OIDC/SSO. The host supports plugin-provided external login
(an
auth-capable plugin asserts a validated OIDC/SAML identity and the host mints the session — see ADR 0050); the pre-auth login-page SSO buttons and anonymous provider discovery (auth.ssoProvidersin the boot payload) ship here too. What remains is a published IdP plugin (e.g. Google OIDC, developed in its own repo) and setup-mode first-admin bootstrap via SSO (SSO requires enforced auth today). - Office workspace-scoped HTTP routes (those carrying a
:wsId) are ownership-checked when auth is enabled; office run subscriptions are not yet ownership-checked (run events carry no workspace context at the subscription layer).