OpenClaw EnterpriseDOCSGitHub

Bootstrap and human authentication flow

Overview

Fresh native-IAM bootstrap creates human and service administrators, shares their Role through separate bindings, and commits them with the Installation. It writes the initial service key to protected storage. Production also delivers a generated human password; development uses its configured password. This flow covers initialization, human sign-in, and exact IAM authorization. The service API key flow covers verification, rotation, and revocation.

Entry Points

Flow

graph TD
  subgraph Bootstrap["Shared installation initializer: one attempt"]
    A["Load Installation"] -->|Existing| B["Verify persisted identity; retain credentials"]
    A -->|Fresh| C["Create human and native IAM seed with service administrator"]
    C --> D["Better Auth persists service-key hash"]
    D --> E["Sync private key JSON and production password file"]
    E --> F["Commit Installation, IAM seed, and audit"]
  end
  F --> G["Complete startup"]
  B --> G
  Bootstrap -->|Any error| H["Exit unsuccessfully; preserve tracked artifacts for manual repair"]
  subgraph Request["Human controller request"]
    G --> P["Verify password or enrolled GitHub identity"]
    P -->|GitHub profile enabled| Q["State rechecks account and method; commits session and audit"]
    Q --> J["Release session cookie"]
    P -->|Password-only profile| J
    Q -->|Disabled, stale proof, or commit failure| R["Reject login; no cookie"]
    J --> N{"Unsafe session request?"}
    N -->|Yes| O["Check console origin and Fetch Metadata"]
    N -->|No| K["Resolve current IAM identity and exact authority"]
    O -->|Trusted| K
    O -->|Rejected| M
    K -->|Allowed| L["Run and audit OCC operation"]
    K -->|Invalid session or denied authority| M["Return 401 or 403"]
  end

Execution Trace

1. Load state and create the fresh administrator identities

scripts/bootstrap-installation.mjs loads the singleton Installation. Existing Installations only verify the configured administrator's immutable account/IAM identity: no key issuance, output changes, or identity/grant repair, including Installations predating service-administrator bootstrap.

For fresh setup, production creates a Better Auth account with a random password; development creates the configured OPENCLAW_DEV_EMAIL/OPENCLAW_DEV_PASSWORD account. packages/iam/src/index.ts:createBootstrapAdministratorSeed adds a non-Agent spn_<uuid> without a Namespace and a separate unrestricted binding to the human's administrator Role. The authorization reference defines exact actions. Additional-account provisioning creates no service administrator.

2. Issue private output, then commit the Installation

apps/controller/src/auth/index.ts:createServiceKey persists a Better Auth key named bootstrap-admin with the default 30-day expiry, scoped to this Installation and service principal. This auth write is independent of the OCC transaction. An uncommitted IAM seed cannot authorize normal OCC operations; startup does not expose the application until bootstrap succeeds.

bootstrap-output.ts:writeProtectedBootstrapFile creates owner-only output exclusively and syncs it before OCC commit. The JSON contains the key response and attempt Installation ID; production also writes its password file on the same protected PVC. Development writes to its bootstrap-only volume or explicit direct-initialization path. No plaintext reaches logs, audit, HTTP bootstrap responses, or the worker.

Both modes commit Installation/IAM/audit through the same controller transaction. The initializer owns one attempt scope for account creation, key issuance, output, and commit. The API subsequently loads committed state without signing into itself or calling POST /installation/bootstrap; that public endpoint remains human-session-only and does not issue bootstrap credentials. Singleton database constraints select at most one committed seed. A losing initializer fails and preserves completed tracked artifacts for operator inspection.

Any error ends the single initialization attempt with installation.bootstrap-failed, available non-secret IDs and paths, and a nonzero exit. Completed tracked accounts, keys, and files remain available for manual inspection; even a partially written output file is preserved. One pre-return Better Auth failure is narrower: if password linkAccount fails inside createAccount, the helper attempts to delete the just-created user before rethrowing. The initializer does not treat that cleanup attempt as a general artifact-recovery path, and it does not automatically revoke, retry, repair, or reset committed or uncertain state. The Helm initialization Job uses backoffLimit: 0.

The operator confirms the original transaction has finished and compares exact attempt IDs before manual repair; file existence or another Installation is insufficient. An uncertain commit can already have persisted the seed, so an error never authorizes an automatic wipe. A deliberate reset must identify the disposable Installation and its dedicated storage. The recovery procedure owns those operator actions.

After confirmed success, the operator retrieves/imports the existing file and retains its non-secret IDs. Lost output does not trigger regeneration; normal service-key management owns replacement and revocation.

3. Construct session authentication

apps/controller/src/auth/index.ts:createPostgresControllerAuth uses OCC's public binding: the caller-owned pool and full schema, no construction I/O or teardown, rejection without fallback, and unchanged PostgreSQL/camelCase/transaction settings.

apps/controller/src/auth/index.ts:createControllerAuth configures Better Auth email/password authentication, protected session cookies, and durable PostgreSQL storage. Sign-in returns { authenticated: true }; the session token stays in its HttpOnly cookie and is omitted from session-inspection responses. safeSessionResponse projects sessionKey, an HMAC of the session record ID under the auth secret (apps/controller/src/auth/session-binding.ts), alongside public user identity. Console compares it to invalidate retained views and drafts after a new session, including for the same user. Sign-out revokes the session, and public signup is disabled. Without an external provider, auth/admission.ts:passwordFailureAdmission limits failed password sign-ins.

requireSessionKey applies the optional x-occ-session-key header after the cookie session resolves, in ControllerAdmissionVerifier.verify (protected API and native admin proxy), session, resolveSession, and signOut. An absent header changes nothing; a malformed, duplicated, or foreign key returns 401, so the header narrows but never selects a session. Sign-out with a foreign key revokes and clears nothing. The native admin proxy strips the header upstream.

When GitHub is configured, apps/controller/src/auth/github.ts:createHumanLogin wraps the Better Auth adapter and provides curated password, GitHub, and logout endpoints. packages/occ/src/state/human-authentication.ts:PostgresHumanAuthentication owns persisted account/method checks and the original State transaction. Password verification captures the credential and account version before the session transaction rechecks them. Both methods pass a controller-private proof to the same guarded session creation path; the session and required audit commit before Better Auth releases its cookie. Session reads check the current account, method, version, and Principal, with an eight-hour absolute lifetime and no refresh. HTTPS uses a __Host- session cookie so a sibling host cannot plant the active cookie through a parent-domain Domain attribute. Session readers and logout reject ambiguous duplicate active-session cookies.

For GitHub, the Console reads GET /api/auth/providers and sends a same-origin POST /api/auth/providers/github/start. The server stores a five-minute attempt with state and browser secret digests, provider instance, callback, and PKCE verifier. A host-only HttpOnly cookie binds the browser; this profile rejects shared-domain sessions. Authorization requests omit OAuth scopes. Callback consumption commits before exchange; a losing, expired, or invalid attempt does not exchange a code. apps/controller/src/auth/github.ts:exchangeGithubSubject exchanges the code with the GitHub App client ID and secret, uses the returned user access token only for /user, and returns the numeric subject. Access and refresh tokens, expiry, and scope data are discarded; the App private key remains with the repository credential consumer. The subject selects an exact existing enrollment; email, login name, and tokens do not become identity or policy. Success redirects to exactly /console/; failure redirects to the fixed Console URL with a sanitized error marker.

Start also returns attemptId, an HMAC of the attempt's state digest. Success sets a signed two-minute SameSite=Strict receipt naming the new session and that attemptId. The Console's same-origin POST /api/auth/providers/github/result reaches oceGithubResult, which checks the receipt signature and expiry, the posted attemptId, and that the session cookie still resolves to the named session. It then records the receipt in a process-local ledger until expiry, clears the cookie, and returns the session key, without issuing or extending a session. Password sign-in in this profile returns the same key. Callback denials are audited as INVALID_ATTEMPT (malformed, unbound, replayed, or expired), PROVIDER_UNAVAILABLE (transport failure, deadline, 429/5xx, malformed body), or EXTERNAL_IDENTITY_REJECTED; State dependency failure or uncertain session completion is not a denial. Neither path retries.

Google reuses apps/controller/src/auth/github.ts:externalProviderEndpoints for start, callback, and result, with provider instance google:<sha256(client ID)>. Authorization adds scope openid email and an auth-secret HMAC of attempt state as nonce, without extra storage. apps/controller/src/auth/google.ts:exchangeGoogleSubject exchanges the code, fetches Google's signing keys through the same bounded transport, verifies the RS256 ID token's signature, issuer, audience, expiry, and nonce (plus hd and email_verified when allowed domains are set), and returns only sub. Tokens and email are discarded.

Password and external-provider work have separate bounded process-local admission; GitHub and Google share one budget. Provider HTTP shares a deadline and limits streamed response bytes; State bounds pending attempts and expired cleanup. State sets the five-minute attempt and eight-hour session deadlines. Cookie Max-Age subtracts monotonic elapsed work from that persisted lifetime; expired completion cannot release a cookie.

Activation requires stopped admission, drained or terminated requests, and every old controller stopped. Both PostgreSQL compositions reject GitHub with enabled native administration, even when its cookie domain is missing. apps/controller/src/auth/index.ts:createPostgresControllerAuth constructs and initializes authentication before activation, checking the secret, canonical HTTP origin, and supported profile. Invalid static configuration leaves legacy sessions, account enrollment, and the recovery designation unchanged. PostgresHumanAuthentication.activateRecovery then validates and enrolls the complete existing password-user/Principal population, fixes the usable recovery administrator, and removes unbound historical sessions in one State transaction before serving resumes. Unsupported or incomplete populations fail activation. This is a stopped-maintenance contract; startup does not fence an old live reader. See the deployment procedure.

The controller's account routes authorize native IAM Installation administer and require a current human session and the configured Origin. State locks both actor and target, rechecks the actor session, and applies the caller's expectedVersion. Attachment, disablement, and account-wide revocation advance that version and invalidate target sessions and proofs without changing IAM. A guarded read returns current account and method state, not a prior operation receipt. Unknown completion returns an explicit dependency failure without replay or compensation; operators must resolve uncertainty before a new action. Logout commits deletion and audit before clearing the cookie. The authentication reference owns configuration, recovery limits, and operator-visible behavior.

4. Admit and authorize protected API calls

ControllerAdmissionVerifier.verifyControllerRequest requires the configured console Origin for unsafe session requests before admission. A supplied Sec-Fetch-Site must be same-origin. Sign-out applies the same check before revoking the session, and the GitHub result exchange before reading it; that exchange shares the GitHub admission lane. Explicit service API keys do not use the cookie origin check, and an invalid key cannot fall back to a cookie.

apps/controller/src/index.ts:createFastifyApp validates the session, resolves its installation-owned issuer and user ID through the selected IAM Driver, and authorizes the exact resource through that same Driver. The Driver loads current policy separately for identity lookup and authorization, so account and permission changes are visible across controller instances. Missing or invalid sessions return 401; denied permissions return 403; dependency failures fail closed. Bearer credentials and caller-supplied identity headers are rejected.

5. Provision additional accounts

apps/controller/src/index.ts:createFastifyApp permits an authorized human Installation administrator to create another account, with or without GitHub sign-in. prepareAccount validates and hashes the password without writing; the PostgreSQL composition then calls provisionPasswordAccount, which writes the user, password method, Principal, explicit existing-role binding, enrollment, and audit in one State transaction, so a failure leaves no partial account. Nothing is compensated after the transaction. A lost COMMIT reply returns 503 stating that the outcome is unknown; the account is either complete or absent, so a deliberate retry with the same email creates it only if the first attempt did not commit, and otherwise returns 409. Account creation issues no session and infers no grants.

Debugging and Verification

Search documentation