OTP 인증 이메일: 빠른 전달, 보안, 모범 사례

일회용 인증 코드(OTP)와 인증 링크를 안전하게 생성·저장·발송하는 방법. 빠른 전달, 적절한 만료 시간과 속도 제한, 사용자가 신뢰하는 템플릿까지 다룹니다.

일회용 코드 이메일은 제품이 보내는 메시지 중 사용자가 가장 조급하게 기다리는 메시지입니다. 사용자는 “보내 드린 6자리 코드를 입력하세요” 라는 화면을 바라보고 있고, 이메일이 늦어지는 1초 1초가 그대로 포기할지 고민하는 시간이 됩니다.

인증 이메일은 보안 경계 바로 위에 있기도 합니다. 잘못 만들면 실제 사용자를 막아 버리거나 공격자를 들여보내게 됩니다. 이 가이드에서는 두 측면을 모두 다룹니다. 코드를 받은편지함에 빠르게 전달하는 것, 그리고 올바른 사람만 코드를 사용할 수 있게 하는 것입니다.

코드와 링크, 무엇을 쓸까?

일반적으로 두 가지 형태가 있습니다.

  • 일회용 코드(OTP): 482913처럼 짧은 숫자로, 사용자가 앱에 직접 입력합니다. 로그인과 2단계 인증에 적합하며, 사용자가 다른 기기에서 이메일을 읽을 때도 사용할 수 있습니다.
  • 인증 링크: URL에 담긴 긴 무작위 토큰입니다. 클릭 한 번으로 절차가 끝나므로 가입 시 이메일 주소 확인이나 비밀번호 재설정에 적합합니다.

아래 내용은 두 가지 모두에 적용됩니다. 차이는 주로 길이입니다. 링크에는 긴 토큰을 담을 수 있지만, 코드는 직접 입력할 수 있을 만큼 짧아야 합니다.

코드를 안전하게 생성하기

사용하는 언어의 암호학적으로 안전한 난수 생성기를 사용하세요. Math.random()과 Python의 random 모듈은 예측 가능하므로 보안 코드에는 절대 사용해서는 안 됩니다.

// Node.js
import { randomInt, createHmac } from 'node:crypto';

const code = String(randomInt(0, 1_000_000)).padStart(6, '0'); // "048213"
// Keyed hash: OTP_SECRET lives in your secrets manager, never in the database.
const codeHash = createHmac('sha256', process.env.OTP_SECRET).update(`${user.id}:${code}`).digest('hex');
# Python
import hashlib
import hmac
import os
import secrets

code = f"{secrets.randbelow(1_000_000):06d}"
# Keyed hash: OTP_SECRET lives in your secrets manager, never in the database.
code_hash = hmac.new(os.environ["OTP_SECRET"].encode(), f"{user_id}:{code}".encode(), hashlib.sha256).hexdigest()

키 기반 해시(HMAC)만 저장하고, 사용자 ID, 만료 시각, 시도 횟수와 함께 보관하세요. 단순 SHA-256으로는 충분하지 않습니다. 가능한 코드가 100만 개뿐이라, 데이터베이스를 손에 넣은 사람은 1초도 안 되어 모든 코드를 대입해 볼 수 있습니다. HMAC에는 데이터베이스 밖에 보관된 시크릿이 필요하므로, 데이터베이스가 유출되더라도 그것만으로는 유효한 코드가 드러나지 않습니다.

빨리 만료시키고 시도 횟수 제한하기

6자리 코드에는 100만 가지 경우의 수가 있습니다. 이는 공격자가 계속 추측할 수 없을 때만 충분합니다.

제어 항목 권장 값
코드 유효 시간 로그인 코드는 10분, 이메일 확인 링크는 최대 24시간
코드당 오입력 허용 횟수 5회, 이후 코드 무효화
주소당 코드 발송 횟수 15분에 약 3회, “재전송” 버튼에 대기 시간 표시
IP 주소당 코드 발송 횟수 더 높은 상한을 두어 한 공격자가 여러 주소를 돌아가며 시도하는 것을 차단
사용 일회용: 인증에 성공하는 즉시 코드를 삭제하거나 사용됨으로 표시

코드는 항상 상수 시간으로 비교하세요(Node.js에서는 crypto.timingSafeEqual, Python에서는 hmac.compare_digest). 또한 새 코드를 발급하면 이전 코드는 무효화해야 합니다.

즉시 발송하기

OTP 이메일은 1분마다 실행되는 배치 작업이 아니라, 코드를 요청한 바로 그 요청 안에서 발송해야 합니다. Inbixio에서는 호출 한 번이면 됩니다.

await fetch('https://backend.inbixio.com/api/send-email/', {
  method: 'POST',
  headers: { 'X-API-Key': process.env.INBIXIO_API_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    to_email: user.email,
    subject: `${code} is your YourApp sign-in code`,
    message: otpTemplate({ code, minutes: 10 }),
  }),
  signal: AbortSignal.timeout(10_000),
});

Inbixio는 요청을 접수하고 곧바로 전달을 대기열에 넣으므로, 엔드포인트가 빠르게 응답할 수 있습니다. 호출이 실패하면 오지 않을 이메일을 기다리게 두지 말고 즉시 사용자에게 알리세요(“코드를 보내지 못했습니다. 잠시 후 다시 시도해 주세요”). 어떤 오류를 자동으로 재시도할 만한지는 이메일 API 오류 처리하기에 정리되어 있습니다.

코드 발송에 특히 유용한 두 가지 팁:

  • 전용 발신 주소를 사용하세요. 예를 들어 security@yourco.com을 별도의 서버로 연결하고, 그 서버에 고정된 API 키를 사용합니다(해당 서버를 제거하게 되면 키를 다시 고정하세요). 사용자는 그 주소를 신뢰하게 되고, 마케팅 캠페인이 그 주소의 평판에 영향을 줄 수 없습니다.
  • 도메인을 인증하세요. 스팸함에 들어간 코드는 도착하지 않은 코드나 마찬가지입니다. SPF, DKIM, DMARC 체크리스트를 차례로 점검하세요.

피싱으로 오해받지 않는 이메일 작성하기

여러분의 템플릿은 바로 이 형식을 그대로 베끼는 피싱 이메일과 경쟁합니다. 진짜 이메일을 쉽게 알아볼 수 있게 만드세요.

  • 제목에 코드를 넣으세요. 예: “482913은 YourApp 로그인 코드입니다”. 사용자는 이메일을 열지 않고도 알림에서 코드를 읽을 수 있고, 휴대폰 키보드가 코드를 자동으로 제안할 수도 있습니다.
  • 용도와 유효 시간을 밝히세요: “YourApp에 로그인하려면 이 코드를 사용하세요. 코드는 10분 후 만료됩니다.”
  • 본인이 요청하지 않았을 때 할 일을 알려 주세요: “로그인을 시도하지 않으셨다면 이 이메일을 무시하셔도 됩니다. 계정은 안전합니다.”
  • 코드를 다시 알려 달라고 절대 요구하지 마세요. 분명하게 적어 두세요: “YourApp은 전화나 이메일로 이 코드를 절대 묻지 않습니다.”
  • 짧게, 마케팅 내용 없이 작성하고, 링크는 최소한으로 줄이세요.

간단하고 접근성 좋은 HTML 템플릿:

<div style="font-family:system-ui,sans-serif;max-width:480px;margin:auto;color:#111">
  <p>Use this code to sign in to YourApp:</p>
  <p style="font-size:32px;font-weight:700;letter-spacing:6px;margin:24px 0">482913</p>
  <p>It expires in 10 minutes. If you didn't try to sign in, you can ignore this email.</p>
  <p style="color:#666;font-size:13px">YourApp will never ask you for this code.</p>
</div>

인라인 스타일은 의도한 것입니다. 많은 이메일 클라이언트가 <style> 블록을 제거하므로, 인라인 CSS가 이메일 서식을 지정하는 가장 확실한 방법입니다.

어떤 이메일에 계정이 있는지 노출하지 않기

로그인 화면이나 “코드 보내기” 화면에서는 주소가 등록되어 있든 아니든 같은 응답을 보여 주세요. “해당 주소로 등록된 계정이 있다면 코드를 보내 드렸습니다.” 그렇지 않으면 여러분의 폼이 어떤 이메일 주소가 제품을 사용하는지 확인하는 무료 도구가 되어 버립니다.

체크리스트

  • 암호학적으로 안전한 생성기로 코드 생성, HMAC만 저장하고 시크릿은 데이터베이스 밖에 보관
  • 10분 만료, 일회용, 5회 시도 제한, 상수 시간 비교
  • 주소별·IP별 속도 제한과 재전송 대기 시간
  • 즉시 발송하고, 발송 실패 시 명확한 오류 표시
  • 인증된 전용 발신 주소
  • 제목에 코드, 본문에 용도와 만료 시간, “본인이 아니신가요?” 안내
  • 계정 존재 여부와 관계없이 동일한 응답

Inbixio로 보내세요

자체 SMTP 서버를 연결하고, 권한이 제한된 API 키를 만들어 HTTPS 요청 한 번으로 어떤 언어에서든 발송하세요.

계정 만들기 요금제 보기