OAuth 2.0 Dynamic Client Registration
ZITADEL implements OAuth 2.0 Dynamic Client Registration (RFC 7591).
It exposes a registration_endpoint that lets clients register
themselves as OIDC applications at runtime, instead of being created upfront in the Management Console or through the management API.
This is required by clients that self-register before any user context exists, most notably Model Context Protocol (MCP) clients such as Claude Desktop, claude.ai and the MCP SDKs.
A registered client can later read, update and delete its own registration through the OAuth 2.0 Dynamic Client Registration Management Protocol (RFC 7592), using the registration access token it receives at registration. See Manage a registration.
Dynamic client registration is disabled by default. Only enable it if you understand the trade-offs described in Registration modes below.
Enable dynamic client registration
The endpoint is only served and advertised as registration_endpoint in the
discovery document when it is enabled in the instance's
security settings.
Enable it through the settings API:
curl --request PUT \
--url ${CUSTOM_DOMAIN}/v2/settings/security \
--header 'Authorization: Bearer <token of an IAM_OWNER>' \
--header 'Content-Type: application/json' \
--data '{ "dynamicClientRegistration": { "enabled": true } }'Registration modes
The flow that MCP clients follow expects an open, unauthenticated registration endpoint, because the client registers
before any user has authenticated. As this is a sensitive capability, ZITADEL supports two modes, selected through the
same security settings. Both require enabled above.
| Mode | Setting | Authorization | Organization of the registered client |
|---|---|---|---|
| Token (default) | dynamicClientRegistration.allowUnauthenticated = false | An access token must be sent as Authorization: Bearer header, and its user must hold the project.app.register_dynamic permission in the token's organization. | The token's organization. |
| Open registration | dynamicClientRegistration.allowUnauthenticated = true | None. Required for the MCP flow. | The instance's default organization. |
Who may register in token mode
The project.app.register_dynamic permission is granted by default to ORG_OWNER, IAM_OWNER and IAM_ORG_MANAGER.
For service users that should do nothing but register clients, assign the dedicated
ORG_DYNAMIC_CLIENT_REGISTRAR role instead. It carries this single permission,
so such a user can self-register clients without gaining write access to your existing applications.
Whenever an Authorization: Bearer header is present it is treated as a registration token, even with open registration
enabled. An invalid or expired token is rejected with 401 Unauthorized and a valid token without the permission with
403 Forbidden; neither falls back to anonymous registration.
In both modes the registered clients are stored in a dedicated, auto-provisioned project named ZITADEL DCR in the
respective organization, so they do not interfere with your other projects. When open registration is enabled, protect
the endpoint with the existing instance rate limiting and only allow the redirect URI schemes you trust.
Register a client
Send the client metadata as JSON to the registration_endpoint:
curl --request POST \
--url ${CUSTOM_DOMAIN}/oauth/v2/register \
--header 'Content-Type: application/json' \
--data '{
"client_name": "My MCP Client",
"redirect_uris": ["https://client.example.com/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}'A successful registration returns HTTP 201 with the client information response:
{
"client_id": "340396026519785524",
"client_id_issued_at": 1718000000,
"registration_access_token": "kFqx...redacted...",
"registration_client_uri": "https://${CUSTOM_DOMAIN}/oauth/v2/register/340396026519785524",
"client_name": "My MCP Client",
"redirect_uris": ["https://client.example.com/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}For confidential clients (token_endpoint_auth_method of client_secret_basic or client_secret_post) the response
also contains a client_secret and client_secret_expires_at (0, secrets do not expire).
The returned client_id can be used immediately to start an
authorization code flow. Public clients (token_endpoint_auth_method of
none) must use PKCE.
The registration_access_token and registration_client_uri let the client manage its own registration afterwards, see
Manage a registration. Store the registration access token securely: it is only returned at
registration and when the registration is updated, and it is never stored in clear text on the server.
Supported metadata
| Member | Notes |
|---|---|
redirect_uris | Required. Validated against the same rules as applications created in the Management Console. |
token_endpoint_auth_method | client_secret_basic, client_secret_post or none. Defaults to none for native applications and client_secret_basic for web applications. |
grant_types | Defaults to ["authorization_code"]. refresh_token, implicit, urn:ietf:params:oauth:grant-type:device_code and urn:ietf:params:oauth:grant-type:token-exchange are supported. |
response_types | Defaults to ["code"]. id_token and id_token token are supported. |
application_type | web or native. When omitted, defaults to web, except for redirect URIs with a custom scheme (myapp://callback), which imply native. Clients that only implement RFC 7591 do not send this member. |
client_name | Optional, human-readable name. |
post_logout_redirect_uris | Optional. |
Metadata errors are returned as defined in RFC 7591 §3.2.2,
for example invalid_redirect_uri or invalid_client_metadata, with HTTP status 400. Authorization failures use the
RFC 6750 §3.1 codes invalid_token (401) and
insufficient_scope (403).
Manage a registration
ZITADEL implements the OAuth 2.0 Dynamic Client Registration Management Protocol (RFC 7592).
A registered client manages itself at the client configuration endpoint returned as registration_client_uri
(${CUSTOM_DOMAIN}/oauth/v2/register/{client_id}), authenticating with the registration_access_token from the
registration response. The endpoints are served whenever dynamicClientRegistration.enabled is set, the same switch as
the registration endpoint itself: a client that may register may manage what it registered. allowUnauthenticated
selects the registration mode and does not apply here, as the registration access token
authorizes these endpoints in both modes.
The registration access token is bound to a single client. Send it as an Authorization: Bearer header. A missing or
invalid token is rejected with 401 Unauthorized; once a client has been deleted its endpoint returns 404 Not Found.
The registration access token is the only authorization path: a client manages itself and needs no user permission, so
neither project.app.register_dynamic (which governs who may create clients) nor project.app.write applies. As
operators cannot obtain the token, they manage dynamically registered clients through the Management API and the
Console like any other application, not through these endpoints.
Read
curl --request GET \
--url ${CUSTOM_DOMAIN}/oauth/v2/register/340396026519785524 \
--header 'Authorization: Bearer <registration_access_token>'Returns HTTP 200 with the current client information response. The client_secret is never returned, as it is not
stored in clear text.
Update
An update replaces the stored metadata, it does not merge into it. RFC 7592 §2.2 therefore requires the client to include all metadata fields it received from the registration, read or update response, not only the ones it wants to change. Send the full set, with the new values in place:
curl --request PUT \
--url ${CUSTOM_DOMAIN}/oauth/v2/register/340396026519785524 \
--header 'Authorization: Bearer <registration_access_token>' \
--header 'Content-Type: application/json' \
--data '{
"client_id": "340396026519785524",
"redirect_uris": ["https://client.example.com/callback", "https://client.example.com/callback2"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}'Omitted members are not preserved: they are re-defaulted exactly as during registration, using the same validation and
defaulting rules. Dropping grant_types from the body above would reset the client to the default
["authorization_code"] and silently lose refresh_token, and dropping token_endpoint_auth_method would re-derive it
from the application type. Read the registration back first if the client does not keep its metadata.
Returns HTTP 200 with the updated registration. An update rotates the registration access token: the response contains
a new registration_access_token, so the client must replace its stored token with the new value. Every update rotates
the token, including an update that does not change any metadata. The new token works immediately; the previous token is
invalidated through the eventually consistent projection, so it may keep working for a brief moment after the update.
Delete
curl --request DELETE \
--url ${CUSTOM_DOMAIN}/oauth/v2/register/340396026519785524 \
--header 'Authorization: Bearer <registration_access_token>'Returns HTTP 204 No Content and removes the client.
Limitations
This version intentionally keeps a small scope:
- Free-form metadata such as
logo_uri,client_uriorcontactsis accepted but not persisted. - The
private_key_jwtauthentication method and thejwks/jwks_urimembers are rejected withinvalid_client_metadata. - Dynamically registered clients share the audience of the
ZITADEL DCRproject. A JWT access token issued to one of them carries every client ID registered in that project, plus the project ID, in itsaudclaim. Theresourceparameter (RFC 8707) is accepted on the authorization code flow but ignored, so it does not narrowaud. Do not rely onaudalone to tell one dynamically registered client from another; validate theclient_idorazpclaim instead. - A registration access token is invalidated by rotating it (through an update) or by deleting the client; there is no separate per-token revocation endpoint.
- The read and update responses return the current metadata but not
client_nameorclient_id_issued_at(not persisted in this version), and never returnclient_secret(only its hash is stored). - Changing
token_endpoint_auth_methodto a confidential method (client_secret_basicorclient_secret_post) through an update does not issue aclient_secret; register a new confidential client instead. - A client whose application, project or organization is deactivated (not deleted) returns
404from the management endpoints until it is reactivated.
Was this page helpful?