Service API Keys Flow
Overview
An Installation administrator, authenticated by human session or service API
key, issues a Better Auth key for an existing non-Agent IAM ServicePrincipal.
Automation uses that key to request an exact resource, and an authorized
administrator can later revoke it. This flow follows a
Namespace reader from issuance through GET /namespaces/:namespaceId to
revocation. It stops at the OCC resource response or the credential's deletion
and audit result; provisioning IAM identities and Agent workload credentials
remain separate lifecycles. Fresh native-IAM bootstrap provisions the initial
service administrator and calls the same key helper, delivering its response to
protected storage rather than an HTTP issuance response. That separate entry and
commit boundary is traced in the bootstrap flow.
Better Auth owns key material and persistence. The selected IAM Driver owns identity lookup and authorization. A key fixes the identity's Installation and optional Namespace at issuance, but it does not snapshot or grant permissions.
Entry Points
POST /api/auth/service-keys: a human session or Installation-scoped service key with current IAMadministeron the bootstrapped Installation names an existing principal in its exact scope. apps/controller/src/index.ts:requireInstallationAdmin.GET /namespaces/:namespaceIdwithx-api-key: a stored, unexpired key authenticates the service identity before IAM evaluates the resource request. apps/controller/src/auth/index.ts:ControllerAdmissionVerifier.verify.DELETE /api/auth/service-keys/:keyId: an administrator with the same credential and authority requirements supplies the non-secret ID returned at issuance. apps/controller/src/auth/index.ts:revokeServiceKey.
The controller already has configured Better Auth storage and its selected IAM Driver. Native IAM requires an explicitly provisioned ServicePrincipal, Role, and AccessBinding; issuance creates none of them. Request fields and lifetime limits are defined in the authentication reference and API reference.
Flow
graph TD
subgraph Issue["Administrator issues a key"]
A["POST service-keys with session or Installation key"] --> B["requireInstallationAdmin and IAM lookup"]
B -->|Unauthorized or invalid principal| C["Reject issuance"]
B -->|Exact non-Agent ServicePrincipal| D["createServiceKey persists hashed key and scope"]
D --> E["Append issuance audit"]
E -->|Success| F["Return plaintext key once"]
E -->|Failure| G["Attempt deletion and return 503"]
end
subgraph Request["Automation requests an exact Namespace"]
F --> H["GET Namespace with x-api-key"]
H --> I["ControllerAdmissionVerifier verifies stored key"]
I -->|Invalid, expired, or deleted| J["Return 401 without cookie fallback"]
I -->|Valid| K["resolveIdentity checks current IAM identity and fixed scope"]
K -->|Scope matches| L["getNamespace asks IAM to authorize exact read"]
K -->|Missing identity or scope mismatch| M["Audit denial and return 403"]
L -->|Denied by current policy| M
L -->|Allowed| N["Return Namespace resource"]
end
subgraph Revoke["Administrator revokes the key"]
N --> O["DELETE service-keys with session or Installation key"]
O --> P["revokeServiceKey deletes the Better Auth record"]
P --> Q["Append revocation audit and return revoked ID"]
P -->|Subsequent key use| J
endExecution Trace
1. Authorize the issuer and resolve the target principal
apps/controller/src/index.ts:requireInstallationAdmin,
called after admit and resolveIdentity in createFastifyApp.
The issuance route admits a human session or Installation-scoped service key,
resolves its current IAM Principal or non-Agent ServicePrincipal, then requires
administer on the server-owned Installation. Invalid credentials return
401 without falling back to an accompanying cookie. A valid caller without
the exact authority, or a Namespace-scoped key, receives 403. Account
creation and bootstrap retain their human-session boundary.
The handler asks the selected IAM Driver for the requested ServicePrincipal and
checks that its ID and optional Namespace match exactly. Unknown identities,
human Principals, Agent-owned ServicePrincipals, and mismatched scope return
400. Successful lookup grants no new permission and passes the existing
principal to Better Auth.
2. Persist the key, freeze its scope, and audit before disclosure
apps/controller/src/auth/index.ts:createServiceKey,
configured by createControllerAuth;
packages/occ/src/state/postgres-schema.ts:apikey.
The server calls Better Auth's supported createApiKey API. The plugin hashes
the key and stores the principal reference with the Installation and optional
Namespace metadata. These metadata values are the credential's fixed scope;
changing IAM scope later does not widen an existing credential. PostgreSQL
uses the official Drizzle adapter and the occ.apikey table. Key-based sessions
and public plugin management routes are not enabled.
The controller appends an issuance audit containing the resolved issuer and
non-secret key/principal IDs, then returns 201 with the plaintext key once.
There is no plaintext retrieval endpoint. If creation or audit persistence
fails, the response is 503 with no credential. If creation already succeeded,
the controller attempts deletion of the unreturned key; this cleanup is best
effort, not an atomic transaction with the audit sink.
3. Verify the credential and enforce its fixed identity scope
apps/controller/src/auth/index.ts:ControllerAdmissionVerifier.verify; apps/controller/src/index.ts:resolveIdentity.
For the resource request, an explicitly supplied x-api-key selects Better
Auth's verifyApiKey before cookie handling. Blank, forged, expired, or revoked
keys return 401 without falling back to a session. A mismatched Installation
or malformed stored key also fails authentication. Without the key header,
the existing session path remains unchanged; bearer authentication remains
unsupported.
Successful verification supplies the service-principal ID and stored scope to
resolveIdentity. The selected IAM Driver looks up the current identity by ID.
The controller rejects a missing identity, Agent ownership, changed scope, or
another requested Namespace with 403. A Namespace key cannot enter an
Installation-level resource operation. Better Auth neither creates a human
session nor authorizes this request.
4. Authorize the exact read with current IAM policy
apps/controller/src/index.ts:perform; packages/occ/src/index.ts:OpenClawController.getNamespace; packages/iam/src/index.ts:NativeIAMDriver.authorize.
perform passes the resolved service-principal ID and requested Namespace to
OCC. Before reading the resource, getNamespace requires read on that exact
Namespace through the selected IAM Driver. Native IAM loads current policy
separately for identity lookup and authorization. Removed bindings or Roles,
and matching Restrictions, affect subsequent decisions without key reissuance.
Installation-scoped keys still need their own explicit grants; the issuer's
permissions are never inherited.
A denied decision records authorization evidence and returns 403. Unavailable
required dependencies fail closed with 503. An allowed read returns the
Namespace response through the existing OCC API envelope. Other resource
operations retain their own OCC authorization and mutation-audit behavior;
key admission does not bypass them.
5. Delete the credential and record revocation
apps/controller/src/index.ts:requireInstallationAdmin,
used by the service-key DELETE handler;
apps/controller/src/auth/index.ts:revokeServiceKey,
called after getServiceKey.
The administrator repeats credential admission and current IAM
Installation-authority checks. Either a human session or an Installation-scoped
service key can authorize this request.
The handler looks up the non-secret key ID in this Installation; a missing or
already removed key returns 404. It deletes the Better Auth record through
the adapter, appends the revocation audit, and returns the ID with
revoked: true. The IAM principal and its policy remain unchanged.
Deletion prevents ordinary concurrent verification updates from recreating the
row. Subsequent verification fails across controller instances, although a
request already authorized may finish. If audit persistence fails after
deletion, the response is 503 and the key stays deleted. Rotation therefore
uses the same lifecycle: issue a replacement, switch the client, then revoke
the old key. An authorized service administrator can perform these calls for
itself or another eligible principal in the same Installation; OCC does not
schedule rotation.
Debugging and Verification
- HTTP integration:
node --test tests/integration/service-api-keys.test.mjsexercises real Fastify, Better Auth memory storage, and native IAM. It covers valid, invalid, expired, revoked, unauthorized, and cross-Namespace requests; IAM-authorized service-key management; session preservation; Agent exclusion; and audit attribution without keys. - PostgreSQL integration:
with
OCC_TEST_DATABASE_URLselecting a disposable application-role database, runnode --test tests/integration/postgres-service-api-keys.test.mjs. This separately checks stored hashing, foreign-Installation rejection, cross-instance revocation, and deletion during concurrent verification. Use the existing test environment instructions. - IAM conformance verifies identity lookup.
Run
pnpm openapi:checkto check that the OpenAPI contract, HTTP API reference, and API cheat sheet remain current with the controller routes. - Investigate
401as credential rejection and403as identity, scope, or policy denial. Checkopenclaw.auth.service-keys.createandopenclaw.auth.service-keys.revokeaudit actions using the request and non-secret key IDs. Never log the header or plaintext key. A503requires checking the auth, IAM, or audit dependency; revocation may already have deleted the credential.
These commands describe the proof hooks, not a new runtime execution record.
