Skip to content

redirect_uri 소유 검증 — 내부 설계 레퍼런스

Internal. 공개 문서(oauth/redirect-uri-verification.md)의 상위 집합 — 엔드포인트 계약, Result#reason 어휘집, TTL/freshness 정책, domain-autopromote 플로우를 한곳에 모은 운영·개발자용 참조. Wave 5b F5 + domain-autoapprove Phase 3.

1. 개요

logi 는 등록된 OAuth redirect_uri호스트 소유권을 검증해 그 결과를 OauthApplication#redirect_uri_metadata(per-uri jsonb)에 stamp 합니다. 단일 진실은 Security::RedirectUriVerificationStatus.derive(tier:, entry:) — 콘솔 카드 / admin iOS 앱 / CLI 가 모두 이 status enum 을 봅니다.

  • 검증기: Security::RedirectUriVerifier (server/app/services/security/redirect_uri_verifier.rb)
  • stamp 래퍼: Security::RedirectUriVerificationService.verify_and_stamp! (.../redirect_uri_verification_service.rb)
  • status 유도: Security::RedirectUriVerificationStatus (.../redirect_uri_verification_status.rb)
  • 분류: Oauth::RedirectUriClassifier (tier 판정)

2. 검증 프로토콜

두 갈래 챌린지. DNS TXT 우선(저렴/캐시), 실패 시 well-known HTTPS fallback.

  1. DNS TXT_logi-verify.<host> TXT 레코드가 챌린지 값과 일치.
  2. well-knownhttps://<host>/.well-known/logi-verification.txt body 가 챌린지 값과 일치.

챌린지 유도: HMAC-SHA256(key, "{app.id}:{uri}") 의 hex. key = "<secret>:logi-redirect-verify". 결정적이라 DB 컬럼 추가 없이 안정.

키 링 회전 (Phase 2, T-7): verification_keys 는 newest-first 목록. 활성 키(첫 항목)로 챌린지를 생성(challenge_for)하고, 검증은 링의 모든 키를 수용(acceptable_challenges). LOGI_REDIRECT_VERIFY_KEYS(comma-sep) 미설정 시 [secret_key_base] — 기존 챌린지와 byte-identical. secret_key_base 회전 시 이전 키를 유예기간 동안 링에 남겨 전 RP 동시 재검증 실패(대량 revoke)를 방지. 활성 챌린지 값이 불변이므로 POLICY_VERSION 은 bump 하지 않는다(bump 시 저장된 autopromote 증거가 게이트에서 일괄 무효화됨).

SSRF 방어

  • pre-gate: localhost / .localhost / RFC1918·RFC4193 / IP 리터럴 / metadata IP 는 네트워크 호출 전에 차단(:unverifiable_host).
  • pinned-IP connect: WebhookUrlValidator.resolve_safe! 로 1회 resolve → 그 IP 로 직접 connect, SNI 는 원 호스트 제시, cert CN/SAN 을 원 호스트와 검증(post_connection_check) → DNS rebinding 봉쇄.
  • redirect 거부: 3xx 즉시 fail(:redirect_not_allowed).
  • body 캡: 256바이트 초과 즉시 abort(:body_too_large).
  • 단일 통합 deadline (Phase 2, T-3): connect + TLS + status line + 모든 header line + body 가 하나의 monotonic deadline(OVERALL_HTTP_DEADLINE = 6s)을 공유. per-read 타임아웃을 개별 리셋하던 slow-loris 헤더 플러드 창을 봉쇄.

3. 엔드포인트 계약

검증 트리거는 세션 + OTP step-up 이 필요한 민감 액션이라 콘솔/admin 에만 존재. 관찰은 PAK 로도 가능.

developer (콘솔, 세션 + OTP)

  • POST /developer/applications/:id/verify_redirect_uri — body { uri }. 게이트: 앱 가시성(scoped_applications) → authorize_app_management_json!(403) → enforce_recent_otp_for_sensitive_json(401 step_up_required). 비동기(Phase 2): Oauth::RedirectUriVerifyJob enqueue 후 202 { uri, status: "queued" }. rate limit: (app,uri) 60초 1회(429 rate_limited) + 개발자/일 50회(429 daily_limit_reached).
  • GET /developer/applications/:id/redirect_uri_verifications — per-uri { uri, tier, challenge_dns_record, challenge_wellknown_url, challenge_wellknown_body, verified_at, verification_method, expires_at, status, last_attempt }.

admin (/api/v1/admin, AdminJwt + step-up)

  • POST .../applications/:id/verify_redirect_uri — developer 와 동일 비동기 계약(202 queued). 60초 rate limit 유지, 일일 상한 미적용(특권·저볼륨, 전역 큐 concurrency 로 여전히 bounded).
  • GET .../applications/:id/redirect_uri_verifications — dev 와 byte-identical shape.

PAK (/api/v1/applications/:id, CLI 관찰용)

  • GET /api/v1/applications/:id show 페이로드에 redirect_uri_verifications 요약 포함: per-uri { uri, tier, status, verified_at, verification_method, expires_at, last_attempt }. challenge 값 없음 — 트리거는 콘솔 전용이므로 PAK 페이로드엔 증거 material 을 싣지 않는다. CLI(logi apps verify <id> [-r <uri>] [--poll])가 이 요약을 읽어 상태 표시 + queued→done 폴링.

비동기 결과 관찰 (Phase 2/3)

  • 성공은 verified_at(영속) 로 관측.
  • 대기/실패는 transient last_attempt(Oauth::RedirectUriVerifyJobRails.cacheRESULT_TTL=10m 로 기록):
    • { "state" => "queued" }
    • { "state" => "done", "verified" => bool, "method" => "dns"|"wellknown"|null, "reason" => <reason>|null, "detail" => str|null }
    • nil — TTL 창 내 시도 없음.
  • 전역 프로브 동시성: 전용 큐 redirect_uri_verification(queue.yml threads:2) + job limits_concurrency to: 2 이중 방어.

4. Result#reason 어휘집

Security::RedirectUriVerifier::Resultreason(검증 실패 시). method 는 성공 시 :dns/:wellknown.

reason의미
:unparseable_uriURI 파싱 실패
:unverifiable_hostblank host / localhost / IP 리터럴 / 사설망 — pre-gate 차단
:unverifiedDNS·well-known 둘 다 챌린지 불일치
:dns_no_record_logi-verify.<host> TXT 레코드 없음
:dns_timeoutDNS 조회 타임아웃
:dns_error기타 DNS 오류
:ssrf_blockedWebhookUrlValidator.resolve_safe! 차단
:not_foundwell-known 404 / 기타 4xx
:server_errorwell-known 5xx
:redirect_not_allowedwell-known 3xx
:tls_invalidTLS 인증서/핸드셰이크 검증 실패
:timeout통합 deadline(6s) 초과
:body_too_largewell-known body 256B 초과
:http_error기타 HTTP/소켓 오류

status enum (RedirectUriVerificationStatus.derive, 콘솔/CLI 표시용): verified · expired · unverifiable_host · unverified.

5. TTL / freshness 정책

  • verified URI 는 무기한: verify_and_stamp! 성공 시 verified_at stamp + expires_at clear. 만료 리마인더 job 은 verified_at 있는 엔트리를 skip.
  • 미검증 tier TTL: ephemeral/production_unverified/staging_unverifiedexpires_at 을 갖고, Oauth::RedirectUriExpiryReminderJob(15분)이 T-7d/T-3d/T-24h/T-1h 리마인더 + 만료 시 expired_at soft-mark.
  • autopromote freshness (Phase 1/2): 프로덕션 자동승격에 쓰인 DNS 증거는 PRODUCTION_VERIFICATION_FRESHNESS_WINDOW = 35일 을 넘으면 게이트가 fail-closed 거부. Oauth::RedirectUriReverificationJob(월간)이 DNS 를 재확인해 통과 시 verified_at 갱신, 실패 시 verified_at clear(per-URI) + 앱 revoke. 35일 = 월간(≈30일) 주기 + 5일 슬랙(결합 상수 — 주기 변경 시 함께 조정).

6. domain-autopromote 플로우

RP 가 자기 redirect_uri 의 도메인 소유를 DNS TXT 로 증명하면, 그 URI 에 한해 live authorize 를 허용(Option A per-URI 바이패스)하고 앱 status: pending → approved 를 원자적으로 부여. 플래그 LOGI_DOMAIN_AUTOPROMOTE(기본 false)로 게이트. LOGI_BETA_AUTO_APPROVE(무검증 blanket)와는 완전 별개.

  • 게이트 (OauthApplication#redirect_uri_verified_for_production?): entry 존재 → requires_risk_acknowledgement? false(T-9) → 런타임 재분류가 production_unverified/staging_unverified(T-6/T-8) → https canonical(fragment/userinfo 금지, T-1) → 저장된 classifier_version/verification_policy_version 가 현재 상수와 일치(T-6) → verification_method == "dns"(well-known 배제, T-10) → verified_at freshness ≤ 35d(T-4) → config 미드리프트(T-2). 전부 통과해야 true.
  • 원자적 전이 (autopromote_status_via_domain_verification!): with_lock + reload, status == "pending" 에서만(suspended/approved 부활 금지, T-5), 락 안 게이트 재평가(TOCTOU). approve_by! 미재사용.
  • config drift revoke (T-2): 승격 시점 config digest 동결. scope/name/logo/app_link/redirect_uris 변경 시 런타임 fingerprint 불일치 → 게이트 즉시 false + after_commit(및 scope-only 변경은 set_scopes!)이 revoke 기록 + 개발자 메일.
  • fail-closed 재검증 (T-4): §5 의 월간 job. DNS 재확인만 인정, 실패 시 per-URI verified_at clear + 앱 revoke. 프로브→락 TOCTOU 는 프로브 시점 verified_at(µs 정밀도 생성 토큰) 캡처 후 락 안에서 불변일 때만 실패 적용.

상세 체크리스트/위협모델: .planning/domain-autoapprove-fast-follow.md.

7. 관련 소스

  • server/app/services/security/redirect_uri_verifier.rb — 검증기(DNS/well-known/SSRF/키 링/단일 deadline)
  • server/app/services/security/redirect_uri_verification_service.rb — stamp 래퍼
  • server/app/services/security/redirect_uri_verification_status.rb — status enum
  • server/app/jobs/oauth/redirect_uri_verify_job.rb — 비동기 verify + last_attempt
  • server/app/jobs/oauth/redirect_uri_reverification_job.rb — 월간 fail-closed 재검증
  • server/app/models/oauth_application.rb — autopromote 게이트/전이/revoke
  • server/app/controllers/{developer,api/v1/admin,api/v1}/applications_controller.rb — 엔드포인트
  • cli/lib/logi/commands/app.rblogi apps verify(관찰/폴링)

최종 수정:

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