Skip to content

How do I register an M2M client and assign it groups without an IdP Admin API token?

Short answer: use the direct M2M client registration API at /api/iam/m2m-clients. Create the M2M client in your IdP (Keycloak, Okta, Entra, Auth0) as you normally would, then register its client_id with the registry and assign groups. The registry writes directly to its own idp_m2m_clients collection -- no OKTA_API_TOKEN or equivalent IdP Admin API credentials required.

When to use this

  • Your enterprise gates IdP Admin API tokens (e.g. Okta requires approval for Admin API access) and getting one is disproportionate overhead.
  • You already know the M2M client_id you want to register (it lives in the IdP).
  • You want to assign groups so the registry's auth server can enrich M2M tokens with those groups during authorization.

If OKTA_API_TOKEN / AUTH0_M2M_CLIENT_ID etc. are available, the existing /api/iam/okta/m2m/* or /api/iam/auth0/m2m/* sync endpoints cover the same ground. This FAQ is for the case where those credentials are not available.

Prerequisites

  • A user JWT (or static API token) with admin scope on the registry.
  • The client_id of an M2M client you have already created in your IdP.
  • M2M_DIRECT_REGISTRATION_ENABLED=true on the registry (this is the default).

Step 1: Create the M2M client in your IdP

In Keycloak Admin UI (example; equivalent steps apply in Okta/Entra/Auth0):

  1. Navigate to Clients > Create client.
  2. Client type: OpenID Connect. Client ID: e.g. my-automation-pipeline. Save.
  3. Enable Client authentication and Service accounts roles. Disable standard/direct flows. Save.
  4. Copy the Client Secret from the Credentials tab. Your application will use this pair to request tokens from Keycloak.

You do not need to assign groups inside the IdP. The registry resolves groups from its own idp_m2m_clients collection, which you will populate in the next step.

Step 2: Register the client with the registry

Using the bundled CLI (api/registry_management.py):

export REGISTRY_URL=http://localhost
export TOKEN_FILE=~/repos/mcp-gateway-registry/.token   # admin user token

uv run python api/registry_management.py \
  --registry-url $REGISTRY_URL --token-file $TOKEN_FILE \
  m2m-client-create \
  --client-id my-automation-pipeline \
  --client-name "My Automation Pipeline" \
  --groups pipeline-operators,registry-readonly \
  --description "CI/CD pipeline service account"

Expected output:

M2M client registered successfully

Client ID:    my-automation-pipeline
Name:         My Automation Pipeline
Provider:     manual
Enabled:      True
Groups:       pipeline-operators, registry-readonly
Description:  CI/CD pipeline service account
Created by:   admin
Created at:   ...
Updated at:   ...

Provider: manual means the record was created via this API (rather than synced from an IdP). Manual records are the only ones this API can modify or delete later.

Step 2b (optional): Register via the Registry UI

In v1.0.22 and later, the same registration is available in the UI:

  1. Open Settings > IAM > M2M Accounts.
  2. Click Register existing client (the gray button next to the purple "Create M2M Account" button).
  3. Fill in Client ID, a human-readable Client Name, pick one or more groups, and (optionally) a description.
  4. Click Register. The new record appears in the list with a manual provider badge, and the Registered by column shows the admin username who submitted the form.

Use the UI path when you want a point-and-click flow; use the CLI path from Step 2 for scripted automation or CI.

The same list page also shows records synced from your IdP (okta, auth0, keycloak, entra). Edit and Delete are disabled on those rows because they are owned by the IdP sync layer; manage them in the IdP instead.

Least-privilege reminder

Registered clients inherit the authorization of their assigned groups as soon as the next token is issued. Grant only the minimum groups necessary. In practice:

  • Start with a read-only group (for example registry-readonly) and add more only when the workload actually needs them.
  • Prefer narrow groups (for example pipeline-operators) over broad ones (for example registry-admins).
  • Removing a group via the UI Edit view or via PATCH /api/iam/m2m-clients/{client_id} takes effect on the next token issued for that client; existing tokens retain their groups until they expire.

Rotating or recovering the client secret

The registry does not manage secrets for manually-registered clients. The client secret lives in your IdP (Keycloak / Entra / Okta / Auth0), where it was created. If you lose it:

  • Rotate the secret in your IdP's admin UI (Keycloak: Clients > your client > Credentials > Regenerate secret; Entra: App registrations > your app > Certificates & secrets).
  • Update your application with the new secret. No change is required in the registry: client_id did not change, so groups and authorization continue to work.

If you want the registry to manage secrets end-to-end, use the legacy "Create M2M Account" path instead; that flow requires an IdP Admin API token.

Step 3: Verify from your application

  1. Request an M2M access token from Keycloak using client credentials:
curl -X POST \
  "http://localhost:8080/realms/mcp-gateway/protocol/openid-connect/token" \
  -d "grant_type=client_credentials" \
  -d "client_id=my-automation-pipeline" \
  -d "client_secret=<from-keycloak-ui>"
  1. Call the registry with that token. The registry's auth server looks up client_id=my-automation-pipeline in idp_m2m_clients, enriches the token with groups ["pipeline-operators", "registry-readonly"], and authorization proceeds based on those groups.
curl -H "Authorization: Bearer $M2M_TOKEN" "$REGISTRY_URL/api/servers"

Managing registered clients

All commands below use the same --registry-url/--token-file prefix as above.

List:

uv run python api/registry_management.py \
  --registry-url $REGISTRY_URL --token-file $TOKEN_FILE \
  m2m-client-list --provider manual

Supports --limit, --skip, --json.

Get one:

uv run python api/registry_management.py \
  --registry-url $REGISTRY_URL --token-file $TOKEN_FILE \
  m2m-client-get --client-id my-automation-pipeline

Update (partial -- fields you omit are left unchanged; pass --groups "" to clear groups):

# Change groups only
uv run python api/registry_management.py \
  --registry-url $REGISTRY_URL --token-file $TOKEN_FILE \
  m2m-client-update \
  --client-id my-automation-pipeline \
  --groups registry-readonly

# Disable the client (kill switch)
uv run python api/registry_management.py \
  --registry-url $REGISTRY_URL --token-file $TOKEN_FILE \
  m2m-client-update \
  --client-id my-automation-pipeline \
  --enabled false

Delete:

uv run python api/registry_management.py \
  --registry-url $REGISTRY_URL --token-file $TOKEN_FILE \
  m2m-client-delete --client-id my-automation-pipeline --force

HTTP endpoints (for reference)

Method Path Auth
POST /api/iam/m2m-clients admin
GET /api/iam/m2m-clients any authenticated user (paginated)
GET /api/iam/m2m-clients/{client_id} any authenticated user
PATCH /api/iam/m2m-clients/{client_id} admin
DELETE /api/iam/m2m-clients/{client_id} admin

Things to know

  • Ownership guard: records created by this API have provider: "manual". Records written by the existing Okta/Auth0 sync services (provider: "okta", provider: "auth0") are visible via GET but return HTTP 403 on PATCH or DELETE from this API, to prevent conflicts with IdP sync.
  • Duplicate client_id: returns HTTP 409 Conflict. One client_id can only have one record across all providers.
  • Admins grant privilege directly: any admin calling this API can assign any group to any client_id. The audit log records every mutation with the calling admin's identity for accountability. Treat the registry admin role accordingly.
  • Feature flag: M2M_DIRECT_REGISTRATION_ENABLED (default true) disables the whole router if set to false. Surface the flag on the System Config page under Authentication.