Skip to content

Dynamic Client Registration (RFC 7591)

A server-to-server registration endpoint based on RFC 7591 — OAuth 2.0 Dynamic Client Registration Protocol. A single POST /oauth/register creates a public (PKCE-only) OAuth client for an RP without any console signup or email verification. The goal is to let MCP connectors (like claude.ai), CLIs, and autonomous agents register themselves without a human going through the console.

📋 Confidential RPs are not eligible for DCR

A client created via DCR is always public — no client_secret is issued and PKCE is mandatory. A confidential RP (a web backend that sends client_secret to /oauth/token) cannot be registered through DCR. Use console app registration for confidential clients. If you're not sure which type you have, read Public vs Confidential first.

When should I use DCR?

  • When a connector or agent must register automatically (e.g. the claude.ai MCP connector — no human can reach the console)
  • Native / desktop clients using custom-scheme redirects (e.g. myapp://oauth/1pass/callback)
  • CLI tools that need to mint their own client at install time

A regular web RP — especially a confidential one that handles a secret — is safer with console registration, which can also grant broader scopes.

Endpoint

POST https://api.1pass.dev/oauth/register
Content-Type: application/json
  • Auth: unauthenticated by default (CSRF-skipped, host-unlocked — the same server-to-server surface as /oauth/token). It is callable without a console signup or email-verification gate.
  • Optional initial access token: if the server has DCR_REQUIRE_INITIAL_ACCESS_TOKEN enabled, an Authorization: Bearer <token> header becomes required (authenticated DCR). It is not required by default. → see Initial access token.

This endpoint is safe not because it requires authentication, but because it uses a defense-in-depth stack: a redirect_uri allowlist + a forced public client shape + a scope ceiling (see Security model).

Request fields (RFC 7591 §3.1)

FieldRequiredDescription
redirect_urisArray of redirect URIs to register. Every one must match the connector allowlist (see redirect_uri constraints).
client_nameDisplay name. Ignored on unauthenticated registration and forced to a fixed label (anti-spoofing on the consent screen). Honored only on authenticated DCR.
scopeSpace-separated scope list. A ceiling is applied (see Scope ceiling).
token_endpoint_auth_methodAccepted but ignored — the response is always none (public).

Any other client metadata in the request body is silently ignored (the server forces the client shape). Clients cannot specify grant_types, response_types, or client_secret.

Response (201 Created, RFC 7591 §3.2.1)

json
{
  "client_id": "logi_pub_xxxxxxxxxxxxxxxx",
  "client_id_issued_at": 1717660800,
  "redirect_uris": ["myapp://oauth/1pass/callback"],
  "token_endpoint_auth_method": "none",
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "scope": "openid agent:read agent:write"
}
FieldMeaning
client_idThe issued public client identifier. Use it in subsequent authorize / token requests.
client_id_issued_atIssue time (Unix epoch seconds).
redirect_urisThe confirmed redirect URIs.
token_endpoint_auth_methodAlways none — no secret.
grant_typesFixed to authorization_code, refresh_token.
response_typesFixed to code.
scopeThe actual allowed scope ceiling (the capped value, not what you requested).

There is no client_secret

Because this is a public client, the response does not include client_secret or client_secret_expires_at. Token exchange is authenticated with PKCE (code_verifier) only. If your integration code expects a secret, that RP is not a DCR candidate — it needs console confidential registration.

curl examples

Unauthenticated (default)

bash
curl -X POST https://api.1pass.dev/oauth/register \
  -H "Content-Type: application/json" \
  -d '{
    "client_name": "My Connector",
    "redirect_uris": ["myapp://oauth/1pass/callback"],
    "scope": "openid"
  }'

# → 201 { "client_id": "...", "token_endpoint_auth_method": "none", ... }
# client_name is ignored on the unauthenticated path and registered under a fixed label.

Authenticated (initial access token)

bash
curl -X POST https://api.1pass.dev/oauth/register \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $DCR_INITIAL_ACCESS_TOKEN" \
  -d '{
    "client_name": "My Tool",
    "redirect_uris": ["myapp://oauth/1pass/callback"],
    "scope": "openid agent:tools.invoke"
  }'

Start the Authorization Code + PKCE flow right away with the issued client_id.

redirect_uri constraints (allowlist)

You cannot register an arbitrary URL. Every requested redirect_uris value must match logi's connector redirect allowlist; if even one fails, the whole request is rejected with invalid_redirect_uri.

  • Non-loopback (https web callbacks): must match exactly (scheme / host / path / query, all of them).
  • Loopback (http://localhost, http://127.0.0.1, http://[::1]): per RFC 8252 §7.3, only the port is flexible; scheme, host, path, and query must match exactly (for desktop clients like Claude Desktop).
  • A URI with a fragment (#...) is rejected.
  • A custom scheme (myapp://...) must be present in the allowlist as registered.

The allowlist is managed by the operations team via the server ENV (CLAUDE_CONNECTOR_REDIRECT_URIS). To add a new connector or scheme, ask them to register it. This allowlist is the core anti-phishing control for DCR, and it shares the same source of truth as redirect validation at authorize time.

Scope ceiling

A requested scope is not granted verbatim — it is capped to a ceiling that depends on the registration path. This value is only the client's allowed-scope ceiling; the scope of the tokens actually issued still goes through user consent at authorize time.

Registration pathGuaranteed baselineAdditionally requestableCannot obtain
Unauthenticated (default)openid agent:read agent:write(no expansion beyond baseline)agent:tools.invoke
Authenticated (initial access token)openid agent:read agent:writeagent:tools.invokeEverything outside the allowlist
  • The full set of allowed scopes is openid, agent:read, agent:write, and agent:tools.invoke. Any other scope (e.g. profile, email) is silently dropped — connectors are designed not to need identity-profile scopes.
  • agent:tools.invoke (tool execution) is the most powerful capability, so it is obtainable only via authenticated DCR. An unauthenticated registration can never reach that ceiling.
  • The baseline is always granted so that nobody can squat an allowlisted redirect with scope=openid only and cripple the real connector (connector DoS).

For scope semantics and userinfo mapping, see the Scope reference.

Idempotent re-registration

Registering again with the same order-insensitive redirect_uris set does not create a new client — it returns the existing one (HTTP 200 OK in that case, vs 201 Created for a fresh registration).

  • Unauthenticated re-registration: returns the existing client unchanged, read-only. Scopes are not widened; this prevents an attacker from adding extra scopes to a squatted client.
  • Authenticated re-registration (initial access token): the existing client's scopes may be widened by union with the requested scopes — but only within the ceiling (ALLOWED_SCOPES).

Matching is limited to DCR-registered clients (it never collides with console-registered clients).

Initial access token (authenticated DCR)

The operations team can turn on an authentication gate via server ENV.

ENVMeaning
DCR_REQUIRE_INITIAL_ACCESS_TOKENWhen true, every /oauth/register call requires Authorization: Bearer <token>.
DCR_INITIAL_ACCESS_TOKENThe token value to validate against when the gate is on.

With the gate on, a missing or wrong token returns 401 invalid_token. Only the authenticated path allows the agent:tools.invoke ceiling, idempotent scope widening, and a custom client_name.

Rate limit

/oauth/register is limited to 10 requests per minute per remote IP. If the limit is exceeded:

FieldValue
HTTP status429 Too Many Requests
errorrate_limited
error_descriptiontoo many registration requests
Retry-After header❌ not set

Since there is no Retry-After, back off with exponential backoff. DCR is meant to be a one-time registration at install — don't re-register on every request. Store the issued client_id and reuse it (idempotent re-registration recovers it if lost).

Error codes

Every error is returned as { "error": "...", "error_description": "..." } JSON.

errorHTTPMeaningAction
invalid_redirect_uri400redirect_uris missing / contains a fragment / doesn't match the allowlistMatch the URI exactly to an allowlist value, or ask ops to register it
invalid_client_metadata400Validation failed, or an unsupported (un-seeded) scopeRestrict scope to the allowed set; check the request body
invalid_token401The initial-access-token gate is on but the token is missing or wrongCheck the Authorization: Bearer token
rate_limited429More than 10 requests/min per IPRetry with exponential backoff; cache the registration result
server_error500A server-side configuration problem (e.g. system-bot conflict)Transient — contact the operations team

Security model

DCR is the most dangerous surface because it is unauthenticated. Instead of auth, it is protected by the following layered controls.

  • redirect_uri allowlist — blocks registering arbitrary redirects (the core anti-phishing control); shares its source of truth with authorize.
  • Forced public shapeclient_type=public + token_endpoint_auth_method=none. There is no secret to leak, and PKCE is mandatory.
  • Scope ceiling — unauthenticated registrations can never reach agent:tools.invoke; only the baseline is guaranteed.
  • Fixed client_name — the unauthenticated path does not trust the caller's client_name; it uses a fixed label to prevent consent-screen spoofing.
  • System developer ownership — every DCR client is owned by a single system bot, making bulk audit / kill-switch trivial.
  • Idempotent deduplication + rate limiting — blocks duplicate issuance and registration floods.

References

Identity가 제품의 신뢰를 만듭니다.