Skip to content

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.erbdata-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 에러가 보임.

원인 후보:

  1. RP 의 origin 이 OauthApplication.widget_origins 에 등록되어 있지 않음
  2. widget_enabled = false
  3. RP 페이지의 CSP frame-ancestorsembed.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"}.

원인 후보:

  1. client_secret 이 환경변수에 안 잡힘 (Render: 빌드 시 vs 런타임 분리)
  2. client_id / client_secret 가 production / staging 다른 환경의 값
  3. client_secret 가 회전 (rotate) 되어서 옛 값 사용 중
  4. (모바일 RP) 빌드 스크립트에 client_id 주입을 깜빡함 → 앱 바이너리에 placeholder 문자열이 박힘. Flutter String.fromEnvironment defaultValue 함정이 대표적. → 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 (/cb vs /cb/)
  • http vs https
  • subdomain (app.example.com vs www.app.example.com)
  • query string 포함 여부 (/cb?env=prod vs /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 화이트리스트 갱신을 함께 처리하세요. 권장 절차:

  1. 이전 path 유지 + 신규 path 추가 로 화이트리스트 먼저 확장 (배포 전)
  2. SP 배포 → 실트래픽 검증
  3. 일정 기간 뒤 (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 -1

CI 에 위 호출을 넣어두면 누락이 production 트래픽에 닿기 전에 잡힙니다.

state mismatch / 세션 분실

증상: callback 도달했는데 state 검증 실패.

원인 후보:

  1. 사용자가 로그인 시작 후 다른 탭에서 다시 시작 (state 덮어씀)
  2. 세션 cookie 가 Lax/Strict 로 첫 redirect 후 미전송 (cross-site)
  3. RP 가 무상태 (load balancer) 인데 sticky session 없음

처방:

  • state → verifier 매핑을 dict 로 보관 (단일 값 X) — recommended-architecture.mdsession[:onepass_pkce] 패턴
  • 세션 store 는 cross-instance 공유 가능한 곳 (Redis, DB) — cookie_store 면 OK
  • iframe 통합 시 SameSite=None; Secure 필요할 수 있음

code 가 만료됨

증상: token 교환 시 invalid_grant.

원인: 1pass 가 발급한 code10분 후 만료 (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 개발자의 선택지:

  1. 익명도 받기 — 자사 서비스가 guest 모드에 적합하면 RP 설정에서 allow_anonymous_grants=true 토글. (설정 방법)
  2. 사용자에게 안내 — 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_expired5분 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 의 isshttps://1pass.dev 인데 RP 코드가 https://api.1pass.dev 로 비교 → 실패.

원인: production issuer 는 https://api.1pass.dev 입니다. (start.1pass.dev 는 콘솔, 1pass.dev 는 docs.)

처방: .well-known/openid-configurationissuer 필드 그대로 사용.

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 버전

MIT License · Identity가 제품의 신뢰를 만듭니다.