Skip to main content

Authentication

The two ways into the platform: Salesforce SSO for organization users, and CLI-provisioned passkeys for platform operators. Covers sessions, JWTs, and route protection.

TL;DR: There are exactly two ways into the platform, and no self-service registration. Organization users sign in with Salesforce SSO, which provisions their account on first login. Platform operators are created from the CLI and enrol a passkey through a setup link. No passwords are created or stored anywhere.

Why There Is No Registration Page

Self-service registration was removed. It created a third way for an account to exist, and every account it made had to be reconciled against an organization, a seat, and a Salesforce identity afterwards — reconciliation that had no automatic path and no owner.

The two remaining doors each have a clear authority behind them. Salesforce decides who is an employee; a platform operator decides who operates the platform. Neither requires a signup form.

Path 1 — Organization Users: Salesforce SSO

This is how everyone who is not a platform operator signs in.

The user clicks Sign in with Salesforce on /admin/login. The platform runs an OAuth 2.0 authorization-code flow with PKCE against your Salesforce org, so your org's own login policies — including MFA — apply unchanged.

On the callback, the platform:

  1. Exchanges the code and reads verified userinfo claims.
  2. Gates them: the email must be present, email_verified, and on an allow-listed domain. A failure never creates an account.
  3. Resolves the identity — returning login, merge onto an existing account by verified email, or just-in-time provisioning against the organization's seat allocation.
  4. Records the Salesforce Username, which later authorizes per-user Salesforce tool calls.
  5. Mints a session JWT and sets it as the access_token cookie.

First login creates the account with no admin step, provided the domain is allow-listed and a seat is free. Failures return to /admin/login?sso=<reason>; the reasons are tabulated in Salesforce App Setup.

Path 2 — Platform Operators: CLI + Passkey

Operators are created out-of-band. There is no way to self-provision one.

# 1. Create the account
systemprompt admin users create --name "Jane" --email jane@astounddigital.com

# 2. Grant the admin role
systemprompt admin users role promote jane@astounddigital.com admin

# 3. Mint a one-shot passkey setup link
systemprompt admin users webauthn generate-setup-token --email jane@astounddigital.com

The third command prints a copy-paste URL of the form {api_external_url}/auth/link-passkey?token=…, valid for 15 minutes by default (--expires-minutes to change). Send it to the operator through a channel you already trust; they open it, create a passkey, and sign in with that passkey from then on.

The role is baked into the JWT at issue time, so an operator promoted while signed in must sign out and back in before the change takes effect.

Passkey Sign-In

Passkey authentication uses public-key cryptography. The browser generates a key pair bound to this domain; the private key stays on the device or in the user's password manager. The server stores only the public key, verifies a signed challenge, and issues an OAuth 2.0 session token via PKCE.

Lost Passkey

There is no self-service recovery — magic links were removed, and no email service is configured to deliver them. An operator who loses passkey access needs another operator to mint a fresh setup link with the same generate-setup-token command. Keep more than one operator account so this is never a single point of failure.

Session Management

Property Value
Cookie name access_token
Token format JWT
Default expiry 3600 seconds (1 hour)
Cookie flags path=/, HttpOnly, SameSite=Lax, Secure on HTTPS
Required scopes user or admin

Every admin request passes through two middleware layers. User context middleware extracts and validates the JWT, then loads the user's roles and department into a UserContext. Auth check middleware rejects protected routes without a valid user ID, returning HTTP 401.

UserContext carries user_id, username, email, roles, department, and is_admin.

To sign out, clear the access_token cookie.

Public vs. Protected Routes

Route Access
/admin/login Public
/admin/auth/salesforce/* Public — the SSO start and callback
/auth/link-passkey Public — consumes a one-shot setup token
/admin/* (everything else) Requires a valid session
/bridge-auth/* Requires a valid session

Pages requiring admin privileges perform an additional is_admin check and return HTTP 403 if the caller lacks the role.

System-originated actions

Every action recorded by the platform — including scheduled jobs, hooks, and MCP-server invocations — traces to a real users row. There is no separate "system user" or synthesized principal. The platform refuses to attribute work to an invented identity.

How ownership is declared

Each scheduled job in services/scheduler/config.yaml carries an explicit owner: field naming an existing admin user:

- name: publish_pipeline
  extension: web
  owner: admin
  schedule: "0 */15 * * * *"
  enabled: true

At startup the scheduler resolves owner: to a users.id. If the named user does not exist or is inactive, startup fails loudly — the platform refuses to run with unowned jobs. To change ownership, edit the YAML and restart.

How attribution flows

The resolved owner becomes JobContext.actor for every execute() call. Job implementations consume it through ctx.actor() and pass it to any audit-row write. Governance audit rows carry three fields that together give full forensic clarity:

Column Meaning
user_id The accountable principal — a real users.id.
actor_kind The surface that ran the action: user, job, mcp.
actor_id A label for that surface (job name, MCP server name, etc.).

A direct human action shows as (user_id = alice, actor_kind = 'user', actor_id = 'alice'). A scheduled job owned by Alice shows as (user_id = alice, actor_kind = 'job', actor_id = 'publish_pipeline'). Same accountability column, different surface, queryable separately:

SELECT actor_kind, user_id, COUNT(*)
FROM governance_decisions
GROUP BY actor_kind, user_id;

Why no separate "system" user

A dedicated "system" identity would be either a synthesized principal (impersonation) or a backdoor account with no real human accountability. Neither passes the "every action traces to a real user" bar. The designated owner is a normal admin who legitimately authorized the platform's existence by installing it — same accountability model as a unix crontab. Compromising the designated owner is exactly as bad as compromising that admin's credentials directly; there is no additional power and no amplification path.