Skip to content

redirect_uri 자동 검증 (DNS / well-known)

logi 는 등록된 OAuth redirect_uri 의 호스트 소유권을 DNS TXT 또는 .well-known HTTPS 챌린지로 검증합니다. 검증 결과는 OauthApplication#redirect_uri_metadata 에 stamp 되고, RP 콘솔의 카드 / admin iOS 앱 / logi apps verify CLI 가 같은 endpoint 를 호출해 같은 status enum 을 본 (Security::RedirectUriVerificationStatus 가 단일 진실).

핵심

  • 두 갈래 챌린지: DNS TXT 우선 (저렴 / 캐시 가능) → 실패 시 well-known HTTPS fallback.
  • HMAC 챌린지 값: HMAC-SHA256(secret_key_base + ":logi-redirect-verify", "{app.id}:{uri}") 의 hex. 결정적이라 DB 컬럼 추가 없이도 안정.
  • SSRF 방어: localhost / RFC1918 / IP 리터럴 / metadata IP 는 네트워크 호출 전에 pre-gate 차단.
  • rate limit: (app, uri) 당 60초 1회. Rails.cache.write(..., unless_exist: true) atomic.
  • manual CLI 와 짝: logi apps verify <app_id> <uri> 가 이 endpoint 의 thin wrapper. 사용자가 직접 호출할 일은 거의 없음.

검증 흐름

mermaid
sequenceDiagram
  autonumber
  participant Console as RP 콘솔 / CLI / Admin iOS
  participant API as logi 서버
  participant DNS as DNS resolver
  participant RP as RP 호스트 (HTTPS)

  Console->>API: GET .../redirect_uri_verifications
  API-->>Console: { verifications: [ {uri, challenge_dns_record, challenge_wellknown_*, status} ] }
  Note over Console: RP 운영자가 DNS TXT 또는<br/>.well-known 파일 배포
  Console->>API: POST .../verify_redirect_uri { uri }
  API->>API: rate-limit check (60s per (app, uri))
  API->>API: pre-gate (localhost / IP literal / RFC1918 차단)
  API->>DNS: _logi-verify.<host> TXT?
  alt DNS TXT match
    DNS-->>API: TXT == challenge
    API-->>Console: { verified: true, method: "dns" }
  else DNS 실패
    API->>RP: GET https://<host>/.well-known/logi-verification.txt
    Note over API,RP: pinned-IP TLS + SNI 검증<br/>리디렉트 거부 · body 256B 캡
    alt body match
      RP-->>API: 200 + challenge
      API-->>Console: { verified: true, method: "wellknown" }
    else
      RP-->>API: 404 / mismatch / timeout
      API-->>Console: { verified: false, reason: "unverified", detail: "dns=... wellknown=..." }
    end
  end

Endpoints

Developer 콘솔 (RP 소유자용)

MethodPath인증
GET/developer/applications/:id/redirect_uri_verifications세션 쿠키 (RP 소유자 / org member)
POST/developer/applications/:id/verify_redirect_uri세션 쿠키 + manage 권한 + 최근 5분 내 OTP 재인증

라우트 정의 — config/routes.rb:524-525:

ruby
get :redirect_uri_verifications
post :verify_redirect_uri
  • Authz (developer/applications_controller.rb):
    • set_applicationscoped_applications.find → 보이지 않으면 404 (다른 org / 멤버십 없음).
    • GET 은 visibility-only — manage 체크 없음 (관리자도 동일).
    • POST 는 추가로 authorize_app_management_json! → 403 forbidden, 그리고 enforce_recent_otp_for_sensitive_json → 401 step_up_required (또는 otp_not_enrolled).
  • 감사: 결과는 Authentication::AuditLogger.record!(event_type: "redirect_uri_verify_dev", ...) 로 기록. event_type 은 AuthenticationAuditLog::EVENT_TYPES 화이트리스트 등록 필수.

Admin iOS 앱용 (/api/v1/admin)

MethodPath인증
GET/api/v1/admin/applications/:id/redirect_uri_verificationsadmin session token (no step-up)
POST/api/v1/admin/applications/:id/verify_redirect_uriadmin session + step-up

라우트 정의 — config/routes.rb:860-861:

ruby
get :redirect_uri_verifications
post :verify_redirect_uri

응답 schema 와 핵심 동작은 developer 쪽과 byte-for-byte 동일 (build_verification_entry 가 양쪽에서 동일 구현, Security::RedirectUriVerificationStatus 가 단일 status enum). 차이는 인증 / 감사뿐:

  • 감사 채널: AdminAuditLog.create!(action: "admin.application.redirect_uri.verify", ...).

GET 응답

http
GET /developer/applications/42/redirect_uri_verifications
json
{
  "application_id": 42,
  "verifications": [
    {
      "uri": "https://app.example.com/auth/callback",
      "tier": "https_public",
      "challenge_dns_record": "_logi-verify.app.example.com TXT \"f3a1...e9\"",
      "challenge_wellknown_url": "https://app.example.com/.well-known/logi-verification.txt",
      "challenge_wellknown_body": "f3a1...e9",
      "verified_at": "2026-05-25T12:34:56Z",
      "verification_method": "dns",
      "expires_at": "2026-08-25T12:34:56Z",
      "status": "verified"
    },
    {
      "uri": "logi-app://oauth/callback",
      "tier": "custom_scheme",
      "challenge_dns_record": null,
      "challenge_wellknown_url": null,
      "challenge_wellknown_body": "ab12...77",
      "verified_at": null,
      "verification_method": null,
      "expires_at": null,
      "status": "unverifiable_host"
    }
  ]
}
필드설명
uri등록된 redirect_uri 원본.
tierOauth::RedirectUriClassifier 분류 — https_public / https_org / localhost / custom_scheme / unknown.
challenge_dns_recordRP 운영자가 publish 할 TXT 레코드 문자열. 호스트가 비어 있거나 IP 리터럴이면 null.
challenge_wellknown_urlwell-known 파일을 둘 절대 URL.
challenge_wellknown_bodywell-known 파일에 그대로 적어야 할 챌린지 값 (HMAC hex).
verified_at마지막으로 성공 stamp 된 시각 (ISO 8601). 미검증이면 null.
verification_method"dns" 또는 "wellknown".
expires_at검증 만료 시각 (현재 정책 기준 stamp 시 자동 부여).
status아래 status enum 4가지 중 하나.

status enum

Security::RedirectUriVerificationStatus.derive 가 단일 진실 (server/app/services/security/redirect_uri_verification_status.rb):

status조건
unverifiable_hosttierlocalhost / custom_scheme / unknown 중 하나 — DNS/HTTP 챌린지가 의미 없음.
unverifiedverified_at 가 비어 있음.
verifiedverified_at 가 있고 expires_at 가 비어 있거나 미래. 단, expires_at 파싱 실패는 "TTL 없음" 으로 취급 (verified 유지) — parse 글리치로 운영자 확인된 URI 가 silent downgrade 되지 않도록.
expiredverified_at 가 있고 expires_at 가 과거.

POST 응답

http
POST /developer/applications/42/verify_redirect_uri
Content-Type: application/json

{ "uri": "https://app.example.com/auth/callback" }

성공:

json
{
  "uri": "https://app.example.com/auth/callback",
  "verified": true,
  "method": "dns",
  "reason": null,
  "detail": null
}

실패 예 (DNS 도 well-known 도 fail):

json
{
  "uri": "https://app.example.com/auth/callback",
  "verified": false,
  "method": null,
  "reason": "unverified",
  "detail": "dns=dns: no matching TXT record wellknown=wellknown: body mismatch"
}

reason

Security::RedirectUriVerifier 가 반환하는 reason 심볼 (redirect_uri_verifier.rb):

reason의미
unparseable_uriURI.parse 가 실패.
unverifiable_host호스트가 비어 있거나, IP 리터럴, 또는 localhost / RFC1918 / link-local — pre-gate 차단.
unverifiedDNS TXT 와 well-known 모두 챌린지 mismatch (정상 시도였으나 일치 못함).
dns_no_recordDNS 가 NXDOMAIN / SERVFAIL.
dns_timeoutDNS resolve 타임아웃 (3초 + 1초 retry).
dns_error기타 DNS 예외. detail 에 클래스명.
ssrf_blockedwell-known 단계에서 WebhookUrlValidator.resolve_safe! 가 차단 (DNS rebinding / private IP 등).
not_foundwell-known URL 이 4xx (404 등).
server_errorwell-known URL 이 5xx.
redirect_not_allowedwell-known 응답이 3xx (리디렉트는 거부).
tls_invalidTLS 핸드셰이크 / cert SAN 불일치.
timeoutwell-known 읽기 타임아웃 (5초).
body_too_largewell-known body 가 256바이트 초과.
http_error기타 HTTP/소켓 오류.

POST 에러 응답

HTTP응답 body원인
422{ "error": "uri_not_in_application" }body 의 uri 가 해당 application 의 redirect_uri_strings 에 없음.
429{ "error": "rate_limited" }같은 (app, uri) 에 대해 60초 안에 재시도.
401{ "error": "step_up_required" } / { "error": "otp_not_enrolled" }(developer) OTP 등록 안 됨 또는 최근 5분 내 재인증 없음.
403{ "error": "forbidden" }(developer) 보이긴 하나 manage 권한 없음.
404(없음)다른 org / 멤버십 없음.

SSRF 및 검증 안전 규칙

Security::RedirectUriVerifier 는 다음을 강제합니다 (라인 24-129, 162-293):

  1. Pre-gate (네트워크 호출 전): localhost / *.localhost / IP 리터럴 (IPv4·IPv6) / RFC1918 / RFC4193 / link-local / metadata IP 차단.
  2. DNS: Resolv::DNS.open(timeouts: [3, 1]) — 짧고 결정적. _logi-verify.<host> TXT 만 조회.
  3. well-known: WebhookUrlValidator.resolve_safe! 로 1회 resolve → 그 IP 로 직접 connect, SNI 는 원래 호스트 로 제시 → cert 의 CN/SAN 을 원래 호스트와 검증 (post_connection_check). DNS rebinding 봉쇄.
  4. redirect 거부: 3xx 응답은 그 자리에서 fail (redirect_not_allowed).
  5. body 캡: 256바이트 초과하면 즉시 abort (body_too_large).
  6. rate limit: verify_attempt:<app.id>:<sha256(uri)> 캐시 키에 unless_exist: true 로 atomic 작성. TOCTOU 없음.

CLI 와의 관계

logi apps verify <app_id> <uri> CLI 는 이 POST endpoint 의 thin wrapper 입니다. 운영자가 직접 raw HTTP 를 칠 일은 거의 없고, 콘솔의 검증 버튼이나 CLI 가 호출합니다. RP integration runbook 의 자동화 경로 (CI 에서 신규 redirect_uri 등록 직후 검증 트리거) 도 같은 endpoint 를 사용합니다.

관련 commits

  • 77c7adaa — Wave 5 F1 status enum 통합 (admin/dev 단일 source).
  • 218ea65RedirectUriVerificationStatus 추출 + malformed expires_at 처리 정책.
  • 659d21e — DNS 우선 / well-known fallback 2-prong 검증기 land.
  • 2c718fa — pinned-IP TLS + SNI cert 체크 + 3xx 거부 + body 256B 캡.
  • 856f5ed — POST endpoint 의 60초 atomic rate limit + audit hook.
  • RP Active Health Check/.well-known/logi-rp-health 활성 헬스 체크. redirect_uri verification 과 보완 관계 (호스트 소유권 ↔ 통합 alive).
  • PKCE — redirect_uri 정책 일반.
  • Public Clients — custom-scheme redirect_uri 의 보안 모델 (이 endpoint 가 unverifiable_host 로 분류하는 케이스).

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