Skip to main content
Version: 2026.07

Identity Service: Registering Clients

Once the Identity Service is installed and configured, the platform services that authenticate against it must be registered with it. This page covers that registration.

info

Agents are not registered here. Agents are provisioned through the platform — the Generate Key action in the admin Agents page, or the CLI key commands and SDK — which creates both the Identity Service key and the matching agent identity in the registry service. Platform-internal service agents (such as the Secure Connection Service) are registered automatically by the Istari Platform Helm chart's identity.agentRegistration hook.

Concepts

Registration happens at two independent layers:

  1. The Zitadel layer — the Identity Service itself is an OIDC client of Zitadel. This was done during installation and is not repeated here.
  2. The Identity Service's own database — platform services (the registry) are clients of the Identity Service. They hold an ECDSA P-384 key pair and authenticate by signing a JWT assertion with their private key (RFC 7523 private_key_jwt). Browser-based public clients (the frontend, the MCP service) are also registered here, but carry no key — their registration is a client ID plus an allowlist of redirect URIs. This page registers those principals.

Credentials take the form of a JSON blob, usually handled base64-encoded:

{
"clientId": "registry-a1b2",
"keyId": "registry-key-c3d4",
"key": "-----BEGIN PRIVATE KEY-----\n..."
}

Two properties are worth understanding before you start:

  • The Identity Service stores only public keys. The private key stays with the client (the registry's secret, the agent's credentials file) and is never sent to or stored by the Identity Service. When a registration tool accepts a full credentials blob, it extracts and stores the public half only.
  • All registration tools are idempotent upserts. Re-running a registration replaces the principal's keys (this is also how you rotate a compromised or expired key) and preserves its identity.

Running the Registration Tools

The registration tools ship inside the Identity Service image itself: /create-tenant, /register-client, and /gen-client-credentials. The image is distroless — there is no shell — so each tool is invoked directly as the container entrypoint.

info

Deployments using the Istari Platform Helm chart can automate these steps with the chart's registration hooks (identity.registryClientRegistration, identity.publicClientRegistration, and identity.agentRegistration, all requiring identity.migrations.runAsJob: true). The manual steps below are for other orchestrators, and for registering additional tenants, clients, or agents after installation.

Two invocation patterns are used throughout this page.

Kubernetes one-off pod (recommended in-cluster — the database URL is injected from the secret and never touches your shell; Kubernetes substitutes $(DATABASE_URL) in args without needing a shell in the container):

kubectl -n istari run identity-registration --rm -i --restart=Never \
--image=istaridigital.jfrog.io/customer-docker/identity-service:<tag> \
--overrides='{
"spec":{"containers":[{
"name":"identity-registration",
"image":"istaridigital.jfrog.io/customer-docker/identity-service:<tag>",
"command":["/<tool>"],
"args":["-database-url","$(DATABASE_URL)","<tool-specific args...>"],
"env":[{"name":"DATABASE_URL","valueFrom":{"secretKeyRef":{
"name":"istari-identity-service",
"key":"ISTARI_DIGITAL_IDENTITY_SERVICE_DATABASE_URL"}}}]
}]}
}'

Plain container (any Docker-compatible runtime with network access to the database):

docker run --rm --entrypoint /<tool> \
istaridigital.jfrog.io/customer-docker/identity-service:<tag> \
-database-url "postgresql://<db_user>:<db_password>@<db_host>:5432/<db_name>" \
<tool-specific args...>

The examples below show only the tool name and its arguments; wrap them in either pattern.

Step 1 — Create a Tenant

Every Zitadel organization must be matched by a tenant in the Identity Service. Create one, mapping it to your existing Zitadel organization:

/create-tenant \
-database-url "$(DATABASE_URL)" \
-slug "<tenant_slug>" \
-display-name "<Human-Readable Name>" \
-provider-name "zitadel" \
-provider-tenant-id "<zitadel_org_id>"
  • -slug is the tenant's stable identifier inside the Identity Service (e.g. acme). It is not the Zitadel organization ID.
  • -provider-name / -provider-tenant-id map the tenant to your Zitadel organization, so tokens issued to the tenant's principals carry the organization context Zitadel-namespaced role lookups need. Use the organization ID recorded during installation.
  • Re-running with the same slug is safe (idempotent).

Step 2 — Register the Registry Service as a Client

The registry service authenticates to the Identity Service (for token introspection and agent provisioning) as a trusted client. Generate its credentials with gen-client-credentials --stdout (no database access needed for this step):

docker run --rm --entrypoint /gen-client-credentials \
istaridigital.jfrog.io/customer-docker/identity-service:<tag> --stdout

Output:

{
"registry_secret": "<base64 credentials blob>",
"registry_client_id": "registry-a1b2",
"frontend_client_id": "frontend-c3d4"
}

Use the three values as follows:

  1. Register the public key with the Identity Service (this stores only the public half of registry_secret):
/register-client \
-database-url "$(DATABASE_URL)" \
-secret "<registry_secret>"
  1. Give the private blob to the registry service: store registry_secret as FILE_SERVICE_IDENTITY_ROUTER_SECRET in the registry's secret (see the install page’s enablement step).

  2. Allow the registry to provision agents: add registry_client_id to the Identity Service's secret as ISTARI_DIGITAL_IDENTITY_SERVICE_AGENT_PROVISIONING_CLIENT_IDS and restart the Identity Service. Without this, agent creation through the platform (UI, CLI, SDK) fails.

  3. Give the frontend its client ID: store frontend_client_id as VITE_IDENTITY_ROUTER_CLIENT_ID in the frontend secret, and register it as a public client in the next step.

Step 3 — Register the Frontend as a Public Client

The frontend is a public OAuth client (browser login flow with PKCE) — it has an identifier but no key. Registering it gives the Identity Service a per-client exact-match allowlist of redirect URIs, so /oauth2/authorize can validate that login requests really come from your frontend and only redirect to it:

/register-client \
-database-url "$(DATABASE_URL)" \
-public-client-id "<frontend_client_id>" \
-redirect-uris "https://<customer_istari_fqdn>"
  • -redirect-uris is comma-separated and exact-match — no wildcard or prefix matching. List every URI the frontend uses as a redirect_uri.
  • -public-client-id and -secret are mutually exclusive: public clients have no key material.
  • Re-running replaces the allowlist (idempotent).

Register the MCP Service (if deployed)

The MCP service logs users in through the Identity Service the same way the frontend does, so its client ID must also be registered before enforcement is turned on — otherwise MCP logins are rejected at /oauth2/authorize:

/register-client \
-database-url "$(DATABASE_URL)" \
-public-client-id "<mcp_client_id>" \
-redirect-uris "https://mcp.<customer_istari_fqdn>/auth/callback"
  • <mcp_client_id> is the value of ISTARI_DIGITAL_IDENTITY_SERVICE_CLIENT_ID in the MCP service's secret (see the install page’s enablement step).
  • The redirect URI is the MCP's OAuth callback: its public base URL plus /auth/callback.
  • The MCP also holds an ISTARI_DIGITAL_IDENTITY_SERVICE_CLIENT_SECRET for its token exchange; current Identity Service releases do not verify it, so the registration above — client ID plus redirect allowlist — is all that is required for enforcement.

Once the frontend (and any other public clients) are registered and browser login is verified, turn on enforcement by setting ISTARI_DIGITAL_IDENTITY_SERVICE_ENFORCE_CLIENT_REGISTRATION to "true" in the Identity Service's secret and restarting it. With enforcement off (the default), unregistered client IDs and redirect URIs are only logged; with it on, they are rejected.

warning

Enable ENFORCE_CLIENT_REGISTRATION only after registering the frontend's — and, if deployed, the MCP service's — client ID and redirect URIs. Enabling it first locks every browser login out.

Verify a Registration

A registered principal proves its registration by exchanging a signed assertion for an Identity Service token at POST /oauth2/token (grant_type=client_credentials with a client_assertion signed by its private key). The easiest way to run this end-to-end is the Istari CLI with a key file generated through the platform:

stari client init "https://api.<customer_istari_fqdn>" --identity-service \
--credentials-file <path_to_key_file> --yes

then run any CLI command. See the CLI documentation for details. A successful exchange returns a token whose claims identify the principal: user_type: agent, the tenant's slug, and — for agents bound to a Zitadel user — the username it was registered with.