테마
RP 통합 테스트 가이드
logi 를 처음 붙이는 RP 가 production 으로 가기 전, OAuth 흐름과 Webhook · merge 까지 모두 동작하는지 확인하기 위한 운영 가이드. 마케팅 문구가 아니라 "출시 전에 이걸 안 보면 새벽에 깬다" 수준의 체크리스트.
테스트 환경 옵션
logi 는 현재 별도 sandbox host 를 운영하지 않습니다. 모든 통합 검증은 production endpoint(https://api.1pass.dev) 의 테스트 전용 RP 등록으로 수행합니다.
- 검증은 production IdP 에 별도의 테스트 OauthApplication 을 등록해 진행 — production 트래픽과 격리됨
- 테스트 RP 에는
localhost,*.ngrok-free.app, 사내 staging 도메인 등 비공개 URL 을redirect_uris로 등록 가능 (HTTPS 또는http://localhost만 허용) - 결제 · 계정 삭제 · 의심 로그인 lockout 등 비가역 동작은 테스트 RP 전용 계정으로 재현
Self-hosted 개발 모드 미제공
1pass 는 docker-compose up 으로 띄울 수 있는 로컬 IdP 를 제공하지 않습니다. RP 단독 통합 작업 시에도 항상 production IdP 의 테스트 RP 를 사용하세요.
테스트용 자격증명 발급
- CLI 또는 개발자 포털에서 테스트 RP 등록 —
name에[test]prefix 권장 (예:[test] ainote staging) redirect_uris에 staging/localhost URL 등록 — redirect_uri 는 완전 일치 이므로 trailing slash · 대소문자까지 정확히webhook_url은 ngrok / Cloudflare Tunnel 로 외부 노출된 staging endpoint 사용 — 로컬 머신을 직접 등록할 수는 없음- 발급되는 값:
client_id(logi_xxx...)client_secret(confidential client 만 — 1회 노출, 즉시 staging secret manager 에 저장)- 초기 webhook signing key (
kid+ plaintextsecret) — 마찬가지로 1회 노출
테스트 RP 와 production RP 는 반드시 다른 client_id / webhook secret 으로 분리하세요. 공유하면 staging 에서 회전한 키가 production 까지 영향을 줍니다.
흐름별 검증 체크리스트
OAuth Authorization Code + PKCE
production 직전 최소한 다음 케이스를 직접 수행:
- [ ] 첫 로그인 —
state·code_challenge생성 → authorize → callback →/oauth/token교환 →access_token으로/oauth/userinfo200 응답 - [ ] 재로그인 (동일 사용자) — 동일
sub가 반환되는지.aud가 본인client_id와 정확히 일치하는지 - [ ] scope 추가 요청 — 기존 동의보다 넓은 scope 요청 시 동의 화면이 다시 표시되는지
- [ ] 사용자 거부 — authorize 화면에서 거부 → callback 의
error=access_denied처리 → 사용자에게 친화적 메시지 - [ ] 만료된 access token —
exp지난 토큰으로 API 호출 시 401, refresh 후 정상화 - [ ] 잘못된
aud— 다른 RP 의 토큰을 본인 백엔드에 보내면 반드시 401 반환 (검증 누락 회귀 방지) - [ ] JWKS 캐시 강제 갱신 —
kid미스매치 시 1회 강제 refetch 후 재검증되는지 (JWKS 캐시 정책)
scripts/verify-rp.sh (repo 루트) 는 위 중 /login 노출 · authorize URL 파라미터 · code_challenge_method=S256 · AASA 매칭을 비파괴로 한 번에 확인합니다. PR 전 최소 1회 돌릴 것.
Webhook 수신
- [ ] Signature 검증 — 두 형식 모두 — PLAN-L (
t=<ts>,kid=<kid>,v1=<hex>) 과 legacy (sha256=<hex>+X-Logi-Timestamp헤더) 둘 다 검증 통과. 두 형식이 공존하는 이유와 verifier 예시는 Webhook 서명 검증 참고 - [ ] Replay 거부 — PLAN-L 은
t=값, legacy 는X-Logi-Timestamp가 현재 시각과 ±5분을 벗어나면 거부 - [ ] Idempotency —
X-Logi-Event-Id(event_id) 기준 dedup. 동일 ID 로 두 번 들어오면 두 번째는 200 만 응답하고 부수효과 무시 - [ ] 순서 보장 없음 —
user.merged가user.created보다 먼저 도착해도 처리되는지 (logi outbox 는 최선순서, 엄격한 ordering 보장 안 함) - [ ] Grace window 동안 두 키 공존 — 키 회전 직후 신 · 구 두
kid모두 검증 통과해야 함 - [ ] Webhook timeout — 10초 안에 200 응답 못 하면 logi 가 retry — 멱등성 없으면 중복 처리됨
Account Merge
자세한 시나리오는 Account Merge 개요 와 Merge Idempotency 참조.
- [ ] T2 (cross-provider, 동일 이메일) — Apple SSO 로 가입 → 동일 이메일로 Google SSO 로 재로그인 → 자동 merge 발생 후
user.mergedwebhook 수신 - [ ] T3 (OTP 기반) — 이메일 OTP 로 두 anonymous 계정을 같은 사람으로 묶기
- [ ] 12.3 session_token 기반 merge — RP 측 session 으로 활성 RP 측 user 와 logi survivor 의 매핑
- [ ] T1 (device-link) 은 실기기 페어가 필요해 자동화하기 어려움 → 수동 검증만 권장
- [ ] 모든 merge 후 RP 측에서
survivor_canonical_sub와merged_sub기준으로 canonical_user_ids 해상도가 올바른지
자주 만나는 함정
| 증상 | 원인 | 해결 |
|---|---|---|
invalid_grant (token 교환) | redirect_uri 가 등록값과 1글자 다름 (trailing /, scheme, 대소문자) | 등록 URI 와 코드의 URI 를 hex dump 로 비교 |
invalid_grant (code_verifier_mismatch) | challenge ↔ verifier session 분실. cookie SameSite=Strict + cross-site redirect 충돌 흔함 | challenge 를 server-side session 또는 signed state 에 보관 |
JWT exp 검증 실패 (정상 발급 직후) | 서버 시계 skew >60s — 특히 Docker host clock drift | chrony / NTP 검증, JWT 라이브러리의 clockTolerance 옵션 사용 |
| Webhook 검증 실패 (서명 불일치) | request body 를 파싱 후 재직렬화 한 값으로 HMAC 계산 | 반드시 raw body 사용 (상세) |
Webhook Content-Type 충돌 | Rails 의 parameter_wrapping / Express 의 body-parser 가 body 를 소비 | webhook endpoint 전용 라우트에서 raw body middleware 등록 |
aud mismatch | 다중 client_id 환경에서 토큰을 잘못된 백엔드로 라우팅 | aud == process.env.LOGI_CLIENT_ID 단정 |
| Universal Link 미동작 (iOS) | AASA 파일이 /oauth/authorize paths 누락 | curl https://api.1pass.dev/.well-known/apple-app-site-association 확인 |
CI 통합
logi 통합 테스트는 두 갈래로 나눠 운영하는 것이 안전합니다.
1) Smoke test (실제 IdP 호출) — PR 마다 또는 deploy 후
yaml
# .github/workflows/logi-verify.yml
name: logi RP verify
on: [pull_request, deployment_status]
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run RP verification
run: ./scripts/verify-rp.sh https://staging.example.com비파괴 — /login 페이지 GET 과 AASA fetch 만 수행. authorize URL 의 client_id · code_challenge_method=S256 박혀 있는지 확인.
2) 단위 테스트 (mock IdP) — 매 commit
- JWT 검증 로직: 테스트용 RSA 키 쌍으로 발급한 토큰을
nock/WebMock으로 JWKS 응답 stub 한 뒤 검증 - Webhook 검증: 알려진
(secret, body, ts)조합에 대해 expected signature 가 일치하는지 fixture test - Refresh / revoke:
/oauth/token,/oauth/revoke응답을 stub
실 IdP 를 매 commit 마다 때리면 rate limit 에 걸립니다 — secret rotation 같은 비가역 동작은 절대 CI 에서 자동화하지 마세요.
logi 내부 — RP 통합 spec 패턴 (logi 본 코드베이스 기여자용)
logi 서버 자체에 RP 통합 회귀 spec 을 추가하는 경우:
1. 디렉토리
spec/integrations/ 에 두면 RSpec 의 type 자동 매칭이 안 됨. 명시 필요:
ruby
RSpec.describe "krx_listing RP integration", type: :request do
# ...
end2. Oauth::KeyStore stub (RAILS_MASTER_KEY 없이도 실행)
JWT 발급은 credentials 의 oauth_jwt.keys 가 필요. CI / 로컬에서 master key 없이도 결정적으로 통과시키려면 RSA 키페어 생성 후 stub:
ruby
before do
rsa = OpenSSL::PKey::RSA.generate(2048)
allow(Oauth::KeyStore).to receive(:active_kid).and_return("test-kid")
allow(Oauth::KeyStore).to receive(:private_key).and_return(rsa)
allow(Oauth::KeyStore).to receive(:public_keys)
.and_return([ { kid: "test-kid", key: rsa.public_key } ])
endJWKS 노출 정확성 자체는 spec/requests/oauth/jwks_spec.rb 가 책임 — 통합 spec 에선 토큰 발급 흐름만 검증.
3. PKCE 테스트 벡터
RFC 7636 §B.1.1 표준 벡터 사용 (다른 oauth spec 들과 일치):
ruby
let(:verifier) { "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" }
let(:challenge) { "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM" }4. app 이름 충돌 회피
let(:app) 은 Rack app 헬퍼와 충돌해 NoMethodError: undefined method 'call' for OauthApplication 으로 죽음. let(:rp_app) 같은 다른 이름 사용.
5. 모범 spec
spec/integrations/krx_listing_rp_integration_spec.rb— Web↔Web, App↔Web, pairwise-sub 격리, post-login fallback non-hijack 회귀 가드spec/requests/oauth/refresh_token_rotation_spec.rb— refresh rotation + reuse detectionspec/requests/oauth/redirect_uri_strictness_spec.rb— exact-match- prefix-trap / fragment / userinfo-injection 거부