본문으로 건너뛰기

결정-0015: 이메일 소유 증명은 제공자 주장이 아니라 자체 OTP로 한다

상태

승인됨

맥락

결정-0014로 세션 방식을 정한 뒤 남은 미결은 소셜 제공자가 알려주는 이메일 검증 여부를 어떻게 다루느냐였습니다. AUTH-05(이메일 키 통합)의 원래 설계는 제공자가 준 이메일을 통합 키로 쓰고, 제공자가 검증했다고 알려줄 때만 자동 연결하는 것이었습니다. 그 전제에서 네이버가 검증 여부를 제공하는지 확인되지 않은 상태였고, 확인 결과에 따라 AUTH-05의 동작이 제공자별로 갈릴 예정이었습니다.

당시 전제는 제공자 3종(구글 · 카카오 · 네이버)과 4테이블 구조(user · session · account · verification), user.email 의 전역 유일성, same-origin 배포, 향후 모바일 앱 추가 가능성입니다. 그리고 비범위에 있던 결제를 나중에 편입할 가능성이 논의의 무게를 바꿨습니다.

확인한 사실

제공자별 이메일 검증 신호는 다음과 같습니다.

제공자 검증 신호 비고
구글 email_verified (OIDC 표준) 있습니다
카카오 is_email_verified 있습니다. 별도로 is_email_valid 도 제공합니다
네이버 없습니다 프로필 응답에 검증 신호가 포함되지 않습니다

네이버만의 문제가 아닙니다. Facebook과 Twitter, Twitch, Spotify, WindowsLive, WorkOS도 명시적 검증 플래그가 없습니다. 즉 제공자 이메일을 신뢰하는 설계는 제공자를 추가할 때마다 인증 로직을 재검토해야 하는 부채입니다. 이 아키텍처를 채용하는 프로젝트가 제공자를 추가할 것을 전제하므로 이 부채가 큽니다.

카카오 문서는 두 가지를 직접 경고합니다. 유효 여부와 인증 여부를 항상 확인하고 사용해야 한다는 것, 그리고 이메일을 ID나 유일성 기준으로 쓰지 말라는 것입니다. 사용자가 변경할 수 있기 때문이며, AUTH-05의 원래 설계가 정면으로 걸리는 지점입니다.

pre-hijacking 실측 결과가 판단을 굳혔습니다. Pre-hijacked accounts (USENIX Security 2022)는 인기 사이트 75개 중 최소 35개가 취약함을 실측했습니다. 5가지 변종 중 둘이 본 결정과 직결됩니다.

변종 성립 경로
Classic-Federated Merge 공격자가 피해자 이메일로 비밀번호 가입을 하고, 미검증 계정이 함정으로 잠들어 있다가, 피해자가 나중에 같은 이메일로 소셜 로그인하면 병합되어 공격자의 비밀번호가 피해자 계정에 남습니다. 공격자는 검증을 통과할 필요가 없습니다. 검증은 피해자가 소셜 로그인으로 대신 해 줍니다
Non-Verifying IdP 위의 거울상입니다. 검증하지 않는 제공자로 공격자가 먼저 가입합니다

논문이 지목한 근본 원인은 하나입니다. 서비스가 계정 기능을 허용하기 전에 사용자가 그 식별자를 실제로 소유하는지 검증하지 않는다는 것입니다.

보편적 관행은 두 층으로 갈립니다. 일반 웹앱용 라이브러리는 자동 병합이 기본값이지만 제공자가 검증을 확인했거나 신뢰 목록에 있을 때만 허용하며, 신뢰 목록에는 계정 탈취 위험을 높인다는 경고가 붙어 있습니다. 반면 IdP 제품은 검증된 이메일이어도 자동 연결하지 않습니다. 검증된 이메일은 사용자가 지금 두 계정 모두에 인증할 수 있다는 증거가 못 된다는 것이 근거이며, 두 계정 모두에 로그인시킨 뒤 연결합니다. 다수 관행이 곧 안전은 아니며 그 다수의 절반이 실제로 취약했습니다.

결제를 붙일 계획이 본 결정의 무게를 바꿉니다

축 내용
탈취의 값 저장된 결제수단과 활성 구독이 들어 있는 계정은 탈취 시 정상 구매 이력 때문에 기본 사기 탐지를 통과합니다. 가맹점은 상품과 차지백으로 두 번 손해를 봅니다. 구독은 차지백이 도착하기까지 몇 달간 이행이 계속되어 더 나쁩니다
중복 계정의 비용 소셜로 구독한 사용자가 나중에 비밀번호로 로그인하면 구독이 보이지 않습니다. 권한 판정이 user 단위인데 user 가 쪼개지면 판정 자체가 틀립니다
사후 병합의 불가능성 결제 전이면 계정 병합은 행 몇 개를 옮기는 일입니다. 결제 후에는 결제 대행사의 Customer 와 구독, 인보이스, 세금 기록이 각 user 에 붙어 있고 구독은 Customer 간에 이관되지 않습니다. 취소하고 재생성해야 하며 청구 주기와 인보이스 이력이 깨집니다

따라서 user 정체성은 결제를 붙이기 전에 확정해야 합니다. 지금 느슨하게 두고 나중에 조이는 선택은 존재하지 않습니다.

검토한 대안

제공자 이메일을 신뢰하고 검증된 경우에만 자동 병합합니다

구분 내용
장점 소셜 가입이 1단계로 끝납니다. 구현이 가장 적습니다
단점 신호가 없는 제공자에서 동작이 갈립니다. 제공자를 추가할 때마다 신호 유무를 재조사해야 합니다
기각 사유 카카오 문서가 경고하는 "이메일을 유일성 기준으로 쓰기"를 그대로 수행합니다

연동 시 두 계정 모두에 인증을 요구합니다

구분 내용
장점 가장 보수적이며 지금 접근 가능함을 증명합니다
단점 소셜 로그인마다 추가 인증이 붙어 사용성 비용이 큽니다
기각 사유 임의의 IdP를 상대하는 제품의 기준입니다. 제공자가 소수로 고정된 상황에는 과합니다

사용자가 직접 입력한 이메일을 자체 OTP로 증명합니다 (채택)

제공자의 검증 신호에 의존하지 않으므로 제공자를 추가해도 인증 로직을 건드릴 필요가 없습니다. 신뢰 근원이 자기 쪽에 있어 제공자별 분기가 사라집니다. 소유 증명을 가입 선행 조건으로 두면 pre-hijacking 5종 중 3종이 전제부터 성립하지 않습니다. 대가는 소셜 최초 로그인이 2단계가 되고 OTP 인프라를 직접 책임진다는 점입니다.

증명 수단은 OTP 와 매직링크 중 무엇인가

축 OTP(6자리) 매직링크
컨텍스트 유지 원래 탭에 머뭅니다 새 탭과 인앱 브라우저에서 열려 컨텍스트가 끊깁니다
메일 스캐너 프리페치 해당 없습니다 안티피싱 프록시가 사용자보다 먼저 토큰을 소진합니다
추측 저항 10⁶ 이므로 시도 제한이 필수입니다 128비트로 원천 안전합니다
모바일 앱 확장 코드 입력으로 끝납니다 Universal Links 와 App Links 설정이 필요합니다
발송 채널 교체 채널과 무관합니다 메일과 도메인 설정에 묶입니다

매직링크가 우세한 축은 엔트로피 하나이며 표준 방어책으로 메울 수 있습니다. 반면 소셜 최초 로그인 중간에 이메일 증명을 끼워 넣기로 했으므로 컨텍스트 유지가 결정적입니다. 매직링크는 제공자 인증을 마친 브라우저와 링크가 열리는 브라우저가 달라 중간 상태를 서버 쪽에서 이어 붙여야 합니다.

결정

사용자가 직접 입력한 이메일을 자체 OTP로 증명하며 증명 수단은 OTP로 합니다.

  1. 이메일은 사용자가 직접 입력하고 자체 OTP로 소유를 증명합니다. 소유 증명 없이는 계정이 생기지 않으며, 이것이 본 결정의 핵심입니다. AUTH-01의 "가입 후 검증" 정책을 "증명 선행"으로 바꿉니다.
  2. 소셜 제공자가 준 이메일은 신뢰하지 않습니다. 이메일 scope를 요구하지 않으며 받더라도 입력칸 기본값 이상으로 쓰지 않습니다. 제공자는 신원만 증명합니다.
  3. OTP 파라미터를 다음과 같이 고정합니다.
항목 값 사유
형식 6자리. CSPRNG 로 생성하며 앞자리 0을 허용합니다 자릿수를 줄이면 전수 공간이 좁아집니다
수명 만료 10분. 사용 즉시 폐기하는 1회용입니다
시도 제한 5회 실패 시 코드를 폐기하고 재발송을 요구합니다
저장 앱 시크릿을 키로 하는 HMAC-SHA256. 평문을 금지합니다 단순 해시는 10⁶ 전수라 DB 유출 시 즉시 복원되고, bcrypt 는 불필요하게 느려 시도 제한과 겹치면 서비스 거부 지점이 됩니다
발송 제한 비대칭으로 둡니다. IP별을 촘촘히, 이메일별은 넉넉히 겁니다 이메일별만 조이면 공격자가 한도를 소진시켜 정상 가입을 막는 서비스 거부가 됩니다
메일 문구 요청하지 않았다면 코드를 누구에게도 알려주지 말라는 안내를 넣습니다
  1. 계정 생성 시점과 대기 레코드를 다음과 같이 규정합니다. 이 항목이 지켜지지 않으면 위 결정이 무의미해집니다.
  • 코드 발급 단계에서 user 행을 만들지 않습니다. 대기 중인 가입 시도는 verification 에만 둡니다. 편의상 대기 사용자를 미리 만들면 없앤 함정이 되살아납니다.
  • 이메일 유일성 제약은 user 에만 걸고 대기 레코드에는 걸지 않습니다. 대기 시도가 이메일을 예약하면 공격자가 코드 발급만 반복해 선점할 수 있습니다. 같은 이메일에 여러 대기 시도가 동시에 존재할 수 있고 먼저 코드를 맞힌 쪽이 계정을 얻습니다.
  • OTP 검증은 (이메일, 코드) 가 아니라 대기 레코드 식별자로 조회합니다. 시작 시점에 발급한 식별자를 클라이언트가 들고 있어야 합니다. 이메일과 코드만으로 찾으면 공격자가 피해자 이메일로 시작한 대기 레코드를 피해자가 자기 화면에서 완성시키는 공격이 성립합니다.
  • 소셜 최초 로그인의 검증된 제공자 신원은 대기 레코드에 함께 담깁니다. account.userId 가 NOT NULL 이므로 user 없이 account 에 둘 수 없습니다.
  • OTP 통과 후 세션 토큰을 새로 발급합니다. 대기 단계의 토큰을 승격시키면 세션 고정이 됩니다.
  1. 이메일 동일성 판정은 소문자화와 앞뒤 공백 제거까지만 수행합니다. 점 제거와 플러스 태그 제거는 하지 않습니다. Gmail 은 같은 함으로 취급하지만 다른 제공자는 다른 주소로 취급하므로, 제거하면 그쪽에서 남의 주소로 가입되는 사고가 납니다. 원문을 저장하고 유일성 제약은 정규화된 값에 겁니다.
  2. AUTH-05 연동 규칙은 기존 방침을 유지합니다. 이메일 입력 단계에서 기존 계정이 발견되면 OTP를 보내지 않고 로그인 후 연동 경로로 안내합니다. 연동 경로에 OTP가 들어가지 않으므로 훔칠 코드가 없고, 기존 계정에 로그인하는 것 자체가 양쪽 인증 요구를 충족합니다.
  3. account 의 키는 (provider, providerAccountId) 이며 유니크입니다. 제공자의 subject id로 잡으며 이메일을 제공자 식별 키로 쓰지 않습니다.
  4. verification 은 다목적이므로 용도 컬럼을 두고 조회에서 항상 필터합니다. 가입과 이메일 변경, 비밀번호 재설정, 로그인 복구가 같은 테이블을 씁니다. 이메일 변경용 코드가 비밀번호 재설정에 통과하면 그것만으로 계정 탈취입니다.
  5. 메일 발송은 트랜잭션 커밋 이후에 합니다. 트랜잭션 안에서 발송하면 롤백 시 유령 코드가 나갑니다. 발송 실패는 재발송으로 복구합니다(결정-0016).
  6. 이메일이 계정의 최상위 신뢰 근원이 된다는 사실을 명시합니다. 메일함을 장악한 사람은 계정을 장악합니다. 비밀번호 재설정이 있는 거의 모든 서비스가 이미 그런 구조이므로 새로 생기는 약점은 아니지만, 민감한 도메인에 이 아키텍처를 적용할 때는 2단계 인증을 얹어야 한다는 신호로 삼습니다.

스키마의 컬럼과 인덱스 수준 상세는 데이터베이스 설계 문서가 정합니다.

재검토 조건

  • 제공자가 자체 증명보다 강한 신원 보증을 주는 경우를 편입할 때는 그 제공자에 한해 OTP를 생략하는 예외를 결정기록으로 남깁니다. 기업 SSO가 그 예입니다.
  • OTP 발송 비용과 도달률이 문제가 되는 경우에는 발송 채널을 바꾸는 것으로 대응합니다. OTP는 채널과 무관하므로 본 결정 자체는 유지됩니다.
  • 가입 이탈률이 실제로 측정되어 문제로 확인되는 경우에는 소셜 2단계를 완화하는 방안을 그때 검토합니다. 다만 결제가 이미 붙어 있으면 되돌리는 비용이 큽니다.

결과

  • pre-hijacking 5종 중 Classic-Federated Merge 와 Non-Verifying IdP, Trojan Identifier 가 전제부터 성립하지 않습니다. 함정 계정이 만들어질 수 없기 때문입니다. 남은 둘은 세션 무효화(AUTH-07)와 이메일 변경 정책(PROF-05)이 담당합니다.
  • 이메일 스쿼팅이 불가능해집니다. 남의 이메일을 선점해 정상 가입을 차단하는 경로가 막힙니다.
  • 제공자를 추가해도 인증 로직을 재검토할 필요가 없습니다. 이 아키텍처를 채용하는 프로젝트가 다른 제공자를 붙일 때 검증 신호 유무를 조사하지 않아도 됩니다.
  • user 정체성이 결제를 붙이기 전에 확정되므로 사후 병합의 불가능성을 피합니다.
  • 감수하는 것
    • 소셜 최초 로그인이 2단계가 됩니다. 제공자 인증 후 이메일 입력과 OTP를 거칩니다.
    • OTP 발급과 검증, 발송 제한, 재발송을 직접 책임집니다. 매직링크보다 방어 항목이 많습니다.
    • 플러스 태그를 허용하므로 한 사람이 계정을 여럿 만들 수 있습니다. 결제 편입 시 무료 체험 남용 경로가 되며, 인증 계층이 아니라 남용 방지 계층에서 다룹니다.
    • 가입과 연동 흐름은 계정 존재 여부를 완전히 숨길 수 없습니다. 화면에는 안내 메일을 보냈다는 문구만 노출하고 실제 분기를 메일 내용으로 가르는 완화책을 설계에서 검토합니다.
© 2026 dev.goraebap