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_TOKENenabled, anAuthorization: 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)
| Field | Required | Description |
|---|---|---|
redirect_uris | ✅ | Array of redirect URIs to register. Every one must match the connector allowlist (see redirect_uri constraints). |
client_name | ⬜ | Display name. Ignored on unauthenticated registration and forced to a fixed label (anti-spoofing on the consent screen). Honored only on authenticated DCR. |
scope | ⬜ | Space-separated scope list. A ceiling is applied (see Scope ceiling). |
token_endpoint_auth_method | ⬜ | Accepted 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)
{
"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"
}| Field | Meaning |
|---|---|
client_id | The issued public client identifier. Use it in subsequent authorize / token requests. |
client_id_issued_at | Issue time (Unix epoch seconds). |
redirect_uris | The confirmed redirect URIs. |
token_endpoint_auth_method | Always none — no secret. |
grant_types | Fixed to authorization_code, refresh_token. |
response_types | Fixed to code. |
scope | The 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)
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)
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 path | Guaranteed baseline | Additionally requestable | Cannot obtain |
|---|---|---|---|
| Unauthenticated (default) | openid agent:read agent:write | (no expansion beyond baseline) | agent:tools.invoke |
| Authenticated (initial access token) | openid agent:read agent:write | agent:tools.invoke | Everything outside the allowlist |
- The full set of allowed scopes is
openid,agent:read,agent:write, andagent: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=openidonly 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.
| ENV | Meaning |
|---|---|
DCR_REQUIRE_INITIAL_ACCESS_TOKEN | When true, every /oauth/register call requires Authorization: Bearer <token>. |
DCR_INITIAL_ACCESS_TOKEN | The 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:
| Field | Value |
|---|---|
| HTTP status | 429 Too Many Requests |
error | rate_limited |
error_description | too 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.
error | HTTP | Meaning | Action |
|---|---|---|---|
invalid_redirect_uri | 400 | redirect_uris missing / contains a fragment / doesn't match the allowlist | Match the URI exactly to an allowlist value, or ask ops to register it |
invalid_client_metadata | 400 | Validation failed, or an unsupported (un-seeded) scope | Restrict scope to the allowed set; check the request body |
invalid_token | 401 | The initial-access-token gate is on but the token is missing or wrong | Check the Authorization: Bearer token |
rate_limited | 429 | More than 10 requests/min per IP | Retry with exponential backoff; cache the registration result |
server_error | 500 | A 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 shape —
client_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
- RFC 7591 — OAuth 2.0 Dynamic Client Registration Protocol
- RFC 8252 §7.3 — Loopback Interface Redirection (port-flexible matching)
- Public vs Confidential — DCR is public-only
- Public Clients (PKCE-only)
- PKCE (RFC 7636) — token exchange without a secret
- Authorization Code Flow — start with the issued client_id
- Scope reference
- Console app registration — the confidential RP path