서문
최근 Next.js로 프론트엔드와 백엔드 프로젝트를 몇 개 작성했는데, 그중 better-auth를 사용해 사용자 인증을 구현했습니다.
그러나 실제로 AI가 관련 코드를 작성하면 비밀번호를 평문으로 전송하는 경우가 많았습니다. 프로덕션 환경에서는 보통 HTTPS를 사용하고 전송 계층에 이미 TLS 암호화가 되어 있지만, 그렇다고 해서 비밀번호를 마음대로 평문으로 요청 본문에 넣어도 된다는 의미는 아닙니다.
HTTPS는 브라우저에서 TLS 종료 지점(예: nginx) 사이의 전송 보안만 보장할 뿐, 비밀번호는 리버스 프록시, 게이트웨이, 로그, 데이터베이스 등에서 여전히 평문 형태로 나타납니다. 이는 해당 접근 권한을 가진 사람이라면 누구나 모든 사용자의 계정에 로그인할 수 있다는 것을 의미합니다. 개인 프로젝트에서는 비밀번호 암호화를 무시해도 되지만, 이는 분명 제품으로서 받아들일 수 없는 일입니다.
이에 이 블로그 글을 작성하게 되었으며, better-auth를 사용할 때 애플리케이션 계층에서 비밀번호에 RSA 비대칭 암호화를 적용하는 방법을 설명합니다. 이 내용을 AI에게 학습시켜 다른 프로젝트에서도 유사하게 구현할 수 있겠지만, MIT 라이선스에 따라 출처를 명시해 주시기 바랍니다. 자세한 내용은 글 끝을 참조하세요.
전체 흐름
- 사용자가 브라우저에서 비밀번호를 입력합니다.
- 프론트엔드에서
NEXT_PUBLIC_RSA_PUBLIC_KEY를 읽습니다. - RSA-OAEP를 사용하여 비밀번호를 암호화합니다.
- 암호화 패킷에 타임스탬프, 랜덤 솔트, 요청 ID, HMAC 서명을 포함합니다.
- 프론트엔드에서 암호화된 문자열을 better-auth에 전달합니다.
- 서버에서 better-auth의
password.hash/password.verify에서 복호화합니다. - 최종적으로 데이터베이스에는 bcrypt hash만 저장합니다.
전체 흐름에서 평문 비밀번호는 사용자가 비밀번호를 입력할 때만 나타납니다.
클라이언트 암호화
여기서는 비밀번호 자체만 암호화하는 것이 아니라, 여러 필드를 RSA 암호문에 함께 넣었습니다:
password|timestamp|salt|requestId
각각:
password는 사용자가 입력한 원본 비밀번호입니다.timestamp는 요청 만료 여부를 판단하는 데 사용됩니다.salt는 동일한 비밀번호라도 매번 다른 암호문이 생성되도록 합니다.requestId는 리플레이 공격 방지에 사용됩니다.
로그인 페이지에서 better-auth에 제출하기 전에 먼저 rsaEncrypt를 호출합니다.
회원가입 시에도 동일합니다:
이렇게 하면 패킷 캡처 시 보이는 password 필드는 원본 비밀번호가 아니라 RSA 암호화된 데이터 패킷입니다.

서버 복호화
먼저 개인 키로 복호화합니다:
그 다음 통합 검증을 수행합니다:
여기서 주로 다음과 같은 작업을 수행합니다:
- 암호화 패킷 형식을 확인합니다.
- 타임스탬프 만료 여부를 확인합니다.
requestId가 이미 사용되었는지 확인합니다.- RSA 개인 키로 복호화합니다.
- 외부 타임스탬프와 내부 타임스탬프가 일치하는지 확인합니다.
- 외부 요청 ID와 내부 요청 ID가 일치하는지 확인합니다.
- HMAC 서명을 검증합니다.
- 비밀번호의 bcrypt hash를 반환합니다.
requestId 중복 제거에는 Redis를 사용합니다:
즉, 첫 번째 요청만 쓰기에 성공하고, 이후 동일한 requestId를 재사용하면 거부되어 리플레이 공격을 방지합니다.
공개 키와 개인 키 쌍 생성 명령은 다음과 같습니다:
물론 ssh-keygen을 직접 사용해도 가능하며, 개인 키의 보안을 반드시 보장해야 합니다.
better-auth 연동
better-auth는 기본적으로 이메일 비밀번호 로그인을 처리하지만, 여기서는 암호화와 해시를 구현하기 위해 비밀번호 처리 로직을 커스터마이즈해야 합니다.
여기에 한 가지 세부 사항이 있습니다: better-auth의 minPasswordLength는 1로, maxPasswordLength는 4096으로 설정되었습니다.
그 이유는 better-auth에 전달되는 것이 이미 원본 비밀번호가 아니라 RSA 암호화된 데이터 패킷이기 때문입니다. 기본 길이 제한을 계속 사용하면 better-auth 계층에서 쉽게 차단될 수 있습니다.
비밀번호 길이를 제한해야 한다면 클라이언트의 rsaEncrypt에서만 수행할 수 있습니다.
물론 better-auth는 기본적으로 로그인과 회원가입 시나리오에서만 비밀번호 검증이 필요하며, 비밀번호 변경, 계정 삭제 등의 경우 rsaEncrypt와 rsaDecryptAndValidate를 재사용하면 됩니다.
better-auth 기타 보안 설정
비밀번호 암호화 외에도 betterAuth 초기화 시 몇 가지 보안 항목을 설정할 수 있습니다:
의미는 대략 다음과 같습니다:
- CSRF 검사를 비활성화하지 않습니다.
- 프로덕션 환경에서 Secure Cookie를 사용합니다.
- Cookie에
httpOnly를 설정합니다. - Cookie에
sameSite: "lax"를 설정합니다.
그리고 로그인, 회원가입, 비밀번호 찾기에 대한 속도 제한:
이렇게 하면 크리덴셜 스터핑, 대량 회원가입, 이메일 스팸 등의 문제를 줄일 수 있습니다.