OpenClaw EnterpriseDOCSGitHub

Authentication

OpenClaw Control Plane (OCC) authenticates human API clients with sessions from email/password sign-in or an enrolled Google or OIDC identity. OIDC supports opt-in team enrollment. Non-Agent automation uses service API keys. Better Auth owns password verification, revocable session cookies, and hashed API-key storage. The selected IAM Driver resolves the authenticated account or service identity to an explicitly provisioned Principal or ServicePrincipal and owns authorization.

See human administrator sign-in or the service-key procedure. The platform console at /console/ uses these session endpoints. Unrestricted public signup, OIDC provisioning or claim mapping, and bearer credentials are unsupported; generic OIDC sign-in for enrolled accounts is in OIDC sign-in.

Installation and account ownership

Authentication belongs to one bootstrapped Installation. The controller requires OCC_AUTH_SECRET and OCC_AUTH_BASE_URL (see settings). An account's immutable Better Auth user ID and Installation-specific trusted issuer identify its IAM Principal. Neither email nor display name grants access.

Fresh native-IAM bootstrap creates the first human administrator and one Installation-scoped, non-Agent ServicePrincipal. Both have separate bindings to the same administrator Role. The service identity has no email, password, session, Namespace, or Agent owner; its authority survives human account deletion.

Fresh bootstrap also creates the initial default Namespace under the bootstrap Principal's ordinary Namespace creation permission. Installation/IAM state, the Namespace, its queued reconciliation, and bootstrap audit commit together. Worker provisioning is asynchronous.

Bootstrap issues a 30-day service API key named bootstrap-admin and writes its one-time response to OCC_BOOTSTRAP_SERVICE_KEY_FILE. The JSON contains data.id, data.servicePrincipalId, data.name, data.expiresAt, data.key, and meta.installationId. Better Auth keeps only the hash; there is no server-side plaintext retrieval.

Production also creates the configured OCC_BOOTSTRAP_ADMIN_EMAIL account with a random password written to OCC_BOOTSTRAP_PASSWORD_FILE. Both paths must be absolute, distinct siblings on protected operator-owned storage. Output is exclusive, owner-only (0600), and synced before committing Installation/IAM state; existing files, symlinks, or unsafe parent directories fail closed. Credentials never appear in logs, audit, or the bootstrap response. OCC creates no Kubernetes Secret or PVC for delivery.

In Helm, bootstrap.password.claimName selects the existing protected PVC. Only the initialization Job mounts it; bootstrap.password.fileName and bootstrap.serviceKey.fileName are written under bootstrap.password.mountPath. See initial-key retrieval and bootstrap recovery.

The shared scripts/bootstrap-installation.mjs initializer runs after migration and before either API or worker startup in Compose and Helm. Development provisions the configured OPENCLAW_DEV_EMAIL and OPENCLAW_DEV_PASSWORD on a fresh database, using the defaults in settings, and bootstraps the Installation before serving requests. It does not generate a password output file or rotate an existing account's password. Compose stores the service-key JSON on the bootstrap-only occ_bootstrap_data volume. The API and worker do not mount it. Direct development runs the same initializer with an explicit private key-file path before starting the API or worker. The quickstart uses the service key for its API check.

An already-bootstrapped Installation receives no new Namespace, identity, grants, key, or output, including installations created before initial-key delivery existed. Restarting does not replace missing files, expired/revoked keys, removed service identities, or removed grants. Use normal issuance/revocation for credential recovery and rotation.

Bootstrap makes one attempt. Any error emits installation.bootstrap-failed with available non-secret IDs and paths, then exits unsuccessfully. Created accounts, keys, and files remain, including partial output from a failed write. Bootstrap does not automatically revoke, delete, retry, repair, or reset them. The Helm initialization Job uses backoffLimit: 0. Better Auth persistence and the Installation/IAM commit are separate, so an error leaves the commit outcome unknown; resolve it before manual repair or reset an identified disposable Installation. See incomplete bootstrap recovery. File existence alone is not proof of successful initialization.

Browser request origin

Cookie-authenticated controller API mutations must include an Origin matching the origin of OCC_AUTH_BASE_URL. This includes sign-out. A missing, malformed, or different origin is rejected with 403, and the error message says that a trusted browser origin is required. If Sec-Fetch-Site is present, it must be same-origin. Safe reads do not require an Origin.

Sign-in rejects an explicitly untrusted or malformed Origin and also rejects Sec-Fetch-Site: cross-site when Origin is missing. Command-line sign-in may omit both headers. For later cookie-authenticated mutations, command-line clients must provide the configured Origin. An explicitly supplied service API key does not require Origin, and an invalid key never falls back to a session cookie.

Session lifecycle

Operation Supported behavior
POST /api/auth/sign-in/email Verifies an existing account's email and password and issues a session cookie. The JSON response confirms authentication, and with external sign-in enabled returns sessionKey, never a session token.
GET /api/auth/session Returns safe account identity and a noncredential sessionKey, or data: null without a valid session.
POST /api/auth/sign-out Revokes the current session. Protected API requests using that session subsequently return 401.

Human-session Namespace IAM writes retain assigning authority through their transaction; see Sharing commit guarantees.

The sessionKey identifies the session, stays stable across reads, and changes at sign-in. It cannot authenticate; the token stays in its HttpOnly cookie. Console pins each tab's requests with x-occ-session-key. Foreign, malformed, or duplicated keys return 401; rejected sign-out preserves sessions and cookies. Omitting the header leaves requests unchanged.

Session inspection never sets cookies; late old-session responses cannot clear newer sign-in cookies.

Password-only sign-out confirms session deletion before clearing its cookie. A storage failure returns 503 DEPENDENCY_UNAVAILABLE without clearing the cookie; retry sign-out after storage recovers.

Password-only sessions expire seven days after sign-in; use does not extend them. With Google or OIDC sign-in enabled, every session expires after eight hours (session controls). Password-only has no online way to end another account's session: removing its IAM bindings and Group memberships refuses permissioned requests from the next one (native admin WebSockets within about 25 s), and purge-sessions --user <userId> --writers-stopped (stopped maintenance) ends it.

Sign-in takes {"email": "...", "password": "..."}. The session arrives only through Set-Cookie.

Sign-in rejects NUL or unpaired-surrogate emails with 400 INVALID_REQUEST before account or known-device State reads. Both profiles retain denial audits and budget accounting, including recovery-only and counted 503 audit failures. Limited attempts remain 429; passwords are unchanged. Stored U+FFFD email is unsupported: password-only returns 400 FORBIDDEN without a session. Guarded credential checks can create a session and cookie, but HTTP session inspection returns 500 INTERNAL_ERROR, not usable-session proof.

In both profiles, after 10 failed sign-ins per minute per email, or 20 per client address with api.trustedProxy, attempts wait 1–8 s and return 429 with Retry-After, repeated in the message, whether or not the email exists; an Installation administrator's correct password still signs in, and so does the recovery account's with an external provider. Successful sign-ins spend nothing. Without one, browsers share the ingress address and startup logs authentication.sign-in-limit-warning. A successful sign-in within the budget clears that email's failures, not the address's; an administrator's success in the slowed lane does not, so pacing lasts until the minute rolls over. The first slowed attempt per lane each minute logs authentication.sign-in-limited at WARN with its lane (email, device, address, or untracked when the budget table is full). email, device, and address events also carry keyHash, a truncated HMAC under the auth secret; untracked events have no key and no hash. The email and address are never logged. The bundled Collector exports the event and lane, not the hash.

Password checks attempt an authentication.login audit: success names the account's Principal and userId; wrong passwords and unknown emails produce denied/INVALID_CREDENTIALS without account identity. Audit writes fail closed. Failed success audits return 503 without a cookie; uncertain session creation or cleanup may leave a server-side session. Failed denial audits still count against tracked budgets, returning 503 or, in the slow lane, 429. Untracked or exhausted lanes are paced without necessarily adding failure entries. Unconfirmed audit writes have unknown persistence outcomes. Admission 429s before password checking are unaudited and reported by authentication.sign-in-limited. Slow-lane administrator checks remain audited; wrong passwords return 429.

Known devices

Every successful sign-in, password or external, sets __Host-occ_known_device (occ_known_device over plain HTTP): HttpOnly, SameSite=Strict, Path=/, no Domain, 90 days. It holds up to three entries, one per recent account, each an HMAC under the auth secret over a hash of the account's email, the issue time and a random nonce, so every sign-in gets its own entry; it never carries the email. Each entry is also bound to the account's password: its user, password method and that method's authentication version, which the database bumps on every password change. An external sign-in marks the browser only when the account has a password. When fresh account proof is available, a later password attempt with a valid entry spends that browser's own budget, the size of the email's, and does not wait for the email's slowed slots. Spending the shared email budget alone cannot refuse that verified browser. If finite proof capacity is exhausted or the reader is unavailable, the attempt instead uses shared email/address admission with signed-device constraints. A nonreserved account can then receive 429 from a spent shared lane despite its valid cookie. Administrator and recovery accounts retain their existing paced password checks.

The cookie never authenticates: a wrong password with it is 401 and spends the browser's lane, and the address lane and global caps still apply. Tampered, expired, foreign-account, stale, or duplicated cookies are ignored, returning the attempt to the shared lane with the same answer a new browser gets. Resetting an account's password, or deleting and recreating the account, revokes every entry issued before, including a reset that commits while a sign-in with the old password is in flight: that sign-in's entry is bound to the state read before its password check. With external sign-in, a disabled account's entries verify nothing until it is enabled again; reset the password as well to revoke them for good. The controller reads the account only for an entry issued for the attempted email, so forged or foreign cookies add no timing signal about which emails exist, and a completed read that rejects the binding gives no exemption. Each controller limits proof reads to 30 per signed entry per minute, two active per entry, 600 per minute overall, and 16 active overall. There is no proof cache or waiting queue. If a read fails or is refused, its signed entry keys also constrain the ordinary email/address lane: losing proof cannot reopen a spent device allowance. These keys never grant an exemption. Reserved accounts retain the existing paced password check. Rotating the auth secret invalidates every entry; the next successful sign-in issues a new one. A new browser gets no exemption.

The controller configures the Better Auth cookie with the openclaw_occ prefix; the OpenAPI contract names it openclaw_occ.session_token. Cookies are HTTP-only, use SameSite=Lax, and cover /. Production enables secure cookies; the configured base URL is also the trusted origin. Session inspection exposes only authenticated, sessionKey, and the account's id, email, and name.

Protected requests resolve the current stored session with cookie caching disabled. A missing, expired, revoked, or forged session is rejected, as is an Authorization header even alongside a session cookie.

External sign-in for existing accounts

An Installation can let enrolled existing accounts sign in with Google or one generic OIDC issuer. External sign-in and account controls defines the single-controller profile, provider flow, session binding, the recovery user, the administrator account API, and sign-in limits.

Session and recovery controls

See session and recovery controls.

Native admin shared sessions

By default, browser sessions use one shared cookie scope, including when native Agent access is disabled. Configure an HTTPS Console origin and an Agent DNS suffix. OCC derives their longest common DNS-label suffix and rejects IP literals, missing or unrelated hosts, public suffixes, and malformed DNS names. For console.oce.example.com and agents.oce.example.com, the scope is oce.example.com. There is no cookie-domain override. Guarded OIDC can instead select the host-only exchange: the Console cookie stays on its host, and the Agent receives a revocable handle bound to the parent session. The exchange creates no account or grant.

Every host that receives this domain cookie must be a trusted OCE ingress endpoint. Public preview hosts must remain outside it. The cookie uses __Secure-openclaw_occ_shared.session_token, Secure, HttpOnly, Path=/, and the existing SameSite and CSRF protections. Password, OIDC and Google sign-in share this format. Duplicate session-cookie values are rejected.

Sign-out revokes the current session and expires its cookie. OCE does not clear obsolete cookie names or scopes. Remove stale OCE cookies in the browser if they cause duplicate-cookie refusals. External-provider attempt cookies remain host-only.

Native-host requests authenticate the shared OCE session or host-only handle, resolve the exact Agent represented by the requested host, authorize exact Agent operate or administer, and validate the selected version and supported native configuration before proxying. OCC strips browser cookies, Authorization, API and session keys, forwarded identity, and native scope headers before forwarding upstream, so the native gateway never receives the OCE session cookie. Native chat or other Agent-host activity does not renew the console session.

Account provisioning

POST /api/auth/accounts requires a human session and the selected IAM Driver's administer grant on the server-owned singleton Installation. Emails are lowercase and unique. Passwords require 12–128 characters. OCE skips email verification. Attach external identities explicitly or use OIDC enrollment for unknown identities.

Before hashing, each controller allows 10 attempts per authorized administrator Principal across sessions and target emails, refilling one every six seconds. Exhaustion returns 429 RATE_LIMITED with Retry-After.

Optional roleId binds an existing Installation Role. Omit it for no direct binding. Unknown, Namespace-scoped, and configured role_config_ Roles return 400. The sole non-configured Role, role_admin_<uuid>, grants only installation:administer and installation:read here. APIs cannot create more Installation Roles. The Namespace policy API omits it.

Creation holds the original session through commit and rechecks the selected IAM Driver before inserting the account. With Native IAM, the assigning authority must also remain current through commit; otherwise creation rolls back. Creation joins configured default Groups and issues no session. The success audit records principalId, roleId or grant: "none", and any groupIds. A lost COMMIT reply returns 503 DEPENDENCY_UNAVAILABLE. Confirm the outcome before retrying or changing grants.

See the request example and transaction flow.

Authorization and failures

For each protected request, OCC resolves the session user or service-key principal through the selected IAM Driver and authorizes the exact resource operation. Each identity lookup and authorization decision loads current IAM policy, so account and permission changes are visible across controller instances. Caller-supplied identity headers and bearer credentials are not authorization evidence.

Condition Result
Missing or invalid session or service key on a protected route 401 UNAUTHENTICATED.
Valid credential without the required IAM grant 403 FORBIDDEN.
Duplicate account during provisioning 409 RESOURCE_CONFLICT.
Authentication or IAM dependency unavailable The request fails closed; dependency failures return 503 DEPENDENCY_UNAVAILABLE.

Anonymous session inspection returns 200 with data: null.

Automation credentials

Non-Agent automation uses x-api-key with an existing IAM ServicePrincipal. Service API keys defines issuance, credential precedence, exact scope, revocation, audit behavior, and failures. Keys do not inherit their issuer’s permissions or create human sessions.

The authentication implementation owns sessions; the HTTP routes own endpoint exposure and provisioning authorization.

Search documentation