OpenClaw EnterpriseDOCSGitHub

Bootstrap and human authentication flow

Overview

Native-IAM bootstrap commits administrators with the Installation and protects their credentials. Human sign-in reaches exact IAM authorization; the service API key flow covers key 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 external identity"]
    P -->|OIDC enrollment configured| T["Check signed admission; enroll account and grants"]
    T --> Q
    P -->|External 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 -->|Other allowed| L["Run and audit OCC operation"]
    K -->|Create account| U["Hold session; recheck selected IAM; commit account"]
    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 verify the administrator's immutable account/IAM identity without issuing keys, changing output, or repairing grants, including Installations predating service-administrator bootstrap. verifiedWithoutAuth checks the administrator's occ."user" row and IAM Principal before loading Better Auth. Success logs installation.already-bootstrapped with step: "fast-path"; a miss or error runs the full Better Auth check. Both paths check the base URL first.

Before creating credentials, apps/controller/src/composition/bootstrap-output.ts:bootstrapOutputPath rejects output basenames over 255 UTF-8 bytes. 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, and development writes to its bootstrap-only volume or explicit direct-initialization path. No plaintext reaches logs, audit, bootstrap HTTP responses or the worker.

The initializer commits Installation, IAM and audit once. API startup verifies committed state without calling POST /installation/bootstrap. Singleton constraints reject competing initializers.

Errors record installation.bootstrap-failed with available non-secret IDs and paths, then exit nonzero. Helm sets backoffLimit: 0. Tracked accounts, keys and partial files remain for inspection. If password linkAccount fails before createAccount returns, the helper attempts to delete the new user and rethrows. The initializer never automatically revokes, retries, repairs or resets uncertain or committed state.

Recovery requires confirming transaction completion and exact attempt IDs. Files alone cannot establish whether the seed committed. Identify the disposable Installation and storage before reset. Import successful output and retain its IDs. Lost output is never regenerated; service-key management covers replacement and revocation.

3. Construct session authentication

apps/controller/src/auth/index.ts:createPostgresControllerAuth uses OCC's public binding, which owns the caller-owned pool, full schema, construction failures, and adapter settings.

apps/controller/src/auth/index.ts:createControllerAuth configures Better Auth password authentication, protected cookies, and PostgreSQL storage. Sign-in returns { authenticated: true }; the token stays in its HttpOnly cookie. safeSessionResponse returns public identity and sessionKey, an auth-secret HMAC of the session ID (apps/controller/src/auth/session-binding.ts). A changed key makes the Console clear retained views and drafts. Password-only sign-out uses apps/controller/src/auth/password-sign-out.ts:passwordSignOut to delete the session through the selected database adapter before clearing its cookie. Storage failure returns 503 without cookie changes; the caller can retry after recovery. auth/admission.ts:passwordFailureAdmission counts failures; fast success clears the selected identity, not the address. onLimited reports authentication.sign-in-limited once per lane/window. auth/known-device.ts checks the email-bound MAC before password-state reads with rate and concurrency limits (see known devices). Verified bindings select the device lane; rejected bindings use the shared lane. Failed or refused reads retain signed keys as email/address constraints, never exemptions. Password-only admission captures issuance state before credentials, preserving reset-race invalidation; success may reissue it. In the password-only profile with PostgreSQL State, passwordSignInAudit attempts authentication.login. A failed success audit returns 503 without a cookie after attempted session deletion. A failed denial audit (DenialAuditUnavailable) counts as a credential failure. With an external provider, /oce/password marks it PASSWORD_DENIAL_AUDIT_UNAVAILABLE for the same accounting. The session lifecycle owns audit fields, status codes, and unconfirmed outcomes. Better Auth logs only errors, so a wrong password writes no unstructured console warning.

requireSessionKey checks optional x-occ-session-key after cookie resolution in ControllerAdmissionVerifier.verify, session, resolveSession, and signOut. Malformed, duplicated or foreign keys return 401 without revoking or clearing sessions. This optional header narrows but never selects a session; the native admin proxy strips it upstream.

For GET /api/auth/session, apps/controller/src/auth/index.ts:session sends the Better Auth result without its cookie headers. A late response for an invalid session leaves any cookie set by a newer sign-in in place.

When external sign-in is configured, apps/controller/src/auth/human-login.ts:createHumanLogin provides password, provider, and logout endpoints through a guarded Better Auth adapter. packages/occ/src/state/human-authentication.ts:PostgresHumanAuthentication owns account and method checks and the State transaction. Password verification captures credential and account versions for the session transaction to recheck. Both methods pass a private proof to guarded session creation; session and audit commit before Better Auth releases the cookie. Session reads check the current account, method, version, and Principal; sessions expire after eight hours without refresh. HTTPS uses the shared session cookie; session reads and logout reject duplicates.

For OIDC, the Console reads provider discovery and posts to /api/auth/providers/oidc/start. auth/human-login.ts:oidcProvider binds the five-minute attempt to the provider instance, client-secret digest, and effective enrollment-policy digest. createPostgresControllerAuth resolves omitted Group IDs from the Installation defaults before computing that digest. The attempt stores state and browser-secret digests, callback and PKCE verifier. A host-only HttpOnly cookie binds the browser. Callback consumption commits before code exchange, so an invalid or consumed attempt cannot exchange a code.

apps/controller/src/auth/oidc.ts:exchangeOidcSubject verifies the ID token and, when configured, its admission claim. A string is one group; an array must contain only strings. Matching is exact and case-sensitive without trimming or comma splitting. A matching claim reaches account lookup; a missing, malformed or unmatched claim is refused. The signed sub selects the identity. Profile claims are read only for an eligible unknown identity; see enrollment. No provider token enters State.

The signed login receipt binds provider, session and attempt. The Console consumes it once with the matching origin and session cookie; see the result contract.

Google reuses auth/human-login.ts:externalProviderEndpoints. apps/controller/src/auth/google.ts:exchangeGoogleSubject verifies its ID token, including hd and email_verified when allowed domains are set, and returns sub. It does not use the OIDC enrollment policy.

apps/controller/src/auth/client-address.ts:clientAddressConfiguration canonicalizes IPv4-mapped addresses before prefix validation, refusing entries covering every IPv4 or IPv6 address. resolveClientAddress uses dotted IPv4 for mapped peers and header hops.

Password admission validates email before known-device or State reads. Both password-only and guarded profiles audit refusals through their existing owners; session lifecycle defines error responses and failure accounting. Google and OIDC share process-local rate and concurrency limits for start, callback and result admission (keyedAdmission), keyed on client addresses behind trusted proxies and browser cookies otherwise. Provider HTTP shares a deadline and response-byte cap; State bounds pending attempts and expired cleanup. State persists attempt and session deadlines; cookie Max-Age subtracts monotonic elapsed work. Expired completion releases no cookie.

scripts/auth-maintain.ts parses arguments with scripts/lib/auth-maintain-arguments.ts:parseAuthMaintainArguments before configuration or database access. Undeclared commands, including inherited object properties, exit 64 with usage. The maintenance procedure lists supported operations and exit codes.

Activation requires stopped admission, drained or terminated requests, and stopped old controllers; startup does not fence live readers. Both PostgreSQL compositions reject Google with native administration; OIDC permits shared-cookie or host-only exchange. apps/controller/src/auth/index.ts:createPostgresControllerAuth checks the secret, canonical origin and profile before activation. Invalid configuration leaves sessions, enrollment and 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. See the deployment procedure.

Account routes enforce the session and recovery controls: State locks actor and target and checks expectedVersion. After method/recovery locks, human-authentication.ts:guardAuthority holds all six IAM tables through commit using 0073_account_administration_policy.sql, awaits the selected IAM Driver through the route's actor callback, then rechecks the session. READ COMMITTED sees grants changed during lock waits; NOWAIT contention returns retryable 503. Denials roll back before HTTP audit. Attachment, disablement and account-wide revocation advance the version and invalidate target sessions and proofs without changing IAM. Guarded reads return current account and method state, not operation receipts. Unknown completion returns a dependency failure without replay or compensation. Resolve uncertainty before proceeding. Without an external sign-in provider, composition omits account operations; routes still authorize the caller, then return 409 RESOURCE_CONFLICT. 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 applies the browser origin rules to unsafe session requests before admission, including sign-out before revocation and the external result exchange, which shares the external admission lane, before reading. Explicit service API keys skip this check and never fall back to a cookie.

apps/controller/src/index.ts:createFastifyApp validates the session and uses the selected IAM Driver to resolve its Installation-owned issuer/user ID and authorize the exact resource. Separate current-policy reads for lookup and authorization expose account and permission changes 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

After admission and throttling and password hashing, apps/controller/src/auth/index.ts:createPostgresAuthAccountProvisioner uses the original State READ COMMITTED transaction. It holds the original session, shared IAM configuration, and Native IAM account-administration policy through commit, in that order. The originally selected IAM Driver rechecks administer on the same Installation. State revalidates the requested Role and atomically writes the account, password method, Principal, grants, enrollment, and final success audit. Earlier session revocation, expiry by COMMIT, or a denied final IAM decision rolls back creation. With Native IAM, a policy withdrawal that commits first is visible to that decision. Sign-out waits behind creation's session hold. Lost COMMIT replies remain 503 with unknown outcome; no compensation or replay.

6. Enroll an OIDC identity

auth/oidc-enrollment.ts:oidcEnrollmentPolicy validates and canonicalizes the operator policy. resolveOidcEnrollmentPolicy resolves default Groups and computes the policy fingerprint once during composition, including the resolved createAccount.requireVerifiedEmail boolean. Composition applies configured IAM first, then verifies Role, Namespace and Group references. auth/oidc.ts:oidcEnrollmentProfile validates the standard email and the configured verification requirement. It rejects present non-boolean verification claims. With requireVerifiedEmail: false, absent or false verification is accepted, and the profile carries emailVerified: false. The callback uses that profile only when creating an account; existing identities keep their saved profile.

snapshotExternal distinguishes missing identities from refused existing identities. Only a missing identity can start account creation, and creation requires a valid profile. The callback hashes a random password and asks native IAM for the configured initial binding or no direct grant.

packages/occ/src/state/human-authentication.ts:PostgresHumanAuthentication.enrollExternal serializes the provider/subject, holds the shared IAM configuration lock, validates references and resolves the identity again. Existing identities retain their attempt-currentness and disabled-account checks. New identities reuse insertPasswordAccount; only the transaction that inserted them can snapshot those new methods without an older attempt timestamp. The prepared account carries the actual verification boolean into the email_verified database column. Account, Principal, external method, initial Role binding, Group memberships and audits commit together. Existing accounts receive only missing Group memberships. The session transaction rechecks the resulting proof before issuing its cookie.

Debugging and Verification

Search documentation