Skip to content

Provision Machine OAuth2 Clients

A deployment without browser sessions has no authenticated human operator who can call the /api/v1/server/oauth2/clients routes. Provision confidential machine clients locally instead of giving a machine token an operator role.

Apply migrations, load the same environment as the server, and run:

uv run python -m app.oauth2.clients.provision \
  --name "Example worker" \
  --scope organization:read \
  --machine-organization-access none

The command prints a JSON object containing client_id and client_secret. The raw secret is shown once and is never written to application logs. Store it in the calling application's secret store.

--scope and --organization-id are repeatable. Organization access accepts:

  • none, the safe default, with no organization identifiers;
  • single, with exactly one organization UUID;
  • selected, with one or more public organization identifiers;
  • unrestricted, with no organization identifiers.

The command checks that assigned organizations exist and serializes concurrent provisioning through ZA_RUNTIME_DIR. It may be run again to create additional clients. Local database and runtime-directory access are therefore administrative capabilities and must be restricted to trusted operators.

The resulting client can authenticate at /oauth2/token with grant_type=client_credentials. It cannot call human operator routes: OAuth2 scopes restrict a token but never manufacture an operator role.

The canonical config/client-credentials.example.toml profile enables only the Client Credentials grant. It disables Refresh Token because Client Credentials does not issue refresh tokens. It also removes built-in and JSON identity workflows and disables unused mail delivery. A deployment transitioning from interactive grants may keep Refresh Token together with the required workflow URL, derivation secret, and mail delivery only long enough to honor already-issued state; that transition policy is separate from a fresh Client Credentials setup.