Files
kandev/docs/specs/auth/spec.md
T
João Salavisa b20047b3eb feat(plugins): add auth capability for plugin-provided OIDC/SAML login (#1964)
* feat(plugins): add auth capability for plugin-provided OIDC/SAML login

Plugins can now authenticate a visitor against an external IdP (OIDC/SAML)
and contribute a login button to the pre-auth screen:

- new `auth` manifest capability + a capability-gated X-Kandev-Auth-Login
  webhook-response directive; the host mints the kandev_session (ADR 0050),
  the plugin never receives the raw token. Plugin-set Set-Cookie is dropped.
- auth.Service.AuthenticateExternal: existing-identity / link-by-email / JIT
  member provisioning; refuses to auto-link to an admin; verified-email is
  the plugin's contract.
- auth_providers manifest block + plugins.SSOProviders() populate the
  anonymous boot payload (auth.ssoProviders); the login page renders a
  "Continue with <provider>" button per provider.
- ADR 0050 + plugin-manifest / plugin-authoring / auth spec docs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: exclude publish_review_findings_kandev from public-docs coverage

The native code-review agent (#1957) added the publish_review_findings_kandev
MCP tool but did not update docs/public/coverage.json; that PR did not touch
docs/public so the path-filtered validator never ran. It's an internal review
reporting tool, not a user-facing feature, so record it as an exclusion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(plugins,auth): address review on SSO directive and JIT provisioning

- takeAuthLoginDirective strips every case variant of X-Kandev-Auth-Login and
  rejects (403) when a plugin sends more than one, so no variant leaks to the
  browser and login never uses a nondeterministically-picked assertion.
- provisionExternalUser rolls back the just-created user when the identity
  insert fails (transient error or a lost UNIQUE(provider,subject) race) and
  resolves to the winning identity, so no account is left without a login and
  no email is reserved by an unusable row. Adds userstore.DeleteUser.
- auth spec: mark the login-page SSO buttons + provider discovery as landed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: drop redundant coverage exclusion; main now covers publish_review_findings_kandev

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-26 18:20:39 +01:00

5.9 KiB

status, created, owner
status created owner
building 2026-07-24 tbd

Opt-in Authentication & Multi-User Segregation

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.auth runtime flag ("Authentication & users" in Settings > System > Feature Toggles, or KANDEV_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) and member. 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 (kandev_session, HttpOnly, SameSite=Lax, sliding 30-day expiry, DB-backed and revocable from Settings > Account). Programmatic clients (external MCP, scripts, CI) use personal access tokens (kandev_pat_…, shown once, revocable).
  • 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.
  • 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 (automation X-Webhook-Secret, office channel HMAC, plugin webhooks).
  • 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.

Known v1 limits

  • Filesystem / agent-credential isolation is NOT enforced. Worktrees and repos live under one ~/.kandev tree 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.ssoProviders in 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).