테마
Troubleshooting
자주 겪는 에러를 증상 → 원인 → 처방 순서로 정리했습니다. 위쪽일수록 빈도가 높습니다.
위젯 (Widget SDK)
QR 코드의 finder pattern이 잘려서 안 스캔됨
증상: 위젯이 정상 마운트됐는데 iframe 안의 QR 코드 우측 finder pattern (top-right) 또는 하단 alignment pattern이 잘려 보임. 카메라가 인식 못 하거나 인식해도 잘못된 페이로드를 읽음.
원인: RQRCode 라이브러리가 SVG 를 native pixel size 로 emit (module_size × modules + offset). 페이로드가 길수록 (URL + session UUID 등) 모듈 수가 늘어 SVG가 200px 컨테이너보다 커짐. 컨테이너에 overflow:hidden 이 없으면 SVG가 가시 영역 밖으로 삐져나가고, 있으면 잘림.
처방 (이미 적용됨, 회귀 방지 메모): embed_qr_controller.js#renderQR 가 SVG element 에 width="100%" height="100%" preserveAspectRatio="xMidYMid meet" 를 강제 합니다. 새로 SVG-based QR 렌더 코드 추가 시 동일 패턴 사용:
js
// ✅ 컨테이너 크기에 맞게 viewBox 비율 유지하며 scale
svgEl.setAttribute("width", "100%")
svgEl.setAttribute("height", "100%")
svgEl.setAttribute("preserveAspectRatio", "xMidYMid meet")
// ❌ 안 됨 — RQRCode native size 그대로 사용 → 컨테이너 overflow
container.replaceChildren(svgEl)왜 native size 가 위험한가: payload 길이가 변하면 (예: nonce 회전, scope 추가) QR 모듈 수가 늘어남 → SVG 폭 증가 → 어느 시점에서 200px 초과 → finder 가 잘림. Defense-in-depth: SVG 는 항상 컨테이너 비율로 fit, 컨테이너 자체 크기로 QR 크기 제어.
Widget shows "unknown client_id" inside iframe
증상: iframe 안에 logi QR 페이지가 뜨긴 했는데 내부에 "unknown client_id / 잠시만요… / 닫기" 텍스트만 떠 있고 QR 자체는 안 그려짐. Render 로그에 POST /oauth/qr/start ... Parameters: {"qr_login" => {}} + Completed 400 Bad Request.
원인: 아이프레임 페이지가 두 Stimulus 컨트롤러 (embed-qr + qr-login) 를 동시에 attach 하고 둘 다 connect() 에서 POST /oauth/qr/start 를 자동 실행하는 race. 뷰가 한쪽 namespace 의 data-*-value 만 채우면 다른쪽 컨트롤러는 빈 body 로 POST → 400.
처방 (서버 측): app/views/embed/qr/show.html.erb 의 data-controller 속성에 embed-qr 만 attach 하세요. embed-qr 컨트롤러 하나만 lifecycle (start → 폴링 → approved → ?embed=1 완료 → postMessage) 을 책임지고, qr-login 의 DOM target attributes (data-qr-login-target=...) 는 plain selector 용으로 남깁니다.
erb
<%# ❌ 잘못 — qr-login 도 함께 자동 시작해서 race + 400 %>
<section data-controller="embed-qr qr-login" ...>
<%# ✅ 올바른 — embed-qr 단독, qr-login namespace 의 value 들도 모두
embed-qr namespace 로 옮길 것 %>
<section data-controller="embed-qr"
data-embed-qr-oauth-params-value='<%= ... %>'
...>Regression guard: app/views/embed/qr/show.html.erb 컨트롤러 선언 위에 인라인 주석으로 "왜 단독 attach 인지" 명시. PR 리뷰 시 두 컨트롤러 동시 attach 부활하면 reject.
위젯이 마운트만 되고 빈 iframe 으로 보임
증상: <div data-logi-qr> 자리에 회색 박스만 뜨고 QR 이 안 보임. 콘솔에 CSP 또는 X-Frame-Options 에러가 보임.
원인 후보:
- RP 의 origin 이
OauthApplication.widget_origins에 등록되어 있지 않음 widget_enabled = false- RP 페이지의 CSP
frame-ancestors가embed.1pass.dev차단
처방:
ruby
# Rails console 또는 [start.1pass.dev/developer](https://start.1pass.dev/developer) 콘솔에서
app = OauthApplication.find_by!(name: "your-rp")
app.update!(
widget_enabled: true,
widget_origins: [
"https://your-app.com", # production
"http://localhost:3000" # dev — scheme + port 정확히
]
)CSP 점검:
# 권장 (RP 페이지 헤더)
Content-Security-Policy: frame-src https://embed.1pass.dev; child-src https://embed.1pass.dev;위젯이 mountWidget 호출 후에도 동작 안 함
증상: <script src="…/widget.js"> 로딩 후 동적으로 <div> 추가했는데 위젯이 마운트 안 됨.
원인: widget.js 의 자동 init 은 DOMContentLoaded 한 번만 실행됩니다. 그 이후 추가된 mount 노드는 자동 발견되지 않습니다.
처방: 직접 호출
js
const mount = document.getElementById("logi-mount");
// data-* 속성 모두 세팅한 후
if (window.LogiWidget) window.LogiWidget.mountWidget(mount);또는 React/Vue/Svelte 라면 Widget SDK 프레임워크 예시 참고.
postMessage 가 부모로 도달 안 함
증상: 사용자가 QR 스캔 + 앱 승인을 끝냈는데 data-on-success 가 호출 안 됨.
원인: 위젯이 event.origin 을 검증하는데 부모 페이지의 origin 과 mismatch. 또는 data-on-success="…" 가 가리키는 함수가 window 에 없음.
처방:
- 콜백 함수가 window 에 등록되어 있는지 확인 —
window.handleLogin = function() {…} - 부모도
event.origin === "https://embed.1pass.dev"검증을 거쳐야 — 그 외 origin 의 메시지는 무시 (보안)
표준 OAuth Flow
401 invalid_client
증상: /oauth/token POST 가 401 + {"error": "invalid_client"}.
원인 후보:
client_secret이 환경변수에 안 잡힘 (Render: 빌드 시 vs 런타임 분리)client_id/client_secret가 production / staging 다른 환경의 값client_secret가 회전 (rotate) 되어서 옛 값 사용 중- (모바일 RP) 빌드 스크립트에 client_id 주입을 깜빡함 → 앱 바이너리에 placeholder 문자열이 박힘. Flutter
String.fromEnvironmentdefaultValue 함정이 대표적. → Flutter 통합 가이드 참조
처방:
bash
# RP 백엔드에서
echo $LOGI_CLIENT_ID # logi_xxxxxxxxxxxx 형식
echo $LOGI_CLIENT_SECRET | head -c 8 # 첫 8자만 노출
# 1pass 콘솔의 RP 설정 페이지에서 client_id 가 같은지 비교
# 일치 안 하면 회전 의심 — Console → 앱 상세 → "Reveal current secret"Render 환경변수 함정
Build Command 에서 $LOGI_CLIENT_SECRET 을 참조하면 빈 문자열이 됩니다 — Render 의 secret 환경변수는 런타임에만 주입됩니다. bundle exec rails s 같은 Start Command 에서만 사용 가능.
302 redirect_uri_mismatch
증상: /oauth/authorize 가 에러 페이지로 302.
원인: redirect_uri 파라미터가 RP 등록된 redirect_uris 와 글자 단위로 일치하지 않음. 흔한 함정:
- trailing slash (
/cbvs/cb/) httpvshttps- subdomain (
app.example.comvswww.app.example.com) - query string 포함 여부 (
/cb?env=prodvs/cb) — query 는 등록된 URI 에 포함되면 안 됨
처방: 콘솔에 등록된 URI 를 그대로 복사해서 사용.
invalid_request: redirect_uri not registered
증상: /oauth/authorize 가 곧장 JSON 에러:
json
{ "error": "invalid_request", "error_description": "redirect_uri not registered" }원인: redirect_uri_mismatch 와 비슷해 보이지만 다릅니다 — mismatch 는 “비슷한 후보가 있는데 글자가 다름”, not registered 는 화이트리스트에 후보 자체가 없음. 가장 흔한 경로:
- 모바일 RP (예:
redirect_uris=["app://oauth/1pass/callback"]) 가 웹 surface 도 같은client_id로 노출 시작 → 웹 콜백 (https://app.example.dev/auth/1pass/callback) 이 화이트리스트에 없음 - staging / preview 도메인 신설 후 RP 갱신 누락
- branch preview URL (예: Vercel
*-git-feature-x.vercel.app) 사용 시도 → 정적 화이트리스트 위반 - RP 측 콜백 path rename (예:
/auth/1pass/callback→/auth/logi/callback) 을 SP 코드에만 반영하고 IdP 화이트리스트 동기 갱신 누락 — 가장 흔한 회귀 패턴. 배포 직후 첫 사용자 클릭에서 발견됨
처방:
bash
# 1. 현재 RP 의 redirect_uris 확인
logi app show $CLIENT_ID
# 2. 누락된 URI 추가 (기존 항목 유지, append)
logi app update $CLIENT_ID --add-redirect-uri "https://app.example.dev/auth/1pass/callback"
# 3. 추가 즉시 검증
logi apps verify $CLIENT_ID -r "https://app.example.dev/auth/1pass/callback"SSH 우회 (CLI 미설치 환경, logi-server 직접 접근권 있을 때):
bash
ssh srv-d7mro4egvqtc73ag82jg@ssh.singapore.render.com \
'cd server && bundle exec rails runner "app = OauthApplication.find_by(client_id: %q[logi_xxxxxxxxxxxxxxxxxxxx]); nu = %q[https://app.example.dev/auth/1pass/callback]; app.update!(redirect_uris: (app.redirect_uris + [nu]).uniq) unless app.redirect_uris.include?(nu); puts app.reload.redirect_uris.inspect"'보안
public + PKCE RP 라면 모바일/웹 같은 client_id 공유 안전. confidential RP (client_secret 사용) 라면 surface 분리가 더 안전 — 별도 client_id 발급 권장.
회귀 방지 (콜백 path rename)
SP 측에서 callback URL 을 바꾸는 PR (예: 브랜드 통합으로 /auth/old/callback → /auth/new/callback) 은 반드시 동일 PR 에서 IdP 화이트리스트 갱신을 함께 처리하세요. 권장 절차:
- 이전 path 유지 + 신규 path 추가 로 화이트리스트 먼저 확장 (배포 전)
- SP 배포 → 실트래픽 검증
- 일정 기간 뒤 (in-flight 세션 만료 후, 일반적으로 7일+) 이전 path 제거
배포 전 즉시 검증:
bash
# 신규 redirect_uri 가 화이트리스트에 있는지 확인 (302 응답 = OK, JSON error = 누락)
curl -sI "https://api.1pass.dev/oauth/authorize?client_id=$CLIENT_ID&redirect_uri=$NEW_URI_ENCODED&response_type=code&scope=openid&state=preflight&code_challenge=x&code_challenge_method=S256" | head -1CI 에 위 호출을 넣어두면 누락이 production 트래픽에 닿기 전에 잡힙니다.
state mismatch / 세션 분실
증상: callback 도달했는데 state 검증 실패.
원인 후보:
- 사용자가 로그인 시작 후 다른 탭에서 다시 시작 (state 덮어씀)
- 세션 cookie 가 Lax/Strict 로 첫 redirect 후 미전송 (cross-site)
- RP 가 무상태 (load balancer) 인데 sticky session 없음
처방:
state → verifier매핑을 dict 로 보관 (단일 값 X) — recommended-architecture.md 의session[:onepass_pkce]패턴- 세션 store 는 cross-instance 공유 가능한 곳 (Redis, DB) —
cookie_store면 OK - iframe 통합 시
SameSite=None; Secure필요할 수 있음
code 가 만료됨
증상: token 교환 시 invalid_grant.
원인: 1pass 가 발급한 code 는 10분 후 만료 (RFC 6749 §4.1.2 best practice — OauthAccessGrant::EXPIRY = 10.minutes). 또는 한 번 사용된 code 재사용.
처방:
- callback 받자마자 즉시 교환 (사용자 입력 받지 말 것)
- 재시도 시
state부터 다시 발급
403 anonymous_not_allowed
증상: 모바일 앱 사용자가 동의 화면에서 승인했는데 logi 가 403 + {"error": "anonymous_not_allowed"} 를 돌려주고, 앱은 "이메일 가입을 완료한 계정에서만…" 같은 메시지를 띄움.
원인: 사용자가 logi 익명 계정 (Apple/Google 연결 안 함, 이메일 등록 안 함) 인 채로 allow_anonymous_grants=false RP 에 consent 시도. logi 의 문제가 아니라 해당 RP 의 정책 — 외부 RP 는 기본적으로 식별된 subject 만 받습니다.
RP 개발자의 선택지:
- 익명도 받기 — 자사 서비스가 guest 모드에 적합하면 RP 설정에서
allow_anonymous_grants=true토글. (설정 방법) - 사용자에게 안내 — 403 응답의
application_name+remediation필드를 활용해 "이 앱은 식별된 계정이 필요해요. 계정 설정에서 Apple/Google 연결 또는 이메일 등록을 마쳐주세요" CTA 제공. 응답 shape: Anonymous Grants — 차단 응답
사용자의 처방: logi 앱 → 설정 → 계정 → Apple / Google 연결, 또는 이메일 등록. promotion 후 다시 RP 로 돌아가 재시도.
더 매끄러운 경험 (권장): 403 응답에는 promotion 객체가 함께 옵니다. 이 객체를 쓰면 OAuth dance 밖으로 사용자를 던질 필요 없이 인라인 promotion sheet 로 Apple/Google/email 등록을 받고, resume_token 으로 같은 흐름을 이어갈 수 있어요. 자세한 흐름과 응답 shape: JIT Promotion.
422 resume_token_expired / resume_token_already_used / promotion_incomplete
증상: JIT promotion 으로 등록 끝내고 POST /api/v1/oauth/authorize/resume 호출했더니 422.
| Error | 의미 | 처방 |
|---|---|---|
resume_token_expired | 5분 TTL 초과 | 처음부터 다시 (/oauth/authorize 재시도) |
resume_token_already_used | 같은 토큰을 두 번 redeem | 새 토큰 발급받기 (재시도) |
promotion_incomplete | 토큰은 유효하지만 user 가 여전히 anonymous | 등록 단계가 끝나지 않음. /api/v1/me/connected_identities 또는 /api/v1/me/emails 응답을 다시 확인 |
resume_user_mismatch (403) | 토큰 발급 후 PAK 가 다른 user 로 갈림 | 처음부터 다시 |
Device Flow (RFC 8628)
slow_down 에러 반복
증상: /oauth/device/poll 응답이 계속 slow_down.
원인: poll interval 이 너무 빠름 (RFC 권장: 5초).
처방: 응답 헤더의 Retry-After 또는 body 의 interval 값을 따를 것. 매 slow_down 마다 interval 을 +5초 늘리는 게 안전.
user_code 가 너무 빨리 만료
증상: 사용자가 QR 스캔 전에 만료.
원인: 1pass 의 device code TTL 은 10분.
처방: 자동 새로고침 — 10분 가까워지면 새 device code 발급. logi 앱 측 user_code 입력 화면도 동일.
id_token 검증
iss mismatch
증상: id_token 의 iss 가 https://1pass.dev 인데 RP 코드가 https://api.1pass.dev 로 비교 → 실패.
원인: production issuer 는 https://api.1pass.dev 입니다. (start.1pass.dev 는 콘솔, 1pass.dev 는 docs.)
처방: .well-known/openid-configuration 의 issuer 필드 그대로 사용.
js
// 동적으로 가져오는 패턴
const config = await fetch("https://api.1pass.dev/.well-known/openid-configuration").then(r => r.json());
expect(idToken.iss).toEqual(config.issuer);JWKS 키 회전 후 검증 실패
증상: 어제까진 됐는데 갑자기 id_token 서명 검증 실패.
원인: JWKS 키가 회전되었고 RP 가 옛 키를 캐싱.
처방: JWKS fetch 캐시는 1시간 이하, 그리고 kid 가 캐시에 없으면 즉시 refresh. 라이브러리 (jose, jwt) 가 보통 처리해줍니다.
일반 디버깅 도구
1. Discovery 문서 확인
bash
curl -s https://api.1pass.dev/.well-known/openid-configuration | jq .issuer, authorization_endpoint, token_endpoint, userinfo_endpoint, jwks_uri, embed_endpoint 가 모두 정상 200 인지.
2. JWKS 확인
bash
curl -s https://api.1pass.dev/.well-known/jwks.json | jq '.keys[].kid'3. id_token 디코드 (서명 검증 X — 디버깅 용도만)
bash
echo "$ID_TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | jq .4. Authorization 시작 URL 손수 빌드
bash
echo "https://api.1pass.dev/oauth/authorize?$(cat <<EOF | tr -d ' \n'
response_type=code&
client_id=logi_xxx&
redirect_uri=https%3A%2F%2Fexample.com%2Fcb&
scope=openid+profile%3Abasic+email&
state=test123&
code_challenge=$CHALLENGE&
code_challenge_method=S256
EOF
)"5. 위젯 demo 페이지
production walking-sample: https://api.1pass.dev/widget-demo.html. 실제 1pass-demo RP 로 마운트되므로 위젯이 정상 동작하는지 한 번 확인할 수 있는 reference.
그래도 안 되면
- 개발자 콘솔 → 앱 상세 → "최근 에러 로그"
- 디버깅에 도움 되는 정보:
client_id(앞 12자), 시각,error+error_description, 사용 중인 SDK 버전