OAuth 오류 코드
모든 오류 응답은 RFC 6749 §5.2 포맷을 따릅니다.
json
{
"error": "<기계 판독 가능 코드>",
"error_description": "<사람이 읽기 위한 설명>"
}/oauth/authorize 에서
| error | 의미 | 어디로 | 대응 |
|---|---|---|---|
invalid_client | client_id 미존재 | HTML/JSON 400 | client_id 오타/복사 실수 확인 |
unauthorized_client | 앱이 pending/suspended 상태 | HTML/JSON 400 | 관리자 승인 대기 |
invalid_request (redirect) | redirect_uri 화이트리스트 불일치 | HTML/JSON 400 | 앱 등록 정보의 redirect_uris와 정확히 일치 필요 (scheme/host/path/query) |
invalid_request (protocol) | PKCE 누락, code_challenge_method ≠ S256 | 302 redirect_uri?error=... | S256 + challenge 포함 |
unsupported_response_type | response_type ≠ code | 302 | response_type=code 고정 |
invalid_scope | allowed_scopes 초과 또는 비어있음 | 302 | 앱 등록 시 allowed_scopes 확인 |
access_denied | 사용자가 "거부" 선택 | 302 | UX 설계에 따라 재시도 유도 |
/oauth/token 에서
| error | status | 의미 | 대응 |
|---|---|---|---|
invalid_client | 401 | client_secret 불일치 또는 누락 | Basic auth 헤더 / body 파라미터 재확인 |
unauthorized_client | 400 | 앱 승인 전 | 관리자 승인 대기 |
invalid_grant | 400 | code not found | code가 이미 교환됐거나 존재 없음 |
invalid_grant | 400 | code already used | 플로우당 1회 — 새로 authorize부터 |
invalid_grant | 400 | code expired | 10분 초과, 새로 authorize |
invalid_grant | 400 | redirect_uri mismatch | authorize 때와 완전히 동일한 URI |
invalid_grant | 400 | PKCE verifier mismatch | verifier 저장소 확인 (sessionStorage 휘발 주의) |
invalid_grant | 400 | refresh token not found | RT가 DB에 없음 |
invalid_grant | 400 | refresh token reuse detected; chain revoked | 재사용 공격 탐지됨. 사용자 재로그인 필요 |
invalid_grant | 400 | refresh token expired | 30일 경과 |
unsupported_grant_type | 400 | password/implicit/device 등 | authorization_code 또는 refresh_token만 |
/oauth/userinfo 에서
| error | status | 헤더 | 의미 |
|---|---|---|---|
invalid_token | 401 | WWW-Authenticate: Bearer error="invalid_token" | JWT 서명/만료/파싱 실패 |
invalid_token | 401 | — | 토큰 revoke됨 |
invalid_token | 401 | — | 사용자 soft-delete 상태 |
/oauth/revoke, /oauth/introspect 에서
| error | status | 의미 |
|---|---|---|
invalid_client | 401 | client_secret 불일치 또는 누락 |
/oauth/introspect 는 토큰이 유효하지 않아도 에러 대신 { "active": false } 를 반환합니다.
Rate Limit
| 엔드포인트 | 한도 | 키 | 초과 시 |
|---|---|---|---|
/session | 5/min (Cloudflare) · 10/3min (Rails) | IP | 429 rate_limited |
/oauth/token | 20/min | client_id | 429 rate_limited |
/api/v1/me/otp/* | 10/min | user_id | 429 rate_limited |
초과 시:
json
{ "error": "rate_limited" }디버깅 팁
invalid_grant가 계속 난다면? — 대부분 PKCE verifier 소실.sessionStorage/localStorage탭 전환·새로고침 동작 확인.invalid_request콜백으로 돌아온다면? — URL의error_description쿼리 파라미터가 정확한 원인 포함.- JWKS 404? — path 정확히
/.well-known/jwks.json(점·대시 주의). redirect_uri mismatch이상한데? — 등록된 URI는https://app.example.com/cb인데 authorize/token에https://app.example.com/cb/(trailing slash)라도 거부됩니다.
로깅 주의사항
절대 로그에 남기지 말 것:
password,client_secret,code_verifier,refresh_token,access_token(JWT 포함),logi_pak_*- logi 자체는
password_digest,otp_secret_encrypted, JWT, PAK plaintext를 한 번도 로그에 남기지 않습니다.