OAuth 오류 코드
Source:
oauth/errors.md· Live: https://docs.1pass.dev/oauth/errors LLM-sanitized: internal links absolutized, VitePress containers → admonitions, line numbers in the Jump-to Index reference this rendered file (1-indexed).
📍 Jump-to Index
- L40-L61: ## 응답 시그널 한눈에
- L62-L217: ## 코드별 레퍼런스
- L67-L82: ### invalid_request
- L83-L100: ### invalid_client
- L101-L112: ### unauthorized_client
- L113-L133: ### invalid_grant
- L134-L147: ### invalid_scope
- L148-L158: ### unsupported_grant_type
- L159-L164: ### unsupported_response_type
- L165-L172: ### access_denied
- L173-L188: ### invalid_token
- L189-L198: ### authorization_pending / slow_down / expired_token
- L199-L217: ### rate_limited
- L218-L226: ## 에러 트리아지 워크플로
- L227-L239: ## 로깅 주의사항
- L240-L246: ## 참고 RFC
OAuth 오류 코드
모든 오류 응답은 RFC 6749 §5.2 포맷을 따릅니다.
{
"error": "<기계 판독 가능 코드>",
"error_description": "<사람이 읽기 위한 설명>",
"error_uri": "https://docs.1pass.dev/oauth/errors#<error>",
"request_id": "8f0c1e7b-..."
}응답 시그널 한눈에
RP 서버가 받는 신호는 body 와 header 두 채널로 나옵니다. 콘솔(웹)과 같은 request_id 로 연결되도록 설계되어 있어, RP 가 자기 로그에 cross-reference 하기 좋습니다.
| 채널 | 필드 | 용도 |
|---|---|---|
| body | error | 기계 판독용 코드 (RFC 6749 §5.2) |
| body | error_description | 사람이 읽는 1줄 설명 |
| body | error_uri | 이 문서의 해당 코드 앵커 |
| body | request_id | 콘솔 request log 와 동일 키 |
| header | X-Logi-Request-Id | body 가 비어 있는 경우(예: HEAD)에도 trace 가능 |
| header | X-Logi-Console-Url | 운영에서 RP 가 인증된 경우, 콘솔 request_logs 딥링크 |
| header | X-Logi-Scope-Drift | token 200 응답에 동봉 — 최근 7일 내 drift 이력이 있을 때. 기본 block 정책에선 drift 요청 자체가 invalid_scope 로 거절되며, 헤더는 log_only/alert 명시 앱 또는 거절·기록된 이력의 echo |
💡 Tip: 처음 통합하는 RP 라면 응답 body 의
error_description만 그대로 RP 서버 로그에 INFO 레벨로 기록해두세요. 대부분의 통합 실수는redirect_uri mismatch/PKCE verifier mismatch두 개로 설명되며,error_description이 정확한 원인을 담고 있습니다.
코드별 레퍼런스
각 코드 앵커는 OAuth 응답의 error_uri 가 가리키는 위치입니다. URL 형식: https://docs.1pass.dev/oauth/errors#<error>
invalid_request
의미: 요청 자체가 RFC 6749 형식을 위반했습니다.
흔한 원인:
redirect_uri가 앱 등록 정보의 화이트리스트와 정확히 일치하지 않음 (scheme/host/path/query 모두)- 앱이
sandboxtier 인데localhost/127.0.0.1외 실도메인 redirect_uri 를 요청 — sandbox tier 는 로컬 호스트만 허용하므로, redirect_uri 가 콘솔에 등록돼 있어도redirect_uri not registered로 거절됨. 실도메인으로 가려면 Production 승급 필요 code_challenge_method가S256이 아님 (plain미지원)- 필수 파라미터 누락 (
response_type,client_id,redirect_uri,code_challenge)
빠른 진단:
- 콘솔 → 앱 상세 → "Redirect URIs" 와 RP 가 보낸 URL 을 글자 단위로 비교
- trailing slash, 쿼리 스트링, fragment 모두 제거 후 일치하는지 확인
- PKCE 라이브러리가
S256출력하는지 확인 (Buffer.from(sha256(verifier)).toString('base64url')) error_description이redirect_uri not registered인데 콘솔의 Redirect URIs 에 분명히 등록돼 있다면 → 앱 tier 가sandbox인지 확인. sandbox 는localhost/127.0.0.1만 허용하므로 실도메인은 Production 승급 후 사용 가능
invalid_client
의미: client 인증 실패 — client_id / client_secret 불일치 또는 secret 누락.
HTTP: 401
흔한 원인:
- 백엔드 없는 모바일/SPA/CLI 앱을 confidential 로 등록함 (가장 흔함) —
error_description이client credentials missing이면 이 케이스. client type 을 명시하지 않으면 기본값이confidential이라, secret 을 안전하게 보관할 수 없는 앱이 매 토큰 교환에서 실패한다. → public client (PKCE) 로 등록해야 한다. client_secret환경변수가 dev/prod 사이에 잘못 매핑됨- HTTP Basic auth 헤더의 base64 인코딩 실수 (
client_id:client_secret형태) - secret rotate 후 RP 서버 재배포 누락 → 옛 secret 사용
- (public client 인데)
client_secret또는 HTTP Basic 인증을 보냄 → downgrade 방어로 거절 (public clients must not present client_secret)
빠른 진단:
error_description이client credentials missing인가? → 콘솔 → 앱 상세 → Client type 확인. 백엔드가 없는데confidential이면 public 으로 재등록해야 한다 (client_type은 생성 후 변경 불가 → 새 RP 등록). Public client 가이드 · Public vs Confidential 결정- 콘솔 → 앱 상세 → "Secret 재발급" → 새 secret 으로 RP env 갱신
- PKCE-only RP 라면 secret 자체가 불필요 (PKCE 가이드)
unauthorized_client
의미: client 자체는 인증됐지만 현재 작업 권한이 없음.
흔한 원인:
- 앱이
pending상태 (관리자 승인 대기 —localhost외 도메인일 때) - 앱이
suspended상태 (운영진이 정지함 — 콘솔 → 감사 로그 확인) - 앱이 사용한
grant_type이 등록 시 허용되지 않음
빠른 진단:
- 콘솔 → 앱 상세 → Status pill 확인.
pending이면 Production 승급 신청을 진행하세요. 승급 신청을 제출해야 앱 상세에 진행 단계 stepper 가 표시됩니다 (신청 전에는 stepper 가 보이지 않습니다).
invalid_grant
의미: authorization_code / refresh_token 이 유효하지 않음.
HTTP: 400
흔한 원인 + 정확한 error_description:
code not found— code 가 이미 한 번 교환됐거나 존재한 적 없음code already used— 같은 code 로 두 번째 교환 시도. authorization code 는 1회용code expired— code 발급 후 10분 초과redirect_uri mismatch—/oauth/authorize때와/oauth/token때의 URI 가 다름PKCE verifier mismatch—code_verifier가 처음 제출한code_challenge와 불일치refresh token not found— RT 가 revoke 됐거나 다른 client 로 발행됨refresh token reuse detected; chain revoked— 재사용 공격 탐지 — 토큰 체인 전체 폐기. 사용자에게 재로그인 강제refresh token expired— RT 30일 경과
빠른 진단:
- PKCE verifier 소실이 가장 흔함 —
sessionStorage사용 시 탭 전환/새로고침으로 휘발하는지 확인 redirect_uri는 authorize 와 token 에 완전히 동일 해야 함 — trailing slash 하나도 다르면 실패- 콘솔 → 앱 상세 → 에러 로그에서
request_id로 정확한 원인 확인
invalid_scope
의미: 요청한 scope 가 앱에 등록되지 않았거나 비어있음.
흔한 원인:
- Scope drift 기본 정책(
block) — 등록 안 된 scope 가 요청에 하나라도 포함됨 allowed_scopes미설정 상태에서 scope 파라미터 전송- 오타 (예:
email↔email_address) - 커스텀 scope 의 namespace prefix 누락 (
<client_id>:reviewer_role형식)
빠른 진단:
- 콘솔 → 앱 상세 → "Allowed Scopes" 에 등록된 정확한 이름 확인
- Scope drift 헤더(
X-Logi-Scope-Drift)가 함께 떴다면 Scope drift 정책
unsupported_grant_type
의미: 지원하지 않는 grant_type 사용.
지원하는 값:
authorization_coderefresh_tokenurn:ietf:params:oauth:grant-type:device_code(RFC 8628)
password, implicit, client_credentials 는 지원하지 않습니다 (보안상 의도적 미지원).
unsupported_response_type
의미: /oauth/authorize 의 response_type 이 code 가 아님.
logi 는 Authorization Code Flow 만 지원합니다. token (implicit), id_token 단독은 미지원.
access_denied
의미: 사용자가 동의 화면에서 "거부" 선택, 또는 device flow 에서 거부됨.
HTTP: 302 (authorize) 또는 400 (token)
대응: RP 는 사용자에게 "로그인 취소됨" UX 제공, 재시도 진입점 노출.
invalid_token
의미: /oauth/userinfo 에서 Bearer access_token 검증 실패.
HTTP: 401 + WWW-Authenticate: Bearer error="invalid_token"
흔한 원인:
- JWT 서명 검증 실패 (RP 가 잘못된 JWKS 사용)
- 토큰 만료 (
expires_in경과) - 토큰 명시적 revoke 됨
- 사용자 계정 soft-delete
빠른 진단:
- JWKS 캐시 무효화 후 재시도
- 토큰 발급 시점의
expires_in과 현재 시각 비교
authorization_pending / slow_down / expired_token
의미: Device Authorization Grant (RFC 8628) 폴링 응답.
| error | 의미 | 대응 |
|---|---|---|
authorization_pending | 사용자가 아직 device_code 승인 안 함 | interval 만큼 대기 후 재폴링 |
slow_down | 폴링이 너무 빠름 | interval 을 5초씩 늘려 재폴링 |
expired_token | device_code 만료 | 처음부터 다시 시작 |
rate_limited
의미: rate-limit 한도 초과.
HTTP: 429
한도:
| 엔드포인트 | 한도 | 키 |
|---|---|---|
/session | 5/min (Cloudflare) · 10/3min (Rails) | IP |
/oauth/token | 20/min | client_id |
/oauth/device_authorization | 30/min | client_id |
/api/v1/me/merge/otp | 10/min | user_id |
대응: Retry-After 헤더 존중, 지수 백오프, 단일 사용자에 대해 토큰 캐시 적극 사용.
에러 트리아지 워크플로
새 통합에서 4xx 가 났을 때 따라갈 절차:
- 응답 body 의
error_description을 RP 서버 로그에 그대로 남기세요. — 가장 흔한 실수의 원인이 여기에 직접 적혀있습니다. request_id또는X-Logi-Request-Id헤더를 같이 기록하세요. — 콘솔 request_logs 에서 같은 키로 정확히 매칭됩니다.- 운영에선
X-Logi-Console-Url헤더의 URL 을 클릭하세요. — 해당 요청이 콘솔에 즉시 떠 있습니다. - 이 문서의
error_uri앵커를 따라가세요. — 위 코드별 섹션에 흔한 원인 + 빠른 진단이 적혀있습니다.
로깅 주의사항
절대 로그에 남기지 말 것:
password,client_secret,code_verifier,refresh_token,access_token(JWT 포함),logi_pak_*- logi 자체는
password_digest,otp_secret_encrypted, JWT, PAK plaintext 를 한 번도 로그에 남기지 않습니다.
남겨도 안전한 것:
error,error_description,error_urirequest_id,X-Logi-Request-Idclient_id,redirect_uri(PII 아님)