Dynamic Client Registration (RFC 7591)
RFC 7591 — OAuth 2.0 Dynamic Client Registration Protocol 기반의 server-to-server 등록 엔드포인트입니다. POST /oauth/register 한 번으로 RP 가 콘솔 가입·이메일 인증 없이 public(PKCE-only) OAuth client 를 직접 발급받습니다. claude.ai 같은 MCP connector, CLI, 자율 agent 가 사람이 콘솔을 거치지 않고 스스로 등록할 수 있게 하는 것이 목적입니다.
📋 Confidential RP 는 DCR 대상이 아닙니다
DCR 로 만들어지는 client 는 항상 public 입니다 — client_secret 이 발급되지 않고 PKCE 가 필수입니다. 서버에서 client_secret 을 /oauth/token 에 전달하는 confidential RP(웹 백엔드 등)는 DCR 로 등록할 수 없습니다. confidential 은 콘솔 앱 등록 을 쓰세요. 어느 타입인지 모르겠다면 Public vs Confidential 결정 을 먼저 읽어주세요.
언제 DCR 을 쓰나요?
- connector / agent 가 자동으로 등록해야 할 때 (claude.ai MCP connector 등 — 사람이 콘솔에 못 들어감)
- 커스텀 스킴 redirect 를 쓰는 네이티브 / 데스크톱 client (예:
myapp://oauth/1pass/callback) - CLI 도구 가 설치 시점에 client 를 자체 발급해야 할 때
일반 웹 RP(특히 secret 을 다루는 confidential)는 콘솔 등록 이 더 안전하고 더 넓은 scope 를 받을 수 있습니다.
엔드포인트
POST https://api.1pass.dev/oauth/register
Content-Type: application/json- 인증: 기본 unauthenticated (CSRF-skip, host-unlocked —
/oauth/token과 동일한 server-to-server surface). 콘솔 가입이나 이메일 인증 게이트 없이 호출됩니다. - 선택적 initial access token: 서버 ENV
DCR_REQUIRE_INITIAL_ACCESS_TOKEN을 켜면Authorization: Bearer <token>이 필요해집니다(authenticated DCR). 기본은 미요구. → Initial access token 참고.
이 surface 가 안전한 이유는 인증이 아니라 redirect_uri allowlist + 강제 public 형상 + scope 상한 의 다층 방어 때문입니다(보안 모델 참고).
요청 필드 (RFC 7591 §3.1)
| 필드 | 필수 | 설명 |
|---|---|---|
redirect_uris | ✅ | 등록할 redirect URI 배열. 모두 connector allowlist 에 맞아야 함(아래 redirect_uri 제약) |
client_name | ⬜ | 표시용 이름. unauthenticated 등록에서는 무시되고 고정 라벨로 강제됨(consent 화면 위장 방지). authenticated DCR 에서만 반영 |
scope | ⬜ | 공백 구분 scope 목록. 상한이 적용됨(scope 상한) |
token_endpoint_auth_method | ⬜ | 받아들이지만 무시 — 응답은 항상 none(public) |
요청 본문에 위 외의 client metadata 를 넣어도 조용히 무시됩니다(client 형상은 서버가 강제). grant_types / response_types / client_secret 등은 클라이언트가 지정할 수 없습니다.
응답 (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"
}| 필드 | 의미 |
|---|---|
client_id | 발급된 public client 식별자. 이후 authorize / token 요청에 사용 |
client_id_issued_at | 발급 시각(Unix epoch 초) |
redirect_uris | 등록 확정된 redirect URI 목록 |
token_endpoint_auth_method | 항상 none — secret 없음 |
grant_types | authorization_code, refresh_token 고정 |
response_types | code 고정 |
scope | 실제 허용된 scope 상한(요청값이 아니라 cap 적용 후 값) |
client_secret 은 없습니다
public client 이므로 응답에 client_secret / client_secret_expires_at 이 들어있지 않습니다. 토큰 교환은 PKCE(code_verifier)로만 인증합니다. secret 을 기대하는 통합 코드가 있다면 그 RP 는 DCR 대상이 아니라 콘솔 confidential 등록 이 필요합니다.
curl 예시
unauthenticated (기본)
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 은 unauthenticated 라 무시되고 고정 라벨로 등록됩니다.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"
}'발급받은 client_id 로 곧바로 Authorization Code + PKCE Flow 를 시작하면 됩니다.
redirect_uri 제약 (allowlist)
아무 URL 이나 등록되지 않습니다. 요청한 모든 redirect_uris 가 logi 의 connector redirect allowlist 에 맞아야 하며, 하나라도 어긋나면 전체 요청이 invalid_redirect_uri 로 거절됩니다.
- non-loopback(https 웹 콜백): allowlist 와 정확히 일치해야 함(scheme / host / path / query 모두).
- loopback(
http://localhost,http://127.0.0.1,http://[::1]): RFC 8252 §7.3 에 따라 port 만 유연하게 매칭, scheme · host · path · query 는 정확히 일치해야 함(Claude Desktop 등 데스크톱 client 용). - fragment(
#...) 가 있는 URI 는 거절됩니다. - 커스텀 스킴(
myapp://...)은 그 값이 allowlist 에 등록돼 있어야 합니다.
allowlist 는 운영팀이 서버 ENV(CLAUDE_CONNECTOR_REDIRECT_URIS)로 관리합니다. 새 connector / 스킴을 추가하려면 운영팀에 등록을 요청하세요. 이 allowlist 가 DCR 의 핵심 anti-phishing 통제이며, authorize 시점의 redirect 검증과 동일한 출처를 공유합니다.
scope 상한
요청한 scope 는 그대로 부여되지 않고 등록 경로에 따른 상한(ceiling) 으로 잘립니다. 이 값은 client 의 허용 scope 상한 일 뿐이며, 실제 발급되는 토큰의 scope 는 여전히 authorize 시점의 사용자 동의를 거칩니다.
| 등록 경로 | 보장 baseline | 추가로 요청 가능 | 받을 수 없음 |
|---|---|---|---|
| unauthenticated (기본) | openid agent:read agent:write | (baseline 외 확장 없음) | agent:tools.invoke |
| authenticated (initial access token) | openid agent:read agent:write | agent:tools.invoke | allowlist 밖 전부 |
- 전체 허용 scope 집합은
openid,agent:read,agent:write,agent:tools.invoke입니다. 이 밖의 scope(예:profile,email)는 요청해도 조용히 드롭됩니다 — connector 는 신원 프로필 scope 가 필요 없도록 설계돼 있습니다. agent:tools.invoke(도구 실행)는 가장 강력한 권한이라 authenticated DCR 에서만 받을 수 있습니다. unauthenticated 등록은 절대 이 상한을 얻지 못합니다.- baseline 이 항상 부여되는 이유: 누군가 allowlist 된 redirect 를
scope=openid로만 선점해 정식 connector 를 불구로 만드는 것(connector DoS)을 막기 위함입니다.
scope 의 의미와 userinfo 매핑은 Scope 레퍼런스 를 참고하세요.
멱등(idempotent) 재등록
동일한 redirect_uris 집합(SET, 순서 무관) 으로 다시 등록하면 새 client 를 만들지 않고 기존 client 를 그대로 반환합니다(이때 HTTP 상태는 200 OK, 신규 발급은 201 Created).
- unauthenticated 재등록: 기존 client 를 변경 없이 read-only 로 반환. scope 가 넓어지지 않습니다(선점 client 에 권한을 union 으로 끼워넣는 공격 차단).
- authenticated 재등록(initial access token): 기존 client 의 scope 를 요청값과 union 으로 WIDEN 가능. 단 상한(
ALLOWED_SCOPES) 안에서만.
매칭 대상은 DCR 로 등록된 client 로 한정됩니다(콘솔 등록 client 와 충돌하지 않음).
Initial access token (authenticated DCR)
운영팀이 서버 ENV 로 게이트를 켤 수 있습니다.
| ENV | 의미 |
|---|---|
DCR_REQUIRE_INITIAL_ACCESS_TOKEN | true 이면 모든 /oauth/register 호출에 Authorization: Bearer <token> 필요 |
DCR_INITIAL_ACCESS_TOKEN | 위 게이트가 켜졌을 때 검증할 토큰 값 |
게이트가 켜진 상태에서 토큰이 없거나 틀리면 401 invalid_token 이 떨어집니다. authenticated 경로에서만 agent:tools.invoke 상한 + 멱등 scope WIDEN + 커스텀 client_name 이 허용됩니다.
Rate limit
/oauth/register 는 remote IP 기준 분당 10건 으로 제한됩니다. 초과 시:
| 필드 | 값 |
|---|---|
| HTTP status | 429 Too Many Requests |
error | rate_limited |
error_description | too many registration requests |
Retry-After 헤더 | ❌ 미설정 |
Retry-After 가 없으므로 RP 는 exponential backoff 으로 재시도하세요. DCR 은 설치/최초 1회성 등록이 정상 패턴입니다 — 매 요청마다 등록을 반복하지 말고, 발급받은 client_id 를 저장해 재사용하세요(멱등 재등록으로 복구 가능).
에러 코드
모든 에러는 { "error": "...", "error_description": "..." } JSON 으로 반환됩니다.
error | HTTP | 의미 | 처리 |
|---|---|---|---|
invalid_redirect_uri | 400 | redirect_uris 누락 / fragment 포함 / allowlist 불일치 | URI 를 allowlist 값과 정확히 맞추거나 운영팀에 등록 요청 |
invalid_client_metadata | 400 | 검증 실패하거나 지원하지 않는(seed 안 된) scope | scope 를 허용 집합으로 제한, 요청 본문 점검 |
invalid_token | 401 | initial access token 게이트가 켜졌는데 토큰이 없거나 틀림 | Authorization: Bearer 토큰 확인 |
rate_limited | 429 | IP 기준 분당 10건 초과 | exponential backoff 후 재시도, 등록 결과 캐시 |
server_error | 500 | 서버 측 설정 문제(system bot 충돌 등) | 일시 오류 — 운영팀 문의 |
보안 모델 (방어 기준)
DCR 은 unauthenticated 라 가장 위험한 surface 입니다. 인증 대신 다음 다층 통제로 보호됩니다.
- ✅ redirect_uri allowlist — 임의 redirect 등록 차단(anti-phishing 핵심). authorize 와 동일 출처 공유.
- ✅ 강제 public 형상 —
client_type=public+token_endpoint_auth_method=none. 유출될 secret 자체가 없음, PKCE 필수. - ✅ scope 상한 — unauthenticated 는
agent:tools.invoke도달 불가. baseline 만 보장. - ✅ 고정 client_name — unauthenticated 경로는 caller 의
client_name을 신뢰하지 않고 consent 화면 위장을 막는 고정 라벨 사용. - ✅ system developer ownership — 모든 DCR client 가 단일 system bot 소유 → bulk audit / kill-switch 가 간단.
- ✅ 멱등 dedup + rate limit — 중복 발급·등록 폭주 차단.
레퍼런스
- RFC 7591 — OAuth 2.0 Dynamic Client Registration Protocol
- RFC 8252 §7.3 — Loopback Interface Redirection (port 유연 매칭)
- Public vs Confidential 결정 — DCR 은 public 전용
- Public Clients (PKCE-only)
- PKCE (RFC 7636) — secret 없는 토큰 교환
- Authorization Code Flow — 발급받은 client_id 로 시작
- Scope 레퍼런스
- 콘솔 앱 등록 — confidential RP 경로