[feat] 소셜 로그인 사용자 정보 조회 api 개발 및 소셜 로그인 리다이렉트 분기 처리#177
Conversation
WalkthroughOAuth2 소셜 로그인 신규 사용자 여부 확인을 위한 신규 REST 엔드포인트( Changes
Sequence Diagram(s)sequenceDiagram
participant Frontend
participant CustomOAuth2UserController
participant UserRepository
participant JwtUtil
Frontend->>CustomOAuth2UserController: POST /oauth2/users {oauth2Id}
CustomOAuth2UserController->>UserRepository: findByOauth2Id(oauth2Id)
alt User exists
CustomOAuth2UserController->>JwtUtil: generateAccessToken(userId)
CustomOAuth2UserController-->>Frontend: {token, isNewUser=false}
else User not exists
CustomOAuth2UserController->>JwtUtil: generateSignupToken(oauth2Id)
CustomOAuth2UserController-->>Frontend: {token, isNewUser=true}
end
sequenceDiagram
participant OAuth2Provider
participant CustomSuccessHandler
participant HttpServletResponse
OAuth2Provider->>CustomSuccessHandler: OAuth2 인증 성공 콜백
CustomSuccessHandler->>CustomSuccessHandler: 신규/기존 사용자 판별
alt 신규 사용자
CustomSuccessHandler->>HttpServletResponse: Set-Cookie(signupToken)
CustomSuccessHandler->>HttpServletResponse: Redirect /signup/genre
else 기존 사용자
CustomSuccessHandler->>HttpServletResponse: Set-Cookie(accessToken)
CustomSuccessHandler->>HttpServletResponse: Redirect /feed
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Assessment against linked issues
Assessment against linked issues: Out-of-scope changes
Poem
Note 🔌 MCP (Model Context Protocol) integration is now available in Early Access!Pro users can now connect to remote MCP servers under the Integrations page to get reviews and chat conversations that understand additional development context. ✨ Finishing Touches
🧪 Generate unit tests
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (11)
src/main/java/konkuk/thip/common/security/constant/AuthParameters.java (1)
15-16: 리다이렉트 URL 하드코딩 → 환경설정 외부화 권장경로가 FE 라우팅/도메인 변화에 민감하므로 properties로 분리하는 편이 안전합니다. 예:
thip.oauth2.redirect.signup-path,...home-path를@ConfigurationProperties로 주입 후 사용.예시:
thip: oauth2: redirect: signup-path: "/signup/genre" home-path: "/feed"src/main/java/konkuk/thip/config/SecurityConfig.java (1)
108-108: CORS exposedHeaders의 Authorization 노출 필요성 재검토토큰을 응답 본문/쿠키로 전달한다면
Authorization응답 헤더 노출은 불필요할 수 있습니다. 실제로 응답 헤더에 Authorization을 사용하지 않는다면 제거하여 최소권한 원칙을 지키는 것을 권장합니다.- config.setExposedHeaders(Collections.singletonList(JWT_HEADER_KEY.getValue())); + // Authorization 헤더를 응답에 노출할 필요가 없으면 제거 + // config.setExposedHeaders(Collections.singletonList(JWT_HEADER_KEY.getValue()));src/main/java/konkuk/thip/common/security/oauth2/OAuth2TokenResponse.java (1)
1-10: 응답 스키마 확장/명확화 제안
token이 액세스 토큰/가입(임시) 토큰 중 무엇인지가isNewUser로 암묵적으로 결정됩니다. 명시적tokenType(e.g.,ACCESS,SIGNUP) 또는 필드 분리(accessToken/signupToken)가 클라이언트 혼동을 줄입니다.- 만료 정보(
expiresIn또는expiresAt)를 포함하면 클라이언트 캐싱/갱신 전략에 유용합니다.- boolean 컴포넌트명은
newUser로 두면 가독성이 조금 더 좋습니다(레코드 JSON 필드가isNewUser→newUser로 단순화).-public record OAuth2TokenResponse( - String token, - boolean isNewUser -) { - public static OAuth2TokenResponse of(String token, boolean isNewUser) { - return new OAuth2TokenResponse(token, isNewUser); - } -} +public record OAuth2TokenResponse( + String token, + String tokenType, // "ACCESS" | "SIGNUP" + Long expiresIn, // seconds, optional + boolean newUser +) { + public static OAuth2TokenResponse of(String token, String tokenType, Long expiresIn, boolean newUser) { + return new OAuth2TokenResponse(token, tokenType, expiresIn, newUser); + } +}src/main/java/konkuk/thip/user/application/port/in/UserSignupUseCase.java (1)
4-4: 계약(Contract) 명세 보강 제안
UserSignupResult가 포함하는 의미(영속화된 userId, 발급된 accessToken의 유효시간/스코프 등)를 Javadoc에 명시하면 포트 사용자가 의도를 명확히 파악할 수 있습니다.src/main/java/konkuk/thip/user/application/port/in/dto/UserSignupResult.java (1)
3-10: 토큰 포함 DTO 반환 시 민감정보 취급 주의 및 문서화 제안
- 이 DTO는 JWT를 포함하므로 서버/APM/액세스 로그에 직렬화되지 않도록 로깅 정책을 점검해 주세요.
- OpenAPI 스키마에 간단한 설명/예시를 추가하면 클라이언트 연동에 도움이 됩니다.
src/main/java/konkuk/thip/user/application/service/UserSignupService.java (2)
29-38: 트랜잭션 경계 내부에서의 토큰 생성 시나리오 재검토현재 @transactional 메서드 내부에서 사용자 저장 직후 토큰을 생성/반환합니다. 커밋 실패 시 이미 발급된 토큰이 외부에 전달될 수 있는(드물지만) 타이밍 이슈가 존재합니다. 아래 선택지 중 하나를 고려해 주세요.
- TransactionSynchronizationManager.afterCommit에서 토큰 생성/세팅
- 토큰 생성 책임을 트랜잭션 외부(상위 계층)로 이동
- 토큰 생성 시 예외가 트랜잭션 롤백을 유발하지 않도록 별도 예외 처리
필수는 아니지만, 일관성 보장 관점에서 권장됩니다.
16-16: 애플리케이션 계층에서 adapter.out 패키지 의존 (레이어링 위반 가능성)
import static konkuk.thip.user.adapter.out.jpa.UserRole.USER;는 application → adapter(out) 방향 의존으로 보입니다. 도메인/공용 모델을 참조하도록 리팩터링하거나, 매핑은 어댑터 계층으로 밀어내는 것을 권장합니다.src/main/java/konkuk/thip/common/security/oauth2/CustomOAuth2UserController.java (2)
3-15: 불필요한 HttpServletResponse/IOException 제거
HttpServletResponse response파라미터와throws IOException은 사용되지 않습니다. 정리해 주세요.적용 diff:
-import jakarta.servlet.http.HttpServletResponse; @@ - @RequestParam("oauth2Id") String oauth2Id, - HttpServletResponse response) throws IOException { + @RequestParam("oauth2Id") String oauth2Id) {또는 POST로 전환 시 함께 제거 가능합니다.
Also applies to: 29-33
28-44: 토큰 응답의 캐시 방지 헤더 적용 권장토큰을 바디로 반환하므로 중간 캐시 방지가 필요합니다. 전역 보안 헤더에서 no-store 적용을 권장합니다.
전역 설정 예시:
http.headers(headers -> headers.cacheControl(withDefaults())); // Cache-Control: no-storesrc/main/java/konkuk/thip/common/security/oauth2/CustomSuccessHandler.java (2)
24-24: 쿠키 만료와 토큰 만료(30일) 간의 불일치 정리 제안쿠키는 1일, JWT는 30일로 만료 정책이 불일치합니다. 의도라면 OK지만, 사용자 경험(빈번한 재로그인)과 보안정책을 고려해 정렬하거나 프로퍼티로 관리하는 것을 권장합니다.
아래처럼 Duration을 사용하고 의미를 명확히 하면 좋습니다:
- private static final int COOKIE_MAX_AGE = 60 * 60 * 24; // 1일 + private static final Duration COOKIE_MAX_AGE = Duration.ofDays(1); // 1일위 변경 시 addTokenCookie에서 maxAge(COOKIE_MAX_AGE)로 그대로 사용할 수 있습니다(이전 코멘트의 ResponseCookie 예시 반영).
9-11: @slf4j 사용 여부 정리(미사용 애노테이션)현재 로그 호출이 없어 @slf4j가 불필요합니다. 유지하려면 민감정보 없이 최소한의 브랜치 로그를 남기거나, 사용하지 않는다면 제거하세요.
예: 민감정보 없이 흐름만 기록
if (oAuth2User.isNewUser()) { + log.info("OAuth2 success: new user → redirect signup"); ... } else { + log.info("OAuth2 success: existing user → redirect home"); ... }토큰/식별자(oauth2Id, userId) 등 PII/민감정보는 로깅 금지 권장.
Also applies to: 19-19
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (11)
build.gradle(1 hunks)src/main/java/konkuk/thip/common/security/constant/AuthParameters.java(1 hunks)src/main/java/konkuk/thip/common/security/oauth2/CustomOAuth2UserController.java(1 hunks)src/main/java/konkuk/thip/common/security/oauth2/CustomSuccessHandler.java(1 hunks)src/main/java/konkuk/thip/common/security/oauth2/OAuth2TokenResponse.java(1 hunks)src/main/java/konkuk/thip/config/SecurityConfig.java(3 hunks)src/main/java/konkuk/thip/user/adapter/in/web/UserCommandController.java(1 hunks)src/main/java/konkuk/thip/user/adapter/in/web/response/UserSignupResponse.java(1 hunks)src/main/java/konkuk/thip/user/application/port/in/UserSignupUseCase.java(1 hunks)src/main/java/konkuk/thip/user/application/port/in/dto/UserSignupResult.java(1 hunks)src/main/java/konkuk/thip/user/application/service/UserSignupService.java(2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/main/java/konkuk/thip/common/security/oauth2/CustomSuccessHandler.java (2)
src/main/java/konkuk/thip/common/security/oauth2/CustomOAuth2UserService.java (1)
Slf4j(20-57)src/main/java/konkuk/thip/common/security/util/JwtUtil.java (1)
Slf4j(20-89)
🪛 ast-grep (0.38.6)
src/main/java/konkuk/thip/common/security/oauth2/CustomSuccessHandler.java
[warning] 58-58: The application does not appear to verify inbound requests which can lead to a Cross-site request forgery (CSRF) vulnerability. If the application uses cookie-based authentication, an attacker can trick users into sending authenticated HTTP requests without their knowledge from any arbitrary domain they visit. To prevent this vulnerability start by identifying if the framework or library leveraged has built-in features or offers plugins for CSRF protection. CSRF tokens should be unique and securely random. The Synchronizer Token or Double Submit Cookie patterns with defense-in-depth mechanisms such as the sameSite cookie flag can help prevent CSRF. For more information, see: [Cross-site request forgery prevention](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Req\ uest_Forgery_Prevention_Cheat_Sheet.html).
Context: response.addCookie(cookie);
Note: [CWE-352] Cross-Site Request Forgery (CSRF). [REFERENCES]
- https://stackoverflow.com/questions/42717210/samesite-cookie-in-java-application
(cookie-missing-samesite-java)
[warning] 58-58: A cookie was detected without setting the 'HttpOnly' flag. The 'HttpOnly' flag for cookies instructs the browser to forbid client-side scripts from reading the cookie. Set the 'HttpOnly' flag by calling 'cookie.setHttpOnly(true);
Context: response.addCookie(cookie);
Note: [CWE-1004] Sensitive Cookie Without 'HttpOnly' Flag. [REFERENCES]
- https://owasp.org/www-community/HttpOnly
(cookie-missing-httponly-java)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (6)
src/main/java/konkuk/thip/config/SecurityConfig.java (1)
27-28: 상수 직접 문자열 의존 제거 OK
JWT_HEADER_KEY상수 사용으로 일관성과 오타 방지 측면에서 개선되었습니다.src/main/java/konkuk/thip/user/application/port/in/UserSignupUseCase.java (1)
8-8: 반환형 변경 적용 현황 점검 및 테스트·문서 검토 요청
UserSignupUseCase반환형을Long에서UserSignupResult로 변경한 사항이 아래 위치에 모두 반영되었습니다:
- 서비스 구현
- src/main/java/konkuk/thip/user/application/service/UserSignupService.java (implements UserSignupUseCase,
return UserSignupResult.of(...))- 웹 어댑터(컨트롤러)
- src/main/java/konkuk/thip/user/adapter/in/web/UserCommandController.java (
UserSignupResponse.of(userSignupUseCase.signup(...)))- 포트용 DTO
- src/main/java/konkuk/thip/user/application/port/in/dto/UserSignupResult.java
- 응답 매핑용 레코드
- src/main/java/konkuk/thip/user/adapter/in/web/response/UserSignupResponse.java
기존
Long반환을 기대하던 로직은 더 이상 코드베이스에서 발견되지 않습니다.
▶ 테스트 코드(src/test/java)에서 오래된 반환형을 참조하는 부분이 없는지, Javadoc/README 등 문서에 변경된 계약 사항이 명시됐는지 추가 검토 부탁드립니다.src/main/java/konkuk/thip/user/adapter/in/web/UserCommandController.java (1)
41-43: 컨트롤러에서 토큰 생성 제거: 책임 분리 👍토큰 생성 책임을 서비스로 이동시켜 컨트롤러를 얇게 유지한 점이 좋습니다. 응답 일관성도 개선되었습니다.
src/main/java/konkuk/thip/common/security/oauth2/CustomSuccessHandler.java (3)
32-36: onAuthenticationSuccess 시그니처/흐름 변경 LGTM메서드 포맷팅과 분기 로직 구조는 명확합니다. 이후 필터 레벨에서 쿠키에서 토큰을 읽어 인증 컨텍스트를 재구성하는지(헤더 기반만 지원하지 않는지)만 확인 부탁드립니다.
4-4: 임포트/정적 임포트 변경 사항 문제 없음변경된 임포트/정적 임포트는 현재 코드 흐름과 일치합니다.
Also applies to: 17-17
54-60: CSRF 보호 설정: 비활성화 발견됨
Cookie 기반 인증 사용 시 CSRF 방어가 필수입니다. 현재 SecurityConfig에서.csrf(AbstractHttpConfigurer::disable)로 CSRF가 완전히 비활성화되어 있습니다.확인 위치:
- 파일:
src/main/java/konkuk/thip/config/SecurityConfig.java- 라인: 62 (
.csrf(AbstractHttpConfigurer::disable))제안하는 수정 예시:
- .csrf(AbstractHttpConfigurer::disable) + .csrf(csrf -> csrf + .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) + )
CookieCsrfTokenRepository를 임포트하세요:
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;- 프론트엔드가 발급된 CSRF 토큰을 요청 헤더 또는 쿠키로 전달하도록 구현해야 합니다.
Likely an incorrect or invalid review comment.
| // Spring Boot Actuator | ||
| implementation 'org.springframework.boot:spring-boot-starter-actuator' | ||
| } |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Actuator 도입 시 엔드포인트 노출/보호 설정 확인 필요
현재 SecurityConfig에서 /actuator/health만 permitAll 처리되어 있어 기본적으로는 안전합니다. 다만 운영/스테이징 전역 설정에서 노출 범위를 명시적으로 health만 포함하도록 고정해 주세요.
다음 스크립트로 설정 여부를 점검할 수 있습니다:
권장 설정 예시(운영/스테이징):
management:
endpoints:
web:
base-path: /actuator
exposure:
include: "health"
endpoint:
health:
probes:
enabled: true
show-details: "never"🏁 Script executed:
#!/bin/bash
# Actuator 노출 설정 점검
echo "== application.*(yml|properties)에서 management 설정 검색 =="
fd -a -t f 'application.*\.(ya?ml|properties)$' | xargs -I{} sh -c 'echo "--- {} ---"; rg -n "management\." "{}" || true'
echo
echo "== 코드에서 actuator 경로 화이트리스트 점검 =="
rg -n "/actuator" -S src/main/java || trueLength of output: 475
Actuator 노출 설정: 운영/스테이징 환경에 명시적 관리 엔드포인트 노출 범위 추가 필요
설정 파일(application-*.yml.properties)에서 management.endpoints.web.exposure.include가 누락되어 있어, 기본값(health, info) 중 info가 의도치 않게 노출될 수 있습니다. SecurityConfig에서는 이미 /actuator/health만 permitAll 처리(코드: src/main/java/konkuk/thip/config/SecurityConfig.java:52)되어 있으나, 운영·스테이징 전역 설정에서도 오직 health만 노출하도록 아래 내용을 반영해 주세요.
• 대상 파일
src/main/resources/application-production.ymlsrc/main/resources/application-staging.yml
• 권장 설정 예시
management:
endpoints:
web:
base-path: /actuator # 기본값과 동일하지만 명시 권장
exposure:
include: health # health만 노출
endpoint:
health:
show-details: never # 상세 정보 숨기기
probes:
enabled: true # 상태 프로브 사용🤖 Prompt for AI Agents
In src/main/resources/application-production.yml and application-staging.yml,
add explicit actuator exposure settings to restrict exposed endpoints to only
health. Include the management.endpoints.web.base-path set to /actuator,
management.endpoints.web.exposure.include set to health, and configure
management.endpoint.health to hide details and enable probes as shown in the
recommended YAML snippet. This ensures only the health endpoint is exposed in
production and staging environments.
| public BaseResponse<OAuth2TokenResponse> checkUserExists( | ||
| @Parameter(description = "소셜 로그인 ID (형식: {provider}_{식별자 ID})", example = "kakao_1234567890") | ||
| @RequestParam("oauth2Id") String oauth2Id, | ||
| HttpServletResponse response) throws IOException { | ||
| return userJpaRepository.findByOauth2Id(oauth2Id) | ||
| .map(user -> { | ||
| // 기존 유저: AccessToken 발급 | ||
| String accessToken = jwtUtil.createAccessToken(user.getUserId()); | ||
| return BaseResponse.ok(OAuth2TokenResponse.of(accessToken,false)); | ||
| }) | ||
| .orElseGet(() -> { | ||
| // 신규 유저: SignupToken 발급 | ||
| String tempToken = jwtUtil.createSignupToken(oauth2Id); | ||
| return BaseResponse.ok(OAuth2TokenResponse.of(tempToken, true)); | ||
| }); |
There was a problem hiding this comment.
크리티컬: 소셜 공급자 토큰 검증 없이 oauth2Id만으로 액세스/가입 토큰 발급
현재 구현은 임의의 oauth2Id를 넘기면:
- 기존 유저: 해당 유저의 AccessToken 발급
- 미존재 유저: SignupToken 발급
즉, 공격자가 타인의 oauth2Id를 알고 있으면 그 계정의 AccessToken을 획득할 수 있습니다. 이는 임퍼소네이션 취약점입니다. 아래를 최소 요건으로 권장합니다.
필수 조치:
- 클라이언트가 공급자 액세스 토큰(또는 ID 토큰)을 제출하도록 요구
- 서버에서 공급자 API로 토큰 유효성 검증 및 사용자 식별자 조회
- 공급자에서 확인된 식별자와 클라이언트가 보낸
oauth2Id(또는 서버 파생값)를 대조하여 불일치 시 401/403 반환 - 유저 존재 여부 판단은 “검증된 식별자”만 사용
추가 보강:
- 엔드포인트를 POST로 전환하고 바디(JSON)로 입력 받기(쿼리스트링/로그 노출 방지)
- 레이트 리밋 및 감사 로깅(토큰/식별자 값 자체는 마스킹)
- SignupToken은 짧은 TTL/스코프 제한(회원가입 전용)
원하는 경우, 카카오/네이버/구글 각각의 검증 서비스 스켈레톤 코드를 제공하겠습니다.
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/common/security/oauth2/CustomOAuth2UserController.java
around lines 29 to 43, the current method issues access or signup tokens based
solely on the provided oauth2Id without verifying the social provider's token,
causing a critical impersonation vulnerability. To fix this, change the endpoint
to accept a POST request with a JSON body containing both the oauth2Id and the
provider's access or ID token. Implement server-side verification of the
provider token by calling the provider's API to confirm its validity and
retrieve the user identifier. Compare the verified identifier with the provided
oauth2Id and return a 401 or 403 error if they do not match. Only after
successful verification should you check user existence and issue tokens.
Additionally, consider adding rate limiting, audit logging with masking, and
ensure signup tokens have short TTL and limited scope.
| @Value("${server.web-redirect-url}") | ||
| private String webRedirectUrl; |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
webRedirectUrl 프로퍼티 존재/값 검증
외부 설정값에 의존하므로 프로퍼티 누락/오타 시 런타임 실패합니다. 또한 REDIRECT_*와의 문자열 결합 시 슬래시 중복/누락 여부를 확인하세요.
다음 스크립트로 프로퍼티 정의와 REDIRECT_* 상수 값을 확인해 주세요(선행/후행 슬래시 일관성):
🏁 Script executed:
#!/bin/bash
# server.web-redirect-url 정의 확인
rg -n --pretty $'server.web-redirect-url'
# REDIRECT_* 상수 정의 확인
rg -n --pretty $'REDIRECT_SIGNUP_URL|REDIRECT_HOME_URL' src/main/javaLength of output: 671
server.web-redirect-url 프로퍼티 추가 및 슬래시 일관성 검증 필요
application.yml(또는 application.properties)에 server.web-redirect-url 설정이 정의되어 있지 않습니다. 런타임 에러 방지를 위해 설정 파일에 해당 프로퍼티를 추가해 주세요.
또한, AuthParameters에 선언된 상수
REDIRECT_SIGNUP_URL("/signup/genre")REDIRECT_HOME_URL("/feed")
와 결합할 때webRedirectUrl값이 끝에/를 포함하는지- 결합 시 앞뒤로 슬래시가 중복(
//)되거나 누락되지 않는지
아래 지점을 확인 및 수정해 주시기 바랍니다.
• src/main/resources/application.yml (또는 application.properties)에 server.web-redirect-url 추가
• src/main/java/konkuk/thip/common/security/oauth2/CustomSuccessHandler.java
– webRedirectUrl + REDIRECT_* 조합 시 URL 포맷 일관성 검증
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/common/security/oauth2/CustomSuccessHandler.java
around lines 26 to 27, ensure the property server.web-redirect-url is defined in
application.yml or application.properties to prevent runtime errors.
Additionally, when concatenating webRedirectUrl with AuthParameters constants
like REDIRECT_SIGNUP_URL and REDIRECT_HOME_URL, verify and normalize the URL
format to avoid duplicate or missing slashes by trimming trailing slashes from
webRedirectUrl or leading slashes from the constants as needed for consistent
URL construction.
| if (oAuth2User.isNewUser()) { | ||
| // 신규 유저 - 회원가입용 임시 토큰 | ||
| String tempToken = jwtUtil.createSignupToken(loginUser.oauth2Id()); | ||
| response.setHeader(JWT_HEADER_KEY.getValue(), JWT_PREFIX.getValue() + tempToken); | ||
| writeResponse(response, BaseResponse.ok(oAuth2User.getLoginUser())); | ||
| return; | ||
| addTokenCookie(response, tempToken); | ||
| getRedirectStrategy().sendRedirect(request, response, webRedirectUrl + REDIRECT_SIGNUP_URL); | ||
| } else { | ||
| // 기존 유저 - 로그인용 액세스 토큰 | ||
| String accessToken = jwtUtil.createAccessToken(loginUser.userId()); | ||
| addTokenCookie(response, accessToken); | ||
| getRedirectStrategy().sendRedirect(request, response, webRedirectUrl + REDIRECT_HOME_URL); | ||
| } |
There was a problem hiding this comment.
💡 Verification agent
❓ Verification inconclusive
회원가입 토큰과 액세스 토큰의 쿠키 이름 분리 권장
현재 동일한 쿠키 이름(JWT_HEADER_KEY)을 사용하여 회원가입용 토큰/액세스 토큰을 모두 저장합니다. 서버 측 로직이 클레임 키로 구분하더라도, 엔드포인트의 기대치(액세스 토큰 필요)와 실제 전달값(회원가입 토큰)이 엇갈리면 예기치 않은 실패/혼동이 발생할 수 있습니다. 쿠키 키를 분리해 의도를 명확히 하세요.
호출부와 메서드 시그니처를 아래처럼 수정하는 것을 제안합니다:
- String tempToken = jwtUtil.createSignupToken(loginUser.oauth2Id());
- addTokenCookie(response, tempToken);
+ String tempToken = jwtUtil.createSignupToken(loginUser.oauth2Id());
+ addTokenCookie(response, tempToken, "signup_token");
getRedirectStrategy().sendRedirect(request, response, webRedirectUrl + REDIRECT_SIGNUP_URL);
...
- String accessToken = jwtUtil.createAccessToken(loginUser.userId());
- addTokenCookie(response, accessToken);
+ String accessToken = jwtUtil.createAccessToken(loginUser.userId());
+ addTokenCookie(response, accessToken, "access_token");
getRedirectStrategy().sendRedirect(request, response, webRedirectUrl + REDIRECT_HOME_URL);그리고 addTokenCookie 시그니처/구현도 함께 변경합니다(아래 라인 54-60에 적용):
- private void addTokenCookie(HttpServletResponse response, String token) {
+ private void addTokenCookie(HttpServletResponse response, String token, String cookieName) {
- ResponseCookie cookie = ResponseCookie.from(JWT_HEADER_KEY.getValue(), token)
+ ResponseCookie cookie = ResponseCookie.from(cookieName, token)
...
}참고: 회원가입 완료 시 signup_token 쿠키를 즉시 삭제(또는 access_token으로 교체)하는 정리 로직도 추가하세요.
회원가입 토큰 vs 액세스 토큰 쿠키 키 분리 필요
현재 CustomSuccessHandler에서 신규 가입용 토큰(signup)과 로그인용 액세스 토큰(access)을 동일한 쿠키 키(JWT_HEADER_KEY)로 저장하고 있어, 잘못된 쿠키가 전달될 경우 엔드포인트의 기대값과 실제 토큰이 달라 예기치 않은 실패가 발생할 수 있습니다. 별도 쿠키 이름으로 명확히 구분하세요.
수정 대상:
- 파일:
src/main/java/konkuk/thip/common/security/oauth2/CustomSuccessHandler.java
제안된 수정 예시:
@@ -41,7 +41,7 @@
if (oAuth2User.isNewUser()) {
// 신규 유저 - 회원가입용 임시 토큰
- addTokenCookie(response, tempToken);
+ addTokenCookie(response, tempToken, "signup_token");
getRedirectStrategy().sendRedirect(request, response, webRedirectUrl + REDIRECT_SIGNUP_URL);
} else {
// 기존 유저 - 로그인용 액세스 토큰
@@ -45,7 +45,7 @@
- addTokenCookie(response, accessToken);
+ addTokenCookie(response, accessToken, "access_token");
getRedirectStrategy().sendRedirect(request, response, webRedirectUrl + REDIRECT_HOME_URL);@@ -54,7 +54,7 @@
- private void addTokenCookie(HttpServletResponse response, String token) {
+ private void addTokenCookie(HttpServletResponse response, String token, String cookieName) {
ResponseCookie cookie = ResponseCookie.from(cookieName, token)
.httpOnly(true)
.secure(isProd)추가로, 회원가입 완료 후에는 signup_token 쿠키를 즉시 삭제하거나 access_token 쿠키로 교체하는 정리 로직도 구현해 주세요.
📝 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.
| if (oAuth2User.isNewUser()) { | |
| // 신규 유저 - 회원가입용 임시 토큰 | |
| String tempToken = jwtUtil.createSignupToken(loginUser.oauth2Id()); | |
| response.setHeader(JWT_HEADER_KEY.getValue(), JWT_PREFIX.getValue() + tempToken); | |
| writeResponse(response, BaseResponse.ok(oAuth2User.getLoginUser())); | |
| return; | |
| addTokenCookie(response, tempToken); | |
| getRedirectStrategy().sendRedirect(request, response, webRedirectUrl + REDIRECT_SIGNUP_URL); | |
| } else { | |
| // 기존 유저 - 로그인용 액세스 토큰 | |
| String accessToken = jwtUtil.createAccessToken(loginUser.userId()); | |
| addTokenCookie(response, accessToken); | |
| getRedirectStrategy().sendRedirect(request, response, webRedirectUrl + REDIRECT_HOME_URL); | |
| } | |
| if (oAuth2User.isNewUser()) { | |
| // 신규 유저 - 회원가입용 임시 토큰 | |
| String tempToken = jwtUtil.createSignupToken(loginUser.oauth2Id()); | |
| addTokenCookie(response, tempToken, "signup_token"); | |
| getRedirectStrategy().sendRedirect(request, response, webRedirectUrl + REDIRECT_SIGNUP_URL); | |
| } else { | |
| // 기존 유저 - 로그인용 액세스 토큰 | |
| String accessToken = jwtUtil.createAccessToken(loginUser.userId()); | |
| addTokenCookie(response, accessToken, "access_token"); | |
| getRedirectStrategy().sendRedirect(request, response, webRedirectUrl + REDIRECT_HOME_URL); | |
| } | |
| // … | |
| // 기존: private void addTokenCookie(HttpServletResponse response, String token) { | |
| private void addTokenCookie(HttpServletResponse response, String token, String cookieName) { | |
| ResponseCookie cookie = ResponseCookie.from(cookieName, token) | |
| .httpOnly(true) | |
| .secure(isProd) | |
| .path("/") | |
| .maxAge(expires) | |
| .build(); | |
| response.addHeader("Set-Cookie", cookie.toString()); | |
| } |
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/common/security/oauth2/CustomSuccessHandler.java
around lines 41 to 51, the signup token and access token are stored using the
same cookie key, which can cause token confusion. Fix this by defining and using
separate cookie keys for the signup token and the access token when calling
addTokenCookie. Additionally, implement logic to clear the signup token cookie
or replace it with the access token cookie immediately after signup completion
to avoid stale tokens.
| private void addTokenCookie(HttpServletResponse response, String token) { | ||
| Cookie cookie = new Cookie(JWT_HEADER_KEY.getValue(), token); | ||
| cookie.setSecure(true); | ||
| cookie.setPath("/"); | ||
| cookie.setMaxAge(COOKIE_MAX_AGE); | ||
| response.addCookie(cookie); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
쿠키 보안 플래그 보강 필요(HttpOnly, SameSite 미설정) 및 CSRF 노출 가능성
현재 토큰 쿠키에 Secure/Path/Max-Age만 설정되어 있고 HttpOnly와 SameSite가 설정되지 않았습니다. CSRF 관점에서 쿠키 기반 인증을 사용하면 공격면이 넓어집니다. 최소한 HttpOnly를 추가하고, SameSite 정책을 명시적으로 설정하세요. 크로스사이트 요청이 필요 없다면 Lax/Strict, 필요하다면 None; Secure(+ CSRF 토큰)로 가는 것을 권장합니다.
아래와 같이 Spring의 ResponseCookie를 사용하면 SameSite를 안전하게 설정할 수 있습니다.
- private void addTokenCookie(HttpServletResponse response, String token) {
- Cookie cookie = new Cookie(JWT_HEADER_KEY.getValue(), token);
- cookie.setSecure(true);
- cookie.setPath("/");
- cookie.setMaxAge(COOKIE_MAX_AGE);
- response.addCookie(cookie);
- }
+ private void addTokenCookie(HttpServletResponse response, String token) {
+ // SameSite 값은 서비스 특성에 맞게 "Lax" 또는 "None" 중 선택하세요.
+ ResponseCookie cookie = ResponseCookie.from(JWT_HEADER_KEY.getValue(), token)
+ .httpOnly(true)
+ .secure(true)
+ .path("/")
+ .maxAge(COOKIE_MAX_AGE) // Duration 사용 제안 (아래 별도 코멘트/디프 참고)
+ .sameSite("Lax")
+ .build();
+ response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
+ }추가 import가 필요합니다(파일 상단에 추가):
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import java.time.Duration;또한, 쿠키 인증을 계속 사용할 경우 CSRF 방어 전략을 반드시 병행하세요(예: CookieCsrfTokenRepository, Double Submit Cookie 패턴 등). SameSite=None을 택하면 CSRF 방어가 더 중요해집니다.
🧰 Tools
🪛 ast-grep (0.38.6)
[warning] 58-58: The application does not appear to verify inbound requests which can lead to a Cross-site request forgery (CSRF) vulnerability. If the application uses cookie-based authentication, an attacker can trick users into sending authenticated HTTP requests without their knowledge from any arbitrary domain they visit. To prevent this vulnerability start by identifying if the framework or library leveraged has built-in features or offers plugins for CSRF protection. CSRF tokens should be unique and securely random. The Synchronizer Token or Double Submit Cookie patterns with defense-in-depth mechanisms such as the sameSite cookie flag can help prevent CSRF. For more information, see: [Cross-site request forgery prevention](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Req\ uest_Forgery_Prevention_Cheat_Sheet.html).
Context: response.addCookie(cookie);
Note: [CWE-352] Cross-Site Request Forgery (CSRF). [REFERENCES]
- https://stackoverflow.com/questions/42717210/samesite-cookie-in-java-application
(cookie-missing-samesite-java)
[warning] 58-58: A cookie was detected without setting the 'HttpOnly' flag. The 'HttpOnly' flag for cookies instructs the browser to forbid client-side scripts from reading the cookie. Set the 'HttpOnly' flag by calling 'cookie.setHttpOnly(true);
Context: response.addCookie(cookie);
Note: [CWE-1004] Sensitive Cookie Without 'HttpOnly' Flag. [REFERENCES]
- https://owasp.org/www-community/HttpOnly
(cookie-missing-httponly-java)
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/common/security/oauth2/CustomSuccessHandler.java
around lines 54 to 60, the addTokenCookie method sets only Secure, Path, and
Max-Age on the cookie but lacks HttpOnly and SameSite attributes, increasing
CSRF risk. Replace the Cookie creation with Spring's ResponseCookie builder to
set HttpOnly(true) and specify SameSite policy (Lax or Strict if cross-site
requests are not needed, or None with Secure if they are). Add necessary imports
for ResponseCookie, HttpHeaders, and Duration at the top of the file. Also,
ensure CSRF protection strategies are implemented alongside cookie
authentication.
| public record UserSignupResponse( | ||
| Long userId, | ||
| String accessToken | ||
| ) { | ||
| public static UserSignupResponse of(UserSignupResult userSignupResult) { | ||
| return new UserSignupResponse(userSignupResult.userId(), userSignupResult.accessToken()); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
토큰 포함 응답의 캐시 방지 및 로깅 주의
이 응답에는 액세스 토큰이 포함됩니다. 중간 프록시/브라우저 캐시를 막기 위해 전역적으로 아래 보안 헤더 설정을 권장합니다.
- Cache-Control: no-store, Pragma: no-cache
또한 응답 바디 로깅이 활성화되어 있다면 토큰 마스킹을 적용해 주세요.
추가 예시(전역 Spring Security 설정):
http.headers(headers -> headers.cacheControl(cache -> {})); // no-store 기본 적용🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/user/adapter/in/web/response/UserSignupResponse.java
around lines 5 to 11, the response includes an access token which should not be
cached by browsers or proxies. To fix this, configure global security headers in
your Spring Security setup to include Cache-Control: no-store and Pragma:
no-cache to prevent caching. Additionally, if response body logging is enabled,
implement token masking to avoid exposing sensitive token data in logs.
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (3)
src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationFilter.java (1)
79-91: shouldNotFilter 범위를 더 안전하게: 정확 경로 + POST 메서드로 제한 제안현재 startsWith("/oauth2/users")는 /oauth2/users뿐 아니라 하위 경로 전부를 제외합니다. 의도된 엔드포인트가 POST /oauth2/users 하나라면:
- 정확 경로 매칭("/oauth2/users".equals(servletPath))
- HTTP 메서드 검증(POST만 허용)
을 함께 적용해 공격면을 줄이는 것을 권장합니다.예시(diff):
- protected boolean shouldNotFilter(HttpServletRequest request) { - String path = request.getRequestURI(); + protected boolean shouldNotFilter(HttpServletRequest request) { + String path = request.getServletPath(); // 컨텍스트 경로 영향 배제 + boolean oauth2Users = "/oauth2/users".equals(path) + && "POST".equalsIgnoreCase(request.getMethod()); // 화이트리스트 경로에 대해서는 JWT 필터 제외 - return path.startsWith("/swagger-ui") + return path.startsWith("/swagger-ui") || path.startsWith("/v3/api-docs") || path.startsWith("/api-docs") || path.startsWith("/oauth2/authorization") || path.startsWith("/login/oauth2/code") - || path.startsWith("/oauth2/users") + || oauth2Users ; }src/main/java/konkuk/thip/common/security/oauth2/dto/OAuth2TokenResponse.java (2)
3-10: 토큰 의미 명확화 제안: tokenType(ACCESS|SIGNUP) 및 expiresIn 추가현재 token 필드가 액세스 토큰/가입 임시 토큰을 상황에 따라 담아 혼동 여지가 있습니다. 클라이언트 분기 로직을 명확히 하기 위해 tokenType(ACCESS|SIGNUP)과 만료 정보(expiresIn)를 함께 제공하는 것을 권장합니다.
예시(diff):
-public record OAuth2TokenResponse( - String token, - boolean isNewUser -) { - public static OAuth2TokenResponse of(String token, boolean isNewUser) { - return new OAuth2TokenResponse(token, isNewUser); - } -} +public record OAuth2TokenResponse( + String token, + TokenType tokenType, // ACCESS or SIGNUP + boolean isNewUser, + long expiresIn // seconds +) { + public static OAuth2TokenResponse of(String token, TokenType tokenType, boolean isNewUser, long expiresIn) { + return new OAuth2TokenResponse(token, tokenType, isNewUser, expiresIn); + } +}추가 파일(예시):
// src/main/java/konkuk/thip/common/security/oauth2/dto/TokenType.java package konkuk.thip.common.security.oauth2.dto; public enum TokenType { ACCESS, SIGNUP }추가로 공개 API 문서화를 위해 @Schema(description=...) 등을 고려하면 사용처 이해가 쉬워집니다.
3-10: 불린 필드 명명 nit: isNewUser → newUser 고려record 컴포넌트가 isNewUser이면 접근자가 isNewUser()로 생성되어 JSON도 보통 "isNewUser"가 됩니다. 선호에 따라 "newUser"를 쓰면 더 자연스러운 경우가 많습니다. 기존과의 호환이 필요 없다면 다음처럼 단순화할 수 있습니다.
-public record OAuth2TokenResponse( - String token, - boolean isNewUser -) { - public static OAuth2TokenResponse of(String token, boolean isNewUser) { - return new OAuth2TokenResponse(token, isNewUser); - } -} +public record OAuth2TokenResponse( + String token, + boolean newUser +) { + public static OAuth2TokenResponse of(String token, boolean newUser) { + return new OAuth2TokenResponse(token, newUser); + } +}만약 응답 JSON 키를 "isNewUser"로 유지하고 싶다면, 다음과 같이 컴포넌트에 @JsonProperty("isNewUser")를 적용하는 방식도 있습니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationFilter.java(1 hunks)src/main/java/konkuk/thip/common/security/oauth2/CustomOAuth2UserController.java(1 hunks)src/main/java/konkuk/thip/common/security/oauth2/dto/OAuth2TokenRequest.java(1 hunks)src/main/java/konkuk/thip/common/security/oauth2/dto/OAuth2TokenResponse.java(1 hunks)
✅ Files skipped from review due to trivial changes (1)
- src/main/java/konkuk/thip/common/security/oauth2/dto/OAuth2TokenRequest.java
🚧 Files skipped from review as they are similar to previous changes (1)
- src/main/java/konkuk/thip/common/security/oauth2/CustomOAuth2UserController.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (3)
src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationFilter.java (2)
79-91: 화이트리스트에 /oauth2/users 추가 — 의도에 부합하며 적절합니다소셜 로그인 사용자인증 사전확인을 위한 비인증 접근이 요구되므로 필터 제외 대상에 추가한 결정은 타당합니다.
79-91: JWT 필터 제외 경로(/oauth2/users) 일치 확인 완료
- src/main/java/konkuk/thip/config/SecurityConfig.java
- WHITELIST 배열에
"/oauth2/users"가 포함되어 있으며.authorizeHttpRequests(auth -> auth.requestMatchers(WHITELIST).permitAll())처리됨- src/main/java/konkuk/thip/common/security/oauth2/CustomOAuth2UserController.java
@PostMapping("/oauth2/users")로 POST 엔드포인트가 정의되어 있음따라서 JWT 필터의
shouldNotFilter예외 설정과 보안 설정 및 컨트롤러 매핑이 모두 일치하며, 추가 조치는 필요 없습니다.src/main/java/konkuk/thip/common/security/oauth2/dto/OAuth2TokenResponse.java (1)
3-10: 단순하고 명확한 OAuth2 응답 DTO 도입 — LGTM레코드 기반 DTO로 불변성과 직렬화 용이성을 확보했고, 신규 사용자 여부 신호도 함께 전달합니다. 목적에 부합합니다.
hd0rable
left a comment
There was a problem hiding this comment.
수고하셨습니닷 간단한 리뷰 남겼는데 확인 부탁드립낟ㅅ
| public BaseResponse<OAuth2TokenResponse> checkUserExists( | ||
| @Parameter(description = "소셜 로그인 ID (형식: {provider}_{식별자 ID})", example = "kakao_1234567890") | ||
| @RequestBody OAuth2TokenRequest oAuth2TokenRequest) throws IOException { | ||
| return userJpaRepository.findByOauth2Id(oAuth2TokenRequest.oauth2Id()) |
| ObjectMapper objectMapper = new ObjectMapper(); | ||
| String body = objectMapper.writeValueAsString(value); | ||
| response.getWriter().write(body); | ||
| private void addTokenCookie(HttpServletResponse response, String token) { |
There was a problem hiding this comment.
p3: 엇 웹은 쿠키로 토큰 전달하는건가요?? 헤더에 넣어주고 httpOnly 속성 해제하는 걸로 통일하는거 아니였나요? 쿠키로 전달할거면 httpOnly속성을 추가하는게 좋아보입니닷!
There was a problem hiding this comment.
웹에서는 서버 측에서 직접 리다이렉트를 처리해야 하기 때문에, 리다이렉트 시 토큰을 헤더에 담아 전달할 수 없고 쿠키 방식을 사용할 수밖에 없습니다.
또한 앞서 이야기한 것처럼 HttpOnly 옵션을 설정하면 JS에서 쿠키에 접근해 토큰을 추출할 수 없기 때문에, 웹은 쿠키 기반 인가, 안드는 헤더 기반 인가로 두 가지 방식으로 나뉘게 됩니다.
하지만 저희 서비스는 민감한 개인정보를 다루는 수준은 아니라고 판단하여, HttpOnly 옵션을 해제하고 JS에서 쿠키에 접근해 토큰을 추출한 뒤, 이후 API 요청부터는 헤더에 토큰을 담아 보내는 방식으로 안드로이드와 웹의 인가 방식을 통일하기로 결정하였습니다!!
There was a problem hiding this comment.
아하 그 후의 인증방식이 통일이고 인가방법이 분기 처리였군여 제가 착각했습니닿 마루머쓱
| config.setMaxAge(3600L); | ||
|
|
||
| config.setExposedHeaders(Collections.singletonList("Authorization")); | ||
| config.setExposedHeaders(Collections.singletonList(JWT_HEADER_KEY.getValue())); |
seongjunnoh
left a comment
There was a problem hiding this comment.
LGTM 바뀐 소셜 로그인 로직 확인했습니다 머지하겠습니다!!
| public record OAuth2TokenResponse( | ||
| String token, | ||
| boolean isNewUser | ||
| ) { |
| Long userId, | ||
| String accessToken |
#️⃣ 연관된 이슈
📝 작업 내용
안드로이드
소셜 로그인 provider로부터 받은 식별자를 쿼리 파라미터로 받아 서버 DB에 존재하는지 여부를 판단하여 액세스 토큰 또는 임시 토큰을 전달하는 api를 구현했습니다.
웹
기존 소셜 로그인 api를 유지하고 정적 redirect url을 통해 신규 유저 여부에 따라 홈화면 또는 회원가입 화면으로 redirect 시키도록 했습니다. 이때, 쿠키를 통해 토큰을 전달합니다.
📸 스크린샷
💬 리뷰 요구사항
토큰을 바디로 넘겨주는 것이 더 안전하다는 피드백을 받아 회원가입 api, 사용자 정보 조회 api에서 반환하는 토큰은 모두 바디로 넘겨주는 것으로 수정하였습니다.
하나의 간단한 api이기 때문에 따로 패키지를 만드는 것보다는 기존 security.oauth2Id 패키지 하위에 컨트롤러 하나만 선언하는 것이 낫다고 판단했습니다~
📌 PR 진행 시 이러한 점들을 참고해 주세요
Summary by CodeRabbit
신규 기능
버그 수정
개선 및 변경
문서화