Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,7 @@ out/

### VS Code ###
.vscode/

### .env file ###
*.env

Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
package com.WhoIsRoom.WhoIs_Server.domain.auth.controller;

import com.WhoIsRoom.WhoIs_Server.domain.auth.dto.request.CodeCheckRequest;
import com.WhoIsRoom.WhoIs_Server.domain.auth.dto.request.MailRequest;
import com.WhoIsRoom.WhoIs_Server.domain.auth.dto.request.PasswordRequest;
import com.WhoIsRoom.WhoIs_Server.domain.auth.dto.request.RefreshTokenRequest;
import com.WhoIsRoom.WhoIs_Server.domain.auth.dto.response.ReissueResponse;
import com.WhoIsRoom.WhoIs_Server.domain.auth.service.JwtService;
import com.WhoIsRoom.WhoIs_Server.domain.auth.service.MailService;
import com.WhoIsRoom.WhoIs_Server.domain.user.service.UserService;
import com.WhoIsRoom.WhoIs_Server.global.common.resolver.CurrentUserId;
import com.WhoIsRoom.WhoIs_Server.global.common.response.BaseResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;

@Slf4j
@RestController
Expand All @@ -18,16 +22,44 @@
public class AuthController {

private final JwtService jwtService;
private final MailService mailService;
private final UserService userService;

@PostMapping("/logout")
public BaseResponse<Void> logout(HttpServletRequest request){
jwtService.logout(request);
public BaseResponse<Void> logout(HttpServletRequest request,
@RequestBody RefreshTokenRequest tokenRequest){
jwtService.logout(request, tokenRequest);
return BaseResponse.ok(null);
}

@PostMapping("/reissue")
public BaseResponse<Void> reissueTokens(HttpServletRequest request, HttpServletResponse response) {
jwtService.reissueTokens(request, response);
public BaseResponse<ReissueResponse> reissueTokens(@RequestBody RefreshTokenRequest tokenRequest) {
ReissueResponse response = jwtService.reissueTokens(tokenRequest);
return BaseResponse.ok(response);
}

@PostMapping("/email/send")
public BaseResponse<Void> sendAuthCodeMail(@RequestBody MailRequest request) {
mailService.sendMail(request);
return BaseResponse.ok(null);
}

@PostMapping("/email/validation")
public BaseResponse<Void> checkAuthCode(@RequestBody CodeCheckRequest request) {
mailService.checkAuthCode(request);
return BaseResponse.ok(null);
}

@PostMapping("/email/find-password")
public BaseResponse<Void> findPassword(@RequestBody MailRequest request) {
userService.sendNewPassword(request);
return BaseResponse.ok(null);
}
Comment on lines +53 to +57
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

비밀번호 찾기 엔드포인트에 추가 보안 검증이 필요합니다.

현재 구현은 이메일만으로 비밀번호를 재설정하므로, 이메일 주소만 알면 누구나 다른 사용자의 비밀번호를 변경할 수 있습니다. 다음 보안 강화를 고려해보세요:

  1. Rate limiting: 동일 이메일에 대한 비밀번호 재설정 요청 빈도를 제한 (예: 5분에 1회)
  2. 추가 인증 단계: 임시 비밀번호를 즉시 변경하지 않고, 이메일로 전송된 토큰/링크를 통한 검증 후 사용자가 직접 새 비밀번호를 설정하도록 변경
  3. 알림: 비밀번호 재설정 시 기존 세션 무효화 및 사용자에게 알림 전송

현재 프로젝트에 rate limiting 설정이 있는지 확인하려면 다음 스크립트를 실행하세요:


🏁 Script executed:

#!/bin/bash
# Rate limiting 관련 설정 확인

echo "=== Checking for rate limiting configurations ==="
rg -i "ratelimit|rate.limit|bucket4j|resilience4j" --type java --type yaml

echo -e "\n=== Checking for interceptor/filter configurations ==="
rg -i "interceptor|filter.*registration" --type java -A 3

Length of output: 105


비밀번호 재설정 엔드포인트에 필수 보안 조치 적용

  • 현재 이메일만으로 AuthController.findPassword (src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/controller/AuthController.java:53)에서 즉시 새 비밀번호가 발급되어, 악의적 요청 시 계정 탈취 위험이 있습니다.
  • 다음 보안 강화 필수 적용:
    1. Rate limiting (예: 동일 이메일 5분당 1회 제한)
    2. 이메일 토큰/링크 검증 후 사용자가 직접 비밀번호 재설정
    3. 기존 세션 무효화 및 사용자 알림
🤖 Prompt for AI Agents
In
src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/controller/AuthController.java
around lines 53-57, the findPassword endpoint currently issues a new password
immediately when given an email; change this to a secure reset flow: replace
immediate password issuance with generation of a single-use, time-limited token
stored server-side (or in a signed JWT) and send a reset link containing that
token to the user's email; enforce rate limiting per email (e.g., allow one
token request per 5 minutes) before generating a token; implement a separate
POST endpoint to accept the token and new password, verify token validity and
expiry, set the new password there, invalidate all existing sessions/tokens for
that user upon successful reset, and send an email notification of the password
change; update UserService to support token creation/verification, rate-limiting
checks, session invalidation, and notification.


@PatchMapping("/password")
public BaseResponse<Void> updatePassword(@CurrentUserId Long userId,
@RequestBody PasswordRequest request) {
userService.updateMyPassword(userId, request);
return BaseResponse.ok(null);
}
Comment on lines +41 to 64
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

요청 바디에 @Valid 어노테이션을 추가하세요.

새로 추가된 모든 엔드포인트(sendAuthCodeMail, checkAuthCode, findPassword, updatePassword)의 요청 바디 파라미터에 @Valid를 붙이지 않으면 DTO에 선언된 유효성 검증 어노테이션(@NotNull, @Email 등)이 실행되지 않습니다.

다음과 같이 수정하세요:

 @PostMapping("/email/send")
-public BaseResponse<Void> sendAuthCodeMail(@RequestBody MailRequest request) {
+public BaseResponse<Void> sendAuthCodeMail(@Valid @RequestBody MailRequest request) {
     mailService.sendMail(request);
     return BaseResponse.ok(null);
 }

 @PostMapping("/email/validation")
-public BaseResponse<Void> checkAuthCode(@RequestBody CodeCheckRequest request) {
+public BaseResponse<Void> checkAuthCode(@Valid @RequestBody CodeCheckRequest request) {
     mailService.checkAuthCode(request);
     return BaseResponse.ok(null);
 }

 @PostMapping("/email/find-password")
-public BaseResponse<Void> findPassword(@RequestBody MailRequest request) {
+public BaseResponse<Void> findPassword(@Valid @RequestBody MailRequest request) {
     userService.sendNewPassword(request);
     return BaseResponse.ok(null);
 }

 @PatchMapping("/password")
 public BaseResponse<Void> updatePassword(@CurrentUserId Long userId,
-                                         @RequestBody PasswordRequest request) {
+                                         @Valid @RequestBody PasswordRequest request) {
     userService.updateMyPassword(userId, request);
     return BaseResponse.ok(null);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@PostMapping("/email/send")
public BaseResponse<Void> sendAuthCodeMail(@RequestBody MailRequest request) {
mailService.sendMail(request);
return BaseResponse.ok(null);
}
@PostMapping("/email/validation")
public BaseResponse<Void> checkAuthCode(@RequestBody CodeCheckRequest request) {
mailService.checkAuthCode(request);
return BaseResponse.ok(null);
}
@PostMapping("/email/find-password")
public BaseResponse<Void> findPassword(@RequestBody MailRequest request) {
userService.sendNewPassword(request);
return BaseResponse.ok(null);
}
@PatchMapping("/password")
public BaseResponse<Void> updatePassword(@CurrentUserId Long userId,
@RequestBody PasswordRequest request) {
userService.updateMyPassword(userId, request);
return BaseResponse.ok(null);
}
@PostMapping("/email/send")
public BaseResponse<Void> sendAuthCodeMail(@Valid @RequestBody MailRequest request) {
mailService.sendMail(request);
return BaseResponse.ok(null);
}
@PostMapping("/email/validation")
public BaseResponse<Void> checkAuthCode(@Valid @RequestBody CodeCheckRequest request) {
mailService.checkAuthCode(request);
return BaseResponse.ok(null);
}
@PostMapping("/email/find-password")
public BaseResponse<Void> findPassword(@Valid @RequestBody MailRequest request) {
userService.sendNewPassword(request);
return BaseResponse.ok(null);
}
@PatchMapping("/password")
public BaseResponse<Void> updatePassword(@CurrentUserId Long userId,
@Valid @RequestBody PasswordRequest request) {
userService.updateMyPassword(userId, request);
return BaseResponse.ok(null);
}
🤖 Prompt for AI Agents
In
src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/controller/AuthController.java
around lines 41 to 64, the request body parameters for sendAuthCodeMail,
checkAuthCode, findPassword and updatePassword are missing @Valid so DTO
validation annotations won't run; add the @Valid annotation to each @RequestBody
parameter (e.g. @Valid @RequestBody MailRequest request, etc.) and ensure the
appropriate Valid import (javax.validation.Valid or jakarta.validation.Valid
consistent with the project) is added to the imports.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.WhoIsRoom.WhoIs_Server.domain.auth.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class CodeCheckRequest {
private String email;
private String authCode;
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.WhoIsRoom.WhoIs_Server.domain.auth.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class MailRequest {
private String email;
Comment on lines +11 to +12
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

이메일 필드에 validation이 빠져있어요.

@NotNull을 import 했지만 실제로 필드에 적용하지 않았네요. 이메일은 인증 코드 발송과 비밀번호 찾기에 사용되는 중요한 필드인 만큼, @NotBlank@Email 검증을 추가하는 게 좋겠어요.

다음처럼 수정해보세요:

+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotBlank;
-import jakarta.validation.constraints.NotNull;
 
 @Getter
 @NoArgsConstructor(access = AccessLevel.PROTECTED)
 public class MailRequest {
+    @NotBlank(message = "이메일은 필수입니다")
+    @Email(message = "유효한 이메일 형식이 아닙니다")
     private String email;
 }
🤖 Prompt for AI Agents
In
src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/dto/request/MailRequest.java
around lines 11-12, the email field lacks validation annotations: add @NotBlank
and @Email to the private String email field (replace or supplement the unused
@NotNull import), and add the corresponding imports (e.g.,
javax.validation.constraints.NotBlank and javax.validation.constraints.Email or
the jakarta equivalents used in the project) so the field is validated for
presence and correct email format.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.WhoIsRoom.WhoIs_Server.domain.auth.dto.request;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class PasswordRequest {
private String prePassword;
private String newPassword;
}
Comment on lines +9 to +12
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

비밀번호 필드에 validation 어노테이션이 빠져있어요.

prePasswordnewPassword 필드에 @NotBlank 같은 검증 어노테이션이 없네요. 비밀번호는 보안에 민감한 데이터인 만큼, 빈 값이나 null이 들어오지 않도록 validation을 추가하는 게 좋겠어요. 필요하다면 @Pattern으로 비밀번호 강도 정책도 적용할 수 있습니다.

다음 diff를 적용해서 기본 검증을 추가해보세요:

 package com.WhoIsRoom.WhoIs_Server.domain.auth.dto.request;
 
+import jakarta.validation.constraints.NotBlank;
 import lombok.AccessLevel;
 import lombok.Getter;
 import lombok.NoArgsConstructor;
 
 @Getter
 @NoArgsConstructor(access = AccessLevel.PROTECTED)
 public class PasswordRequest {
+    @NotBlank(message = "이전 비밀번호는 필수입니다")
     private String prePassword;
+    @NotBlank(message = "새 비밀번호는 필수입니다")
     private String newPassword;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public class PasswordRequest {
private String prePassword;
private String newPassword;
}
package com.WhoIsRoom.WhoIs_Server.domain.auth.dto.request;
import jakarta.validation.constraints.NotBlank;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class PasswordRequest {
@NotBlank(message = "이전 비밀번호는 필수입니다")
private String prePassword;
@NotBlank(message = "새 비밀번호는 필수입니다")
private String newPassword;
}
🤖 Prompt for AI Agents
In
src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/dto/request/PasswordRequest.java
around lines 9 to 12, the password fields lack validation annotations; add
javax.validation constraints to both fields (e.g., @NotBlank on prePassword and
newPassword) and optionally a @Pattern on newPassword to enforce strength, and
import the necessary validation annotations (and update any
Lombok/getters/setters if needed) so incoming requests are validated for
non-null/non-empty values.

Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.WhoIsRoom.WhoIs_Server.domain.auth.dto.request;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class RefreshTokenRequest {
private String refreshToken;
}
Comment on lines +9 to +11
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

refreshToken 필드에 validation을 추가해주세요.

토큰 재발급과 로그아웃에 사용되는 중요한 필드인데 validation이 없네요. @NotBlank를 추가해서 빈 값이나 null이 들어오는 걸 방지하는 게 좋겠어요.

 package com.WhoIsRoom.WhoIs_Server.domain.auth.dto.request;
 
+import jakarta.validation.constraints.NotBlank;
 import lombok.AccessLevel;
 import lombok.Getter;
 import lombok.NoArgsConstructor;
 
 @Getter
 @NoArgsConstructor(access = AccessLevel.PROTECTED)
 public class RefreshTokenRequest {
+    @NotBlank(message = "리프레시 토큰은 필수입니다")
     private String refreshToken;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public class RefreshTokenRequest {
private String refreshToken;
}
package com.WhoIsRoom.WhoIs_Server.domain.auth.dto.request;
import jakarta.validation.constraints.NotBlank;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class RefreshTokenRequest {
@NotBlank(message = "리프레시 토큰은 필수입니다")
private String refreshToken;
}
🤖 Prompt for AI Agents
In
src/main/java/com/WhoIsRoom/WhoIs_Server/domain/auth/dto/request/RefreshTokenRequest.java
around lines 9 to 11, the refreshToken field lacks validation; add the
javax.validation.constraints.NotBlank annotation to the refreshToken field to
prevent null or empty values, and add the corresponding import statement; ensure
the DTO is used in controller methods validated by @Valid (or class-level
validation enabled) so the constraint is enforced at request binding.

Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.WhoIsRoom.WhoIs_Server.domain.auth.dto.response;

import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
public class ReissueResponse {
private String accessToken;
private String refreshToken;
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,6 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo
jwtService.storeRefreshToken(refreshToken);
log.info("[CustomAuthenticationSuccessHandler], refreshToken={}", refreshToken);

jwtService.sendTokens(response, accessToken, refreshToken);

LoginResponse data = LoginResponse.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
package com.WhoIsRoom.WhoIs_Server.domain.auth.service;

import com.WhoIsRoom.WhoIs_Server.domain.auth.dto.request.RefreshTokenRequest;
import com.WhoIsRoom.WhoIs_Server.domain.auth.dto.response.LoginResponse;
import com.WhoIsRoom.WhoIs_Server.domain.auth.dto.response.ReissueResponse;
import com.WhoIsRoom.WhoIs_Server.domain.auth.exception.CustomAuthenticationException;
import com.WhoIsRoom.WhoIs_Server.domain.auth.exception.CustomJwtException;
import com.WhoIsRoom.WhoIs_Server.domain.auth.util.JwtUtil;
import com.WhoIsRoom.WhoIs_Server.global.common.redis.RedisService;
import com.WhoIsRoom.WhoIs_Server.global.common.response.BaseResponse;
import com.WhoIsRoom.WhoIs_Server.global.common.response.ErrorCode;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.time.Duration;

@Slf4j
Expand All @@ -37,27 +44,30 @@ public class JwtService {

private final RedisService redisService;
private final JwtUtil jwtUtil;
private final ObjectMapper objectMapper;

public void logout(HttpServletRequest request) {
public void logout(HttpServletRequest request, RefreshTokenRequest tokenRequest) {
String accessToken = jwtUtil.extractAccessToken(request)
.orElseThrow(() -> new CustomAuthenticationException(ErrorCode.SECURITY_INVALID_ACCESS_TOKEN));
String refreshToken = jwtUtil.extractRefreshToken(request)
.orElseThrow(() -> new CustomAuthenticationException(ErrorCode.SECURITY_INVALID_REFRESH_TOKEN));

String refreshToken = tokenRequest.getRefreshToken();
jwtUtil.validateToken(refreshToken);
if (!"refresh".equals(jwtUtil.getTokenType(refreshToken))) {
throw new CustomJwtException(ErrorCode.INVALID_TOKEN_TYPE);
}

deleteRefreshToken(refreshToken);
//access token blacklist 처리 -> 로그아웃한 사용자가 요청 시 access token이 redis에 존재하면 jwtAuthenticationFilter에서 인증처리 거부
invalidAccessToken(accessToken);
}

public void reissueTokens(HttpServletRequest request, HttpServletResponse response) {
String refreshToken = jwtUtil.extractRefreshToken(request)
.orElseThrow(() -> new CustomAuthenticationException(ErrorCode.SECURITY_INVALID_REFRESH_TOKEN));

public ReissueResponse reissueTokens(RefreshTokenRequest tokenRequest) {
String refreshToken = tokenRequest.getRefreshToken();
jwtUtil.validateToken(refreshToken);
if (!"refresh".equals(jwtUtil.getTokenType(refreshToken))) {
throw new CustomJwtException(ErrorCode.INVALID_TOKEN_TYPE);
}
reissueAndSendTokens(response, refreshToken);
return reissueAndSendTokens(refreshToken);
}

public void checkLogout(String accessToken) {
Expand All @@ -83,7 +93,7 @@ private void invalidAccessToken(String accessToken) {
Duration.ofMillis(ACCESS_TOKEN_EXPIRED_IN));
}

private void reissueAndSendTokens(HttpServletResponse response, String refreshToken) {
private ReissueResponse reissueAndSendTokens(String refreshToken) {

// 새로운 Refresh Token 발급
String reissuedAccessToken = jwtUtil.createAccessToken(jwtUtil.getUserId(refreshToken), jwtUtil.getProviderId(refreshToken), jwtUtil.getRole(refreshToken), jwtUtil.getName(refreshToken));
Expand All @@ -95,12 +105,9 @@ private void reissueAndSendTokens(HttpServletResponse response, String refreshTo
// 기존 Refresh Token 폐기 (DB나 Redis에서 삭제)
deleteRefreshToken(refreshToken);

sendTokens(response, reissuedAccessToken, reissuedRefreshToken);
}

public void sendTokens(HttpServletResponse response, String accessToken,
String refreshToken) {
response.setHeader(ACCESS_HEADER, BEARER_PREFIX + accessToken);
response.setHeader(REFRESH_HEADER, BEARER_PREFIX + refreshToken);
return ReissueResponse.builder()
.accessToken(reissuedAccessToken)
.refreshToken(reissuedRefreshToken)
.build();
}
}
Loading