Skip to content

Commit

Permalink
Merge pull request #93 from alzzaipo/fix/#92-email-verification-vulne…
Browse files Browse the repository at this point in the history
…rability

이메일 인증 우회 취약점 해결
  • Loading branch information
csct3434 authored Jan 10, 2024
2 parents d477fb2 + a142cf7 commit d0fba32
Show file tree
Hide file tree
Showing 34 changed files with 716 additions and 600 deletions.
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
package com.alzzaipo.common.email.adapter.out.persistence;

import com.alzzaipo.common.email.domain.EmailVerificationPurpose;
import com.alzzaipo.common.email.domain.EmailVerificationStatus;
import com.alzzaipo.common.email.port.out.verification.CheckEmailVerifiedPort;
import com.alzzaipo.common.email.port.out.verification.CheckEmailVerificationCodePort;
import com.alzzaipo.common.email.port.out.verification.DeleteEmailVerificationStatusPort;
import com.alzzaipo.common.email.port.out.verification.SaveEmailVerificationCodePort;
import com.alzzaipo.common.email.port.out.verification.VerifyEmailVerificationCodePort;
import com.alzzaipo.common.util.JsonUtil;
import java.util.concurrent.TimeUnit;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
Expand All @@ -18,69 +15,33 @@
@Repository
@RequiredArgsConstructor
public class EmailVerificationPersistenceAdapter implements SaveEmailVerificationCodePort,
VerifyEmailVerificationCodePort,
CheckEmailVerifiedPort,
DeleteEmailVerificationStatusPort {

@Value("${cache.expiration-time-millis.email-verification}")
private long EXPIRATION_TIME_MILLIS;

private final RedisTemplate<String, String> redisTemplate;

@Override
public void save(String email, String verificationCode, String subject, EmailVerificationPurpose purpose) {
String namespace = purpose.getNamespace();
EmailVerificationStatus initialStatus = new EmailVerificationStatus(subject, false);

redisTemplate.opsForValue().set(namespace + verificationCode, email,
EXPIRATION_TIME_MILLIS, TimeUnit.MILLISECONDS);

redisTemplate.opsForValue().set(namespace + email, JsonUtil.toJson(initialStatus),
EXPIRATION_TIME_MILLIS * 2, TimeUnit.MILLISECONDS);
}

@Override
public boolean verify(String verificationCode, EmailVerificationPurpose purpose) {
String namespace = purpose.getNamespace();

String foundEmail = redisTemplate.opsForValue().get(namespace + verificationCode);
if (foundEmail == null) {
return false;
}

String jsonVerificationStatus = redisTemplate.opsForValue().get(namespace + foundEmail);
if (jsonVerificationStatus == null) {
return false;
}

EmailVerificationStatus verificationStatus = JsonUtil.fromJson(jsonVerificationStatus,
EmailVerificationStatus.class);
verificationStatus.setVerified(true);
redisTemplate.opsForValue().set(namespace + foundEmail, JsonUtil.toJson(verificationStatus));
return true;
}

@Override
public boolean check(String email, String subject, EmailVerificationPurpose purpose) {
String namespace = purpose.getNamespace();

String jsonVerificationStatus = redisTemplate.opsForValue().get(namespace + email);
if (jsonVerificationStatus == null) {
return false;
}

EmailVerificationStatus verificationStatus = JsonUtil.fromJson(jsonVerificationStatus,
EmailVerificationStatus.class);
if (!subject.equals(verificationStatus.getSubject())) {
return false;
}
return verificationStatus.isVerified();
}

@Override
public void delete(String email, EmailVerificationPurpose purpose) {
String namespace = purpose.getNamespace();
redisTemplate.delete(namespace + email);
}
CheckEmailVerificationCodePort,
DeleteEmailVerificationStatusPort {

@Value("${cache.expiration-time-millis.email-verification}")
private long EXPIRATION_TIME_MILLIS;

private final RedisTemplate<String, String> redisTemplate;

@Override
public void save(String email, String verificationCode, EmailVerificationPurpose purpose) {
String namespace = purpose.getNamespace();

redisTemplate.opsForValue()
.set(namespace + email, verificationCode, EXPIRATION_TIME_MILLIS, TimeUnit.MILLISECONDS);
}

@Override
public boolean check(String email, String userInputVerificationCode, EmailVerificationPurpose purpose) {
String namespace = purpose.getNamespace();
String validVerificationCode = redisTemplate.opsForValue().get(namespace + email);
return validVerificationCode != null && validVerificationCode.equals(userInputVerificationCode);
}

@Override
public void delete(String email, EmailVerificationPurpose purpose) {
String namespace = purpose.getNamespace();
redisTemplate.delete(namespace + email);
}

}
Original file line number Diff line number Diff line change
@@ -1,27 +1,23 @@
package com.alzzaipo.common.email.domain;

import jakarta.validation.constraints.Pattern;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.validator.constraints.Length;

@Getter
@Setter
@NoArgsConstructor
public class EmailVerificationCode {

public static final int length = 8;
public static final int length = 8;

@Length(min = length, max = length)
@Pattern(message = "이메일 형식 오류", regexp = "^[A-Za-z0-9]{8}$")
private String emailVerificationCode;
@Length(min = length, max = length)
@Pattern(message = "이메일 형식 오류", regexp = "^[A-Za-z0-9]{8}$")
private String emailVerificationCode;

public EmailVerificationCode(String emailVerificationCode) {
this.emailVerificationCode = emailVerificationCode;
}
public EmailVerificationCode(String emailVerificationCode) {
this.emailVerificationCode = emailVerificationCode;
}

public String get() {
return emailVerificationCode;
}
public String get() {
return emailVerificationCode;
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.alzzaipo.common.email.port.out.verification;

import com.alzzaipo.common.email.domain.EmailVerificationPurpose;

public interface CheckEmailVerificationCodePort {

boolean check(String email, String verificationCode, EmailVerificationPurpose purpose);
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@

public interface SaveEmailVerificationCodePort {

void save(String email, String emailVerificationCode, String subject, EmailVerificationPurpose purpose);
void save(String email, String emailVerificationCode, EmailVerificationPurpose purpose);
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,26 +1,25 @@
package com.alzzaipo.member.adapter.in.web;

import com.alzzaipo.common.MemberPrincipal;
import com.alzzaipo.common.Id;
import com.alzzaipo.common.MemberPrincipal;
import com.alzzaipo.common.email.domain.Email;
import com.alzzaipo.common.email.domain.EmailVerificationCode;
import com.alzzaipo.common.email.domain.EmailVerificationPurpose;
import com.alzzaipo.member.adapter.in.web.dto.ChangeLocalAccountPasswordWebRequest;
import com.alzzaipo.member.adapter.in.web.dto.CheckLocalAccountEmailVerificationCodeRequest;
import com.alzzaipo.member.adapter.in.web.dto.EmailDto;
import com.alzzaipo.member.adapter.in.web.dto.EmailVerificationCodeDto;
import com.alzzaipo.member.adapter.in.web.dto.LocalAccountPasswordDto;
import com.alzzaipo.member.adapter.in.web.dto.RegisterLocalAccountWebRequest;
import com.alzzaipo.member.adapter.in.web.dto.SendSignUpEmailVerificationCodeWebRequest;
import com.alzzaipo.member.adapter.in.web.dto.UpdateMemberProfileWebRequest;
import com.alzzaipo.member.application.port.in.account.local.ChangeLocalAccountPasswordUseCase;
import com.alzzaipo.member.application.port.in.account.local.CheckEmailVerificationCodeQuery;
import com.alzzaipo.member.application.port.in.account.local.CheckLocalAccountEmailAvailableQuery;
import com.alzzaipo.member.application.port.in.account.local.CheckLocalAccountIdAvailableQuery;
import com.alzzaipo.member.application.port.in.account.local.RegisterLocalAccountUseCase;
import com.alzzaipo.member.application.port.in.account.local.SendSignUpEmailVerificationCodeUseCase;
import com.alzzaipo.member.application.port.in.account.local.SendUpdateEmailVerificationCodeUseCase;
import com.alzzaipo.member.application.port.in.account.local.VerifyEmailVerificationCodeUseCase;
import com.alzzaipo.member.application.port.in.account.local.SendUpdateLocalAccountEmailVerificationCodeUseCase;
import com.alzzaipo.member.application.port.in.account.local.VerifyLocalAccountPasswordQuery;
import com.alzzaipo.member.application.port.in.dto.ChangeLocalAccountPasswordCommand;
import com.alzzaipo.member.application.port.in.dto.CheckLocalAccountEmailVerificationCodeCommand;
import com.alzzaipo.member.application.port.in.dto.MemberProfile;
import com.alzzaipo.member.application.port.in.dto.RegisterLocalAccountCommand;
import com.alzzaipo.member.application.port.in.dto.SendSignUpEmailVerificationCodeCommand;
Expand Down Expand Up @@ -60,8 +59,8 @@ public class MemberController {
private final UpdateMemberProfileUseCase updateMemberProfileUseCase;
private final WithdrawMemberUseCase withdrawMemberUseCase;
private final SendSignUpEmailVerificationCodeUseCase sendSignUpEmailVerificationCodeUseCase;
private final VerifyEmailVerificationCodeUseCase verifyEmailVerificationCodeUseCase;
private final SendUpdateEmailVerificationCodeUseCase sendUpdateEmailVerificationCodeUseCase;
private final CheckEmailVerificationCodeQuery checkEmailVerificationCodeQuery;
private final SendUpdateLocalAccountEmailVerificationCodeUseCase sendUpdateLocalAccountEmailVerificationCodeUseCase;

@GetMapping("/register/verify-account-id")
public ResponseEntity<String> checkLocalAccountIdAvailability(
Expand All @@ -84,21 +83,23 @@ public ResponseEntity<String> checkLocalAccountEmailAvailability(@RequestParam("

@PostMapping("/register/send-verification-code")
public ResponseEntity<String> sendSignUpVerificationCode(
@Valid @RequestBody SendSignUpEmailVerificationCodeWebRequest dto) {
@Valid @RequestBody EmailDto emailDto) {

SendSignUpEmailVerificationCodeCommand command =
new SendSignUpEmailVerificationCodeCommand(dto.getAccountId(), dto.getEmail());
new SendSignUpEmailVerificationCodeCommand(emailDto.getEmail());

sendSignUpEmailVerificationCodeUseCase.sendSignUpEmailVerificationCode(command);
return ResponseEntity.ok().body("전송 완료");
}

@PostMapping("/register/validate-verification-code")
public ResponseEntity<String> validateVerificationCode(@Valid @RequestBody EmailVerificationCodeDto dto) {
EmailVerificationCode verificationCode = new EmailVerificationCode(dto.getVerificationCode());
boolean verified = verifyEmailVerificationCodeUseCase.verifyEmailVerificationCode(verificationCode,
EmailVerificationPurpose.SIGN_UP);
if (verified) {
public ResponseEntity<String> validateVerificationCode(
@Valid @RequestBody CheckLocalAccountEmailVerificationCodeRequest request) {

CheckLocalAccountEmailVerificationCodeCommand command = new CheckLocalAccountEmailVerificationCodeCommand(
request.getEmail(), request.getVerificationCode(), EmailVerificationPurpose.SIGN_UP);

if (checkEmailVerificationCodeQuery.checkEmailVerificationCode(command)) {
return ResponseEntity.ok().body("인증 성공");
}
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("인증 실패");
Expand Down Expand Up @@ -146,42 +147,43 @@ public ResponseEntity<String> findMemberNickname(@AuthenticationPrincipal Member
}

@GetMapping("/profile")
public ResponseEntity<MemberProfile> findMemberProfile(
@AuthenticationPrincipal MemberPrincipal principal) {
public ResponseEntity<MemberProfile> findMemberProfile(@AuthenticationPrincipal MemberPrincipal principal) {
MemberProfile memberProfile = findMemberProfileQuery.findMemberProfile(principal.getMemberId(),
principal.getCurrentLoginType());

return ResponseEntity.ok().body(memberProfile);
}

@PostMapping("/profile/update/send-verification-code")
public ResponseEntity<String> sendUpdateEmailVerificationCode(
@AuthenticationPrincipal MemberPrincipal principal,
@Valid @RequestBody EmailDto dto) {
sendUpdateEmailVerificationCodeUseCase.sendUpdateEmailVerificationCode(new Email(dto.getEmail()),
principal.getMemberId());
public ResponseEntity<String> sendUpdateEmailVerificationCode(@Valid @RequestBody EmailDto dto) {
sendUpdateLocalAccountEmailVerificationCodeUseCase.sendUpdateLocalAccountEmailVerificationCode(
new Email(dto.getEmail()));

return ResponseEntity.ok().body("전송 완료");
}

@PostMapping("/profile/update/validate-verification-code")
public ResponseEntity<String> validateRegisterVerificationCode(
@Valid @RequestBody EmailVerificationCodeDto dto) {
EmailVerificationCode emailVerificationCode = new EmailVerificationCode(dto.getVerificationCode());
if (!verifyEmailVerificationCodeUseCase.verifyEmailVerificationCode(emailVerificationCode,
EmailVerificationPurpose.UPDATE)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("인증 실패");
@Valid @RequestBody CheckLocalAccountEmailVerificationCodeRequest request) {

CheckLocalAccountEmailVerificationCodeCommand command = new CheckLocalAccountEmailVerificationCodeCommand(
request.getEmail(), request.getVerificationCode(), EmailVerificationPurpose.UPDATE);

if (checkEmailVerificationCodeQuery.checkEmailVerificationCode(command)) {
return ResponseEntity.ok().body("인증 성공");
}
return ResponseEntity.ok().body("인증 성공");
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("인증 실패");
}

@PutMapping("/profile/update")
public ResponseEntity<String> updateMemberProfile(@AuthenticationPrincipal MemberPrincipal principal,
@Valid @RequestBody UpdateMemberProfileWebRequest dto) {
@Valid @RequestBody UpdateMemberProfileWebRequest request) {

UpdateMemberProfileCommand command = new UpdateMemberProfileCommand(
principal.getMemberId(),
dto.getNickname(),
new Email(dto.getEmail()));
request.getNickname(),
new Email(request.getEmail()),
request.getVerificationCode());

updateMemberProfileUseCase.updateMemberProfile(command);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.alzzaipo.member.adapter.in.web.dto;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
public class CheckLocalAccountEmailVerificationCodeRequest {

@Email
@NotBlank
private String email;

@NotBlank
private String verificationCode;

}
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,7 @@ public class RegisterLocalAccountWebRequest {

@NotBlank
private String nickname;

@NotBlank
private String verificationCode;
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,10 @@ public class UpdateMemberProfileWebRequest {
private String nickname;

@Email
@NotBlank
private String email;

@NotBlank
private String verificationCode;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.alzzaipo.member.application.port.in.account.local;

import com.alzzaipo.member.application.port.in.dto.CheckLocalAccountEmailVerificationCodeCommand;

public interface CheckEmailVerificationCodeQuery {

boolean checkEmailVerificationCode(CheckLocalAccountEmailVerificationCodeCommand command);
}
Loading

0 comments on commit d0fba32

Please sign in to comment.