-
Notifications
You must be signed in to change notification settings - Fork 0
feat: 이메일 발송 재전송 rate limit 및 계정 열거 방지 #38
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a587e04
192bd0d
8dc0861
cae1bb0
3ac1ab3
441eead
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,6 @@ | ||
| package com.nhnacademy.insightonauth.email; | ||
|
|
||
| import com.nhnacademy.insightonauth.exception.EmailSendException; | ||
| import com.nhnacademy.insightonauth.exception.InvalidVerificationCodeException; | ||
| import com.nhnacademy.insightonauth.exception.InvalidVerificationTokenException; | ||
| import com.nhnacademy.insightonauth.exception.VerificationTemporarilyLockedException; | ||
| import com.nhnacademy.insightonauth.exception.*; | ||
| import com.nhnacademy.insightonauth.redis.RedisKey; | ||
| import com.nhnacademy.insightonauth.redis.RedisService; | ||
| import jakarta.mail.MessagingException; | ||
|
|
@@ -51,14 +48,23 @@ public void sendVerificationCode(String email) { | |
|
|
||
| // 비밀번호 재설정 경로 발성 | ||
| public void sendPasswordResetPath(String email) { | ||
| // 1. 역방향 키로 이 이메일의 기존 토큰을 찾아 예전 링크를 무효화 | ||
| String oldUuid = redisService.get(RedisKey.PASSWORD_RESET_BY_EMAIL.getPrefix() + email); | ||
| if (oldUuid != null && !oldUuid.isBlank()) { | ||
| redisService.delete(RedisKey.PASSWORD_RESET.getPrefix() + oldUuid); | ||
| } | ||
|
|
||
| // 2. 새 토큰 발급 | ||
| String uuid = UUID.randomUUID().toString(); | ||
| // 재설정 경로 | ||
| String path = "https://insighton.store/password/reset?token=" + uuid; | ||
|
|
||
| // Redis에 저장 (10분 TTL) | ||
| // 3. 정방향 키 저장 (uuid → email, 10분 TTL) | ||
| redisService.set(RedisKey.PASSWORD_RESET.getPrefix() + uuid, email, Duration.ofMinutes(10)); | ||
|
|
||
| // 메일 발송 | ||
| // 4. 역방향 키 갱신 (email → uuid, 같은 TTL) | ||
| redisService.set(RedisKey.PASSWORD_RESET_BY_EMAIL.getPrefix() + email, uuid, Duration.ofMinutes(10)); | ||
|
Comment on lines
+51
to
+65
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift 비밀번호 재설정 토큰의 다중 키 갱신을 원자적으로 처리하세요.
그러면 다음 재발급이 최신 정방향 키를 찾지 못합니다. 최신 토큰이 이후 재발급 또는 비밀번호 변경 뒤에도 유효하게 남을 수 있습니다. 발급과 검증을 Lua 스크립트 또는 동등한 원자 연산으로 처리하세요. 검증 시에는 역방향 키의 값이 제출한 Also applies to: 133-135 🧰 Tools🪛 ast-grep (0.45.1)[warning] 58-58: Avoid building a URL host from untrusted input (tainted-url-host) 🤖 Prompt for AI Agents
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 역방향 키 삭제 시 값이 현재 처리 중인 token과 일치할 때만 삭제하도록 수정했습니다. 그 사이 재발급된 최신 토큰의 역방향 키는 보존됩니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
email_service=$(fd -a '^EmailService\.java$' . | head -n 1)
redis_service=$(fd -a '^RedisService\.java$' . | head -n 1)
printf '%s\n' '--- EmailService outline ---'
ast-grep outline "$email_service" --items all --type method
printf '%s\n' '--- Relevant EmailService methods ---'
rg -n -A35 -B4 'void sendPasswordResetPath\s*\(|String emailTokenVerify\s*\(' "$email_service"
printf '%s\n' '--- RedisService outline ---'
ast-grep outline "$redis_service" --items all --type method
printf '%s\n' '--- Conditional deletion / compare-and-delete usages ---'
rg -n -C3 -P '\b(deleteIf|compareAnd|execute|eval|PASSWORD_RESET_BY_EMAIL)\b' srcLength of output: 8016
redisService.delete(RedisKey.PASSWORD_RESET_BY_EMAIL.getPrefix() + savedEmail);또한 조건부 삭제 구현을 PR 브랜치에 반영해 주세요. 🧬 Code Graph Analysis Results
|
||
|
|
||
| // 5. 메일 발송 | ||
| send(email, "[InsightOn] 비밀번호 재설정", | ||
| "비밀번호 재설정 경로: " + path + "\n10분 이내에 수정해 주세요."); | ||
| } | ||
|
|
@@ -82,24 +88,25 @@ private void send(String to, String subject, String text) { | |
| } | ||
|
|
||
| // 이메일 코드 확인 | ||
| public String emailVerify(String email, String inputCode) { | ||
| public String emailCodeVerify(String email, String inputCode) { | ||
| // 입력 실패 잠금 체크 (검증 전용) | ||
| if (redisService.hasKey(RedisKey.VERIFY_FAIL_LOCK.getPrefix() + email)) { | ||
| throw new VerificationTemporarilyLockedException("인증 시도가 5회 초과되어 5분간 잠겼습니다."); | ||
| } | ||
|
|
||
| String savedCode = redisService.get(RedisKey.VERIFY.getPrefix() + email); | ||
|
|
||
| if (savedCode == null || !savedCode.equals(inputCode)) { | ||
| // 만료되었거나 애초에 요청한 적 없음 | ||
| increaseVerifyFailCount(email); | ||
| increaseVerifyFailCount(email); // 입력 실패 카운트 (여기서 5회 넘으면 VERIFY_FAIL_LOCK 걸림) | ||
| throw new InvalidVerificationCodeException("인증 코드가 올바르지 않거나 만료되었습니다."); | ||
| } | ||
|
|
||
| // 성공 처리 | ||
| redisService.delete(RedisKey.VERIFY_FAIL.getPrefix() + email); | ||
| redisService.delete(RedisKey.VERIFY.getPrefix() + email); | ||
|
|
||
| String verificationToken = UUID.randomUUID().toString(); | ||
| redisService.set(RedisKey.VERIFIED.getPrefix() + email, verificationToken, Duration.ofMinutes(15)); | ||
| redisService.delete(RedisKey.VERIFY.getPrefix() + email); | ||
| return verificationToken; | ||
| } | ||
|
|
||
|
|
@@ -123,7 +130,10 @@ public String emailTokenVerify(String token) { | |
| throw new InvalidVerificationTokenException("인증 토큰이 올바르지 않거나 만료되었습니다."); | ||
| } | ||
|
|
||
| // 정방향 + 역방향 키 모두 삭제 (토큰 1회용, 재설정 완료 후 정리) | ||
| redisService.delete(RedisKey.PASSWORD_RESET.getPrefix() + token); | ||
| redisService.delete(RedisKey.PASSWORD_RESET_BY_EMAIL.getPrefix() + savedEmail); | ||
|
|
||
| return savedEmail; | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.nhnacademy.insightonauth.exception; | ||
|
|
||
| public class InvalidMergeRequestException extends BusinessException { | ||
| public InvalidMergeRequestException(String message) { | ||
| super(ErrorCode.INVALID_MERGE_REQUEST, message); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.nhnacademy.insightonauth.exception; | ||
|
|
||
| public class OauthLinkedToOtherAccountException extends BusinessException { | ||
| public OauthLinkedToOtherAccountException(String message, Long conflictingUserId) { | ||
| super(ErrorCode.OAUTH_LINKED_TO_OTHER_ACCOUNT, message); | ||
|
Comment on lines
+4
to
+5
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
예외 필드와 getter를 추가하고, 전역 오류 응답에 이 값을 포함하세요. 수정 예시 public class OauthLinkedToOtherAccountException extends BusinessException {
+ private final Long conflictingUserId;
+
public OauthLinkedToOtherAccountException(String message, Long conflictingUserId) {
super(ErrorCode.OAUTH_LINKED_TO_OTHER_ACCOUNT, message);
+ this.conflictingUserId = conflictingUserId;
}
+
+ public Long getConflictingUserId() {
+ return conflictingUserId;
+ }
}🤖 Prompt for AI Agents
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. secondaryUserId 오류 응답에 포함 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
exception_file="src/main/java/com/nhnacademy/insightonauth/exception/OauthLinkedToOtherAccountException.java"
ast-grep outline "$exception_file" --items all --view expanded
printf '\n--- exception source ---\n'
cat -n "$exception_file"
printf '\n--- exception and error-response references ---\n'
rg -n -C 4 \
-e '\bOauthLinkedToOtherAccountException\b' \
-e '\bconflictingUserId\b' \
-e '\bsecondaryUserId\b' \
-e '`@ExceptionHandler`' \
src/main/java
printf '\n--- candidate error response files ---\n'
fd -t f -e java . src/main/java | rg '(Exception|Error|Response|Advice|Handler)'Length of output: 13856 🏁 Script executed: #!/bin/bash
set -euo pipefail
for file in \
src/main/java/com/nhnacademy/insightonauth/exception/ErrorResponse.java \
src/main/java/com/nhnacademy/insightonauth/exception/BusinessException.java \
src/main/java/com/nhnacademy/insightonauth/handler/GlobalExceptionHandler.java
do
printf '\n--- %s ---\n' "$file"
cat -n "$file"
done
printf '\n--- mergeAccount endpoint and request DTO references ---\n'
rg -n -C 5 \
-e '\bmergeAccount\s*\(' \
-e '\bsecondaryUserId\b' \
src/main/javaLength of output: 7416
따라서 이 예외에 대해 You are interacting with an AI system. |
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.nhnacademy.insightonauth.exception; | ||
|
|
||
| public class PasswordResetResendLockedException extends BusinessException { | ||
| public PasswordResetResendLockedException(String message) { | ||
| super(ErrorCode.PASSWORD_RESET_RESEND_LOCKED, message); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.nhnacademy.insightonauth.exception; | ||
|
|
||
| public class PasswordResetResendTooSoonException extends BusinessException { | ||
| public PasswordResetResendTooSoonException(String message) { | ||
| super(ErrorCode.PASSWORD_RESET_RESEND_TOO_SOON, message); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.nhnacademy.insightonauth.exception; | ||
|
|
||
| public class VerificationResendLockedException extends BusinessException { | ||
| public VerificationResendLockedException(String message) { | ||
| super(ErrorCode.VERIFICATION_RESEND_LOCKED, message); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.nhnacademy.insightonauth.exception; | ||
|
|
||
| public class VerificationResendTooSoonException extends BusinessException { | ||
| public VerificationResendTooSoonException(String message) { | ||
| super(ErrorCode.VERIFICATION_RESEND_TOO_SOON, message); | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.