Skip to content

[URECA-22] Feat: kakao login - #15

Closed
40food wants to merge 8 commits into
developfrom
URECA-22/Feat/kakao-login
Closed

40food wants to merge 8 commits into
developfrom
URECA-22/Feat/kakao-login

Conversation

@40food

@40food 40food commented Jan 9, 2026

Copy link
Copy Markdown
Contributor

Key Changes

카카오, 구글, 네이버 로그인 동작 확인 완료

작업 내역

  • KakaoResponse 생성
  • client key naming 수정
  • (백엔드 외적으로) redirect uri 통일

close: #6

💬 공유사항 to 리뷰어

  • 리팩토링을 나중에 해야할 것 같긴 합니다 의견 주세요

비고

Summary by CodeRabbit

  • 새로운 기능

    • 카카오 OAuth 인증 흐름 개선: 토큰 교환과 사용자 정보 조회가 더 안정적이고 예외에 강해졌습니다.
    • CORS 설정 추가: 프론트엔드와의 교차 출처 요청이 허용되어 웹 클라이언트 통신이 원활해졌습니다.
  • 리팩터

    • 인증 및 토큰 처리 로직 정리로 응답 처리와 오류 검증이 향상되었습니다.

✏️ Tip: You can customize this high-level summary in your review settings.

40food added 5 commits January 9, 2026 11:31
commit 218f057
Author: JunHyeong Park <64461926+joonhyong@users.noreply.github.com>
Date:   Thu Jan 8 15:49:29 2026 +0900

    [URECA-21] Chore: domain/auth 및 global/security 구조 정립 (#8)

    * chore: domain/auth 구조 및 global/security 구조 분리

    * chore: OAuth 공통부분 파일 작성

    * chore: KakaoOAuthClient, GoogleOAuthClient 파일 생성

    * chore: PR 템플릿 업로드

    * chore: OAuthService 시그니처 변경, OAuthProvider Enum 도입, OAuthController 엔드포인트 정리, DTO 불변성 개선

    * chore: OAuthController 입력 검증 추가, GlobalExceptionHandler 추가

    * chore: GlobalExceptionHandler 에러 발생 시 내부 구현 노출 문제 및 로깅 누락 해결
commit c6aef4f
Author: JunHyeong Park <64461926+joonhyong@users.noreply.github.com>
Date:   Fri Jan 9 15:44:50 2026 +0900

    [URECA-27] Feat: Ureca 27/feat/oauth client (#14)

    * Feat: User 엔티티 생성

    * Feat OAuthClient 생성 및 Google, Kakao, Naver OAuthClient 구현

    * Feat: OAuthServiceImpl 및 UserMapper.xml 구현 -> 로그인 요청 확인 및 DB 연동

    * Fix: GoogleOAuthClient, NaverOAuthClient에 name에 대한 fallback 추가

commit 218f057
Author: JunHyeong Park <64461926+joonhyong@users.noreply.github.com>
Date:   Thu Jan 8 15:49:29 2026 +0900

    [URECA-21] Chore: domain/auth 및 global/security 구조 정립 (#8)

    * chore: domain/auth 구조 및 global/security 구조 분리

    * chore: OAuth 공통부분 파일 작성

    * chore: KakaoOAuthClient, GoogleOAuthClient 파일 생성

    * chore: PR 템플릿 업로드

    * chore: OAuthService 시그니처 변경, OAuthProvider Enum 도입, OAuthController 엔드포인트 정리, DTO 불변성 개선

    * chore: OAuthController 입력 검증 추가, GlobalExceptionHandler 추가

    * chore: GlobalExceptionHandler 에러 발생 시 내부 구현 노출 문제 및 로깅 누락 해결
@40food 40food self-assigned this Jan 9, 2026
@coderabbitai

coderabbitai Bot commented Jan 9, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

카카오 OAuth 연동을 위한 세 가지 변경을 추가합니다. KakaoUserResponse DTO를 도입해 카카오 사용자 응답(id, 이메일, 닉네임)을 매핑합니다. KakaoOAuthClient는 액세스 토큰 교환을 폼 인코딩 HTTP 호출로 재구성하고 TokenResponse DTO를 추가했으며, 사용자 정보 조회는 KakaoUserResponse를 사용해 매핑하도록 변경되었습니다. WebMvcConfig를 추가해 프로퍼티로 지정된 허용 출처로 CORS를 설정합니다.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes


🎓 교육적 피드백

✅ 잘된 점

  • 타입 안전한 DTO 사용으로 Map 기반 파싱을 제거한 점은 런타임 오류를 줄이는 훌륭한 선택입니다 — 깔끔해요.
  • RestTemplate 호출을 명시적으로 바꾸고 null/empty 체크를 추가한 것은 안정성 측면에서 긍정적입니다.

🔍 개선 제안

  1. TokenResponse 위치 재검토
  • 현재 inner class인 TokenResponse를 공용 DTO로 분리하세요 (예: src/main/java/.../dto/TokenResponse.java). 다른 OAuth 제공자와 재사용 가능성이 높습니다.
  1. 예외/응답 에러 처리 강화
  • RestTemplate 호출 결과가 null이거나 access_token이 없을 때 명시적 예외(예: OAuthException)를 던지세요.
  • 4xx/5xx, 타임아웃에 대한 재시도/백오프 및 로깅 전략을 추가하세요.
  1. JSON 매핑 견고성 개선
  • KakaoUserResponse 필드에 @JsonProperty와 필요시 @JsonIgnoreProperties(ignoreUnknown = true), 또는 @Nullable 주석을 사용해 예상치 못한 응답 변화에 대비하세요.
  1. CORS 설정 유연화
  • 하드코딩 대신 프로퍼티 기본값 및 쉼표 분리 처리(여러 출처)를 적용하세요. 개발/프로덕션 프로필별 설정을 권장합니다.
  1. 보안 체크리스트 우선 완료
  • Linked issue의 미완료 항목 중 state 검증과 redirect URI 고정, 토큰 저장 전략을 우선 도입하세요. CSRF와 오픈 리다이렉트 취약점을 방치하면 위험합니다.
  1. 테스트 보강
  • RestTemplate을 Mocking한 단위/통합 테스트로 정상/실패(토큰 누락, 4xx/5xx) 시나리오를 검증하세요.

짧게 한 마디: 타입 정리는 잘하셨습니다 — 이제 실패 케이스와 보안 경계선에 방어벽을 더 쌓아주세요.

🚥 Pre-merge checks | ✅ 2 | ❌ 3
❌ Failed checks (2 warnings, 1 inconclusive)
Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning PR은 KakaoUserResponse 생성, KakaoOAuthClient 리팩토링, CORS 설정만 포함하고 있으며, 이슈 #6의 필수 요구사항인 '인가 코드 콜백 API', '유저 매핑', '서비스 토큰 발급', '보안 처리(state 검증, redirect uri 고정, 토큰 서버 저장)' 등이 미구현 상태입니다. 이슈 #6의 완료 기준을 명확히 정의하고 PR 설명에 어느 체크리스트 항목을 구현했는지 명시하세요. 현재 구현 범위가 전체 기능의 일부임을 인지하고 진행하세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Out of Scope Changes check ❓ Inconclusive WebMvcConfig의 CORS 설정 추가는 카카오 로그인의 필수 요소가 아니며, PR 설명에서 언급되지 않았습니다. 이것이 의도된 변경인지 불명확합니다. WebMvcConfig 추가 사유를 PR 설명에 명시하세요. 카카오 로그인 기능과의 관계를 설명하거나, 별도 PR로 분리를 검토하세요.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목은 카카오 로그인이라는 주요 변경사항을 명확하게 요약하고 있으며, Jira 이슈 번호를 포함하고 있습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Comment @coderabbitai help to get the list of available commands and usage tips.

@40food 40food changed the title Ureca 22/feat/kakao login kakao login Jan 9, 2026
@github-actions github-actions Bot changed the title kakao login [URECA-22] Feat: kakao login Jan 9, 2026
joonhyong
joonhyong previously approved these changes Jan 9, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🤖 Fix all issues with AI agents
In @src/main/java/com/ureca/unity/domain/auth/dto/KakaoUserResponse.java:
- Around line 9-12: KakaoUserResponse currently exposes a nullable kakaoAccount
which can cause NPEs when callers in KakaoOAuthClient do
body.getKakaoAccount().getEmail()/getProfile().getNickname(); add null-safe
accessors on KakaoUserResponse such as getEmail() and getNickname() that return
Optional<String> by mapping through kakaoAccount (and profile for nickname), and
update KakaoOAuthClient to use these Optionals (or check isPresent()) instead of
chaining getters; alternatively, mark kakaoAccount with @JsonProperty(required =
true) if you want deserialization to enforce presence, but prefer adding the
Optional-returning methods to avoid NPEs.

In
@src/main/java/com/ureca/unity/domain/auth/service/oauth/KakaoOAuthClient.java:
- Line 6: KakaoOAuthClient defines a duplicate inner TokenResponse class (lines
~93-106) while com.ureca.unity.domain.auth.dto.TokenResponse already exists;
remove the inner class and update usages in KakaoOAuthClient (e.g., methods
parsing/returning token payload) to import and use
com.ureca.unity.domain.auth.dto.TokenResponse instead, or map to a service-level
DTO like ServiceTokenResponse if you need a different shape, ensuring
constructors/parsers (e.g., whereTokenResponseIsBuilt or parseTokenResponse) are
adjusted to instantiate or convert to the existing DTO.
- Around line 34-40: getUserInfo currently throws a generic RuntimeException
when accessToken is missing; create a custom OAuthTokenException (e.g., in
domain/auth/exception) that accepts an OAuthProvider and message, and replace
the RuntimeException in getUserInfo with throwing new
OAuthTokenException(OAuthProvider.KAKAO, "Failed to get Kakao access token");
also update getAccessToken to throw OAuthTokenException on error cases so
callers get a specific exception type, and ensure your GlobalExceptionHandler
maps OAuthTokenException to an appropriate client response.
- Around line 85-90: The code in KakaoOAuthClient that builds the OAuthUserInfo
is unsafe because body.getKakaoAccount() and its nested
getEmail()/getProfile().getNickname() can be null; add null checks (or use
Optional/guard clauses) before accessing these nested fields in the method that
returns new OAuthUserInfo so you pass a safe email and nickname (e.g., null or
empty string) when absent, and complement this by adjusting KakaoUserResponse
annotations/mapping (e.g., remove required=true or document optional fields) to
reflect that email/profile may be missing.
- Around line 34-67: The OAuth flow lacks state parameter handling: update the
authorization initiation and callback flow so a cryptographically random state
is generated and stored server-side (e.g., session or secure cookie) when
building the auth URL, then require and validate the incoming state on callback
handlers; specifically, modify OAuthController to accept both code and state,
have OAuthServiceImpl's start/login method emit and persist the state, and
update OAuth client implementations (KakaoOAuthClient.getUserInfo and analogous
methods in Google/Naver clients) to accept the state, compare it against the
stored value and throw a clear exception (rejecting the request) on mismatch or
missing state; ensure storage is tied to the user session and that invalid state
leads to an immediate error response to prevent CSRF.

In @src/main/java/com/ureca/unity/global/config/WebMvcConfig.java:
- Around line 11-14: The CORS origin is hard-coded in WebMvcConfig
(registry.addMapping(...).allowedOrigins("http://localhost:3000")), so
externalize it to a config property and read it at runtime: add a
cors.allowed-origins property (default via application.yml and overridable by
env var CORS_ALLOWED_ORIGINS), inject that property into the WebMvcConfig class
(e.g., via @Value or a @ConfigurationProperties bean) and call
registry.addMapping(...).allowedOrigins(...) with the injected value(s) (split
into an array if multiple origins are supported); ensure allowCredentials(true)
and allowedMethods remain unchanged.
🧹 Nitpick comments (1)
src/main/java/com/ureca/unity/domain/auth/service/oauth/KakaoOAuthClient.java (1)

57-57: 리팩토링 권장: API URL을 설정 파일로 외부화하세요.

현재 카카오 API URL이 하드코딩되어 있습니다. 향후 API 버전 변경이나 테스트 환경 구성 시 코드 수정이 필요합니다.

♻️ 설정 외부화 예시

application.yml:

oauth:
  kakao:
    token-uri: https://kauth.kakao.com/oauth/token
    user-info-uri: https://kapi.kakao.com/v2/user/me

코드:

+@Value("${oauth.kakao.token-uri}")
+private String tokenUri;
+
+@Value("${oauth.kakao.user-info-uri}")
+private String userInfoUri;
+
 private String getAccessToken(String code) {
     // ...
     TokenResponse response = restTemplate.postForObject(
-            "https://kauth.kakao.com/oauth/token",
+            tokenUri,
             request,
             TokenResponse.class
     );

Also applies to: 76-76

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c6aef4f and 89c44a8.

📒 Files selected for processing (3)
  • src/main/java/com/ureca/unity/domain/auth/dto/KakaoUserResponse.java
  • src/main/java/com/ureca/unity/domain/auth/service/oauth/KakaoOAuthClient.java
  • src/main/java/com/ureca/unity/global/config/WebMvcConfig.java
🧰 Additional context used
🧬 Code graph analysis (2)
src/main/java/com/ureca/unity/domain/auth/dto/KakaoUserResponse.java (1)
src/main/java/com/ureca/unity/domain/auth/service/oauth/KakaoOAuthClient.java (1)
  • Getter (93-106)
src/main/java/com/ureca/unity/domain/auth/service/oauth/KakaoOAuthClient.java (3)
src/main/java/com/ureca/unity/domain/auth/dto/KakaoUserResponse.java (3)
  • Getter (6-24)
  • Getter (14-18)
  • Getter (20-23)
src/main/java/com/ureca/unity/domain/auth/dto/OAuthUserInfo.java (1)
  • Getter (7-16)
src/main/java/com/ureca/unity/domain/auth/dto/TokenResponse.java (1)
  • Getter (6-12)
🔇 Additional comments (1)
src/main/java/com/ureca/unity/domain/auth/service/oauth/KakaoOAuthClient.java (1)

25-26: client_secret은 Kakao OAuth에서 필수 파라미터입니다 — 현재 구현이 올바릅니다.

공식 Kakao OAuth 문서에 따르면, 토큰 엔드포인트(/oauth/token)에 client_secret을 전송하는 것이 필수입니다. 이 기능은 앱 설정에서 기본적으로 활성화되어 있습니다. 따라서 현재 코드에서 @ValueclientSecret을 주입하고 요청 파라미터에 포함하는 방식은 정확합니다.

JavaScript 앱이나 네이티브 앱에서는 PKCE 흐름을 사용하여 client_secret 없이 동작하지만, 이 코드처럼 백엔드 서버에서 authorization code를 교환하는 경우(confidential client flow)에는 client_secret이 반드시 필요합니다.

수정할 필요가 없습니다.

Also applies to: 46-46

Comment on lines +34 to 67
public OAuthUserInfo getUserInfo(String code) {
String accessToken = getAccessToken(code);
if (accessToken == null || accessToken.isEmpty()) {
throw new RuntimeException("Failed to get Kakao access token");
}
return getUser(accessToken);
}

/* 1. Authorization Code → Access Token */
private String getAccessToken(String code) {
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("grant_type", "authorization_code");
params.add("client_id", clientId);
params.add("client_secret", clientSecret);
params.add("redirect_uri", redirectUri);
params.add("code", code);

HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("grant_type", "authorization_code");
body.add("client_id", clientId);
body.add("redirect_uri", redirectUri);
body.add("code", code);
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(params, headers);

// Client Secret을 쓰는 경우만 포함
if (clientSecret != null && !clientSecret.isBlank()) {
body.add("client_secret", clientSecret);
}

HttpEntity<MultiValueMap<String, String>> request =
new HttpEntity<>(body, headers);

ResponseEntity<Map> response = restTemplate.exchange(
tokenUri,
HttpMethod.POST,
// TokenResponse는 Jackson이 인식할 수 있도록 기본 생성자 필요
TokenResponse response = restTemplate.postForObject(
"https://kauth.kakao.com/oauth/token",
request,
Map.class
TokenResponse.class
);

if (response.getBody() == null || response.getBody().get("access_token") == null) {
throw new IllegalArgumentException("Failed to retrieve Kakao access token");
if (response == null || response.getAccessToken() == null) {
throw new RuntimeException("Kakao token response is null or invalid");
}

return response.getBody().get("access_token").toString();
return response.getAccessToken();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# KakaoOAuthClient.java 전체 파일 확인
cat src/main/java/com/ureca/unity/domain/auth/service/oauth/KakaoOAuthClient.java

Repository: Ureca3/Ureca-server

Length of output: 3682


🏁 Script executed:

# 다른 OAuth 클라이언트 파일 확인
fd "OAuthClient" src/main/java --type f

Repository: Ureca3/Ureca-server

Length of output: 370


🏁 Script executed:

# state 파라미터 관련 코드 검색
rg "state" src/main/java/com/ureca/unity/domain/auth/ -i --type java -B2 -A2

Repository: Ureca3/Ureca-server

Length of output: 45


🏁 Script executed:

# OAuth 관련 설정이나 인터페이스 확인
fd -e java src/main/java/com/ureca/unity/domain/auth/ | head -20

Repository: Ureca3/Ureca-server

Length of output: 536


🏁 Script executed:

# OAuth 콜백 관련 컨트롤러 검색
fd -e java src/main/java/com/ureca/unity/domain/auth/ --type f

Repository: Ureca3/Ureca-server

Length of output: 536


🏁 Script executed:

# GoogleOAuthClient와 NaverOAuthClient에서 state 처리 확인
cat src/main/java/com/ureca/unity/domain/auth/service/oauth/GoogleOAuthClient.java

Repository: Ureca3/Ureca-server

Length of output: 3349


🏁 Script executed:

# state 관련 코드 검색 (다양한 형식으로)
rg "state" src/main/java/com/ureca/unity/domain/auth/ --type java -i

Repository: Ureca3/Ureca-server

Length of output: 45


🏁 Script executed:

# 인증 관련 서비스나 컨트롤러 파일 목록
fd -e java src/main/java/com/ureca/unity/domain/auth/ -type f

Repository: Ureca3/Ureca-server

Length of output: 233


🏁 Script executed:

# 올바른 fd 문법으로 auth 디렉토리의 모든 파일 찾기
fd . src/main/java/com/ureca/unity/domain/auth/

Repository: Ureca3/Ureca-server

Length of output: 1184


🏁 Script executed:

# OAuth 콜백/인증 관련 컨트롤러 찾기
find src/main/java/com/ureca/unity/domain/auth/ -name "*Controller*" -o -name "*Service*"

Repository: Ureca3/Ureca-server

Length of output: 276


🏁 Script executed:

# state 파라미터 검색 (대소문자 구분 없이)
rg -i "state" src/main/java/com/ureca/unity/domain/auth/

Repository: Ureca3/Ureca-server

Length of output: 45


🏁 Script executed:

# NaverOAuthClient도 확인
cat src/main/java/com/ureca/unity/domain/auth/service/oauth/NaverOAuthClient.java

Repository: Ureca3/Ureca-server

Length of output: 3564


🏁 Script executed:

# OAuthController.java 확인
cat src/main/java/com/ureca/unity/domain/auth/controller/OAuthController.java

Repository: Ureca3/Ureca-server

Length of output: 985


🏁 Script executed:

# OAuthServiceImpl.java 확인
cat src/main/java/com/ureca/unity/domain/auth/service/OAuthServiceImpl.java

Repository: Ureca3/Ureca-server

Length of output: 2419


🏁 Script executed:

# OAuthService 인터페이스도 확인
cat src/main/java/com/ureca/unity/domain/auth/service/OAuthService.java

Repository: Ureca3/Ureca-server

Length of output: 342


OAuth 보안: state 파라미터 검증 필수 구현

현재 OAuth 흐름에서 CSRF 공격을 방지하는 state 파라미터가 완전히 빠져있습니다. OAuthController는 code만 받고 있으며, OAuthServiceImpl과 모든 OAuthClient(Kakao, Google, Naver)에서도 state 처리가 없습니다.

개선 방향 (RFC 6749 Section 10.12):

  1. 인가 요청 시 클라이언트가 생성한 랜덤 state를 세션/쿠키에 저장
  2. 콜백 핸들러에서 요청 파라미터의 state와 저장된 state 비교 검증
  3. 불일치 시 요청 거부 및 예외 처리

현재 상태로는 공격자가 사용자를 강제로 다른 계정으로 인가하도록 유도할 수 있습니다. 다음 리팩토링 단계에서 반드시 추가하세요.

🤖 Prompt for AI Agents
In
@src/main/java/com/ureca/unity/domain/auth/service/oauth/KakaoOAuthClient.java
around lines 34 - 67, The OAuth flow lacks state parameter handling: update the
authorization initiation and callback flow so a cryptographically random state
is generated and stored server-side (e.g., session or secure cookie) when
building the auth URL, then require and validate the incoming state on callback
handlers; specifically, modify OAuthController to accept both code and state,
have OAuthServiceImpl's start/login method emit and persist the state, and
update OAuth client implementations (KakaoOAuthClient.getUserInfo and analogous
methods in Google/Naver clients) to accept the state, compare it against the
stored value and throw a clear exception (rejecting the request) on mismatch or
missing state; ensure storage is tied to the user session and that invalid state
leads to an immediate error response to prevent CSRF.

Comment on lines +56 to 60
TokenResponse response = restTemplate.postForObject(
"https://kauth.kakao.com/oauth/token",
request,
Map.class
TokenResponse.class
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

예외 처리 누락: RestTemplate 호출 실패 시나리오를 처리하세요.

RestTemplate은 네트워크 오류, 4xx/5xx 응답 시 RestClientException을 던지는데, 현재 코드는 이를 처리하지 않습니다. OAuth 플로우 중 외부 API 실패는 충분히 예상 가능한 상황입니다.

🔧 예외 처리 추가
+import org.springframework.web.client.RestClientException;
+
 private String getAccessToken(String code) {
     // ... params 설정 ...
     
-    TokenResponse response = restTemplate.postForObject(
-            "https://kauth.kakao.com/oauth/token",
-            request,
-            TokenResponse.class
-    );
+    TokenResponse response;
+    try {
+        response = restTemplate.postForObject(
+                "https://kauth.kakao.com/oauth/token",
+                request,
+                TokenResponse.class
+        );
+    } catch (RestClientException e) {
+        throw new OAuthTokenException(OAuthProvider.KAKAO, 
+            "카카오 토큰 발급 API 호출 실패: " + e.getMessage(), e);
+    }
     
     if (response == null || response.getAccessToken() == null) {

getUser() 메서드도 동일하게 적용하세요.

Spring Security OAuth2 Client 라이브러리를 사용하면 재시도, 타임아웃 등 더 견고한 처리를 기본 제공합니다.

Also applies to: 75-80

Comment thread src/main/java/com/ureca/unity/global/config/WebMvcConfig.java
40food added 2 commits January 9, 2026 19:42
- KakaoUserResponse 중첩 객체 null 처리
- KakaoOAuthClient 중첩 필드 null 검증 추가
- CORS origin 환경변수로 외부화

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In
@src/main/java/com/ureca/unity/domain/auth/service/oauth/KakaoOAuthClient.java:
- Around line 38-40: Replace the generic RuntimeException thrown in
KakaoOAuthClient when the access token is missing with a specific custom
exception (e.g., OAuthTokenException or OAuthAuthenticationException): create
the custom exception class (extending RuntimeException or a project base
exception) with message and cause constructors, then change the throw in the
method that obtains the token inside KakaoOAuthClient (the accessToken
null/empty check) to throw the new OAuthTokenException with a descriptive
message; update any callers or method signatures if they expect this specific
exception for clearer error handling.
- Line 33: The RestTemplate in KakaoOAuthClient is created without timeouts
(private final RestTemplate restTemplate = new RestTemplate()), which can cause
blocking on external Kakao calls; replace this direct instantiation by injecting
a configured RestTemplate bean (or build one with RestTemplateBuilder) that sets
connection timeout ~3000ms and read timeout ~5000ms (or use
SimpleClientHttpRequestFactory with setConnectTimeout and setReadTimeout) and
update KakaoOAuthClient to accept the RestTemplate via constructor injection and
remove the direct new RestTemplate() field initialization.
- Around line 71-103: In getUser, avoid repeating null-unsafe calls to
body.getKakaoAccount() and directly accessing nested fields; compute local
variables for kakaoAccount, email and nickname using Optional (or null checks)
once (e.g., var account = body.getKakaoAccount(); String email =
Optional.ofNullable(account).map(KakaoUserResponse.KakaoAccount::getEmail).orElse(null);
String nickname =
Optional.ofNullable(account).map(KakaoUserResponse.KakaoAccount::getProfile).map(KakaoUserResponse.Profile::getNickname).orElse("Unknown");)
and then pass those locals into the OAuthUserInfo constructor instead of calling
body.getKakaoAccount().getEmail() and
body.getKakaoAccount().getProfile().getNickname(), ensuring null-safety and
removing the duplicated optional chains and the stray extra semicolon.
- Line 6: Remove the dead inner static TokenResponse class (the one defined
around lines 108-118) so the code relies solely on the imported
com.ureca.unity.domain.auth.dto.TokenResponse; delete that inner class
declaration and any redundant getAccessToken() method, leaving the existing
import on line 6 and usages like TokenResponse.class intact, and then verify the
external DTO has Jackson annotations (e.g., @JsonProperty("access_token")) or
proper naming strategy so the access_token field maps correctly.
🧹 Nitpick comments (4)
src/main/java/com/ureca/unity/domain/auth/dto/KakaoUserResponse.java (2)

6-12: Jackson 역직렬화를 위한 기본 생성자가 필요합니다.

RestTemplate.postForObject() 또는 exchange()로 응답을 매핑할 때 Jackson은 기본 생성자를 사용합니다. 현재 명시적 생성자가 없어 Lombok의 암묵적 처리에 의존하고 있지만, 명확성을 위해 @NoArgsConstructor를 추가하는 것이 좋습니다.

또한 @JsonProperty(required = true)를 설정했지만, 실제 필드가 null일 경우 런타임에 NullPointerException이 발생할 수 있습니다. @NotNull 같은 validation 어노테이션 추가를 고려하세요.

♻️ 제안하는 개선안
+import lombok.NoArgsConstructor;
+
 @Getter
+@NoArgsConstructor
 public class KakaoUserResponse {
 
     private Long id;

14-23: 중첩 클래스에도 기본 생성자 명시가 필요합니다.

KakaoAccountProfile 클래스도 Jackson 역직렬화 대상이므로 @NoArgsConstructor를 추가해야 합니다. 특히 외부 API 응답 구조가 변경되거나 필드가 누락될 경우를 대비해 명시적으로 선언하는 것이 안전합니다.

♻️ 제안하는 개선안
 @Getter
+@NoArgsConstructor
 public static class KakaoAccount {
     private String email;
     private Profile profile;
 }
 
 @Getter
+@NoArgsConstructor
 public static class Profile {
     private String nickname;
 }
src/main/java/com/ureca/unity/domain/auth/service/oauth/KakaoOAuthClient.java (2)

44-69: 하드코딩된 URL을 properties로 외부화하세요.

Line 59의 https://kauth.kakao.com/oauth/token이 하드코딩되어 있습니다. 개발/운영 환경 분리나 테스트 시 유연성을 위해 application.yml에 설정하는 것이 좋습니다.

또한 Line 64-66의 예외 처리도 앞서 언급한 커스텀 예외로 개선이 필요합니다.

⚙️ 제안하는 개선안

application.yml 추가:

oauth:
  kakao:
    token-uri: https://kauth.kakao.com/oauth/token
    user-info-uri: https://kapi.kakao.com/v2/user/me

코드 수정:

 @Component("kakao")
 @RequiredArgsConstructor
 public class KakaoOAuthClient implements OAuthClient {
 
     @Value("${oauth.kakao.client-id}")
     private String clientId;
 
     @Value("${oauth.kakao.client-secret}")
     private String clientSecret;
 
     @Value("${oauth.kakao.redirect-uri}")
     private String redirectUri;
+    
+    @Value("${oauth.kakao.token-uri}")
+    private String tokenUri;
 
     // ...
 
     private String getAccessToken(String code) {
         // ...
         TokenResponse response = restTemplate.postForObject(
-                "https://kauth.kakao.com/oauth/token",
+                tokenUri,
                 request,
                 TokenResponse.class
         );

105-118: 중복된 getter 메서드를 제거하세요.

@Getter 어노테이션이 클래스 레벨에 있으므로, Line 115-117의 명시적 getAccessToken() 메서드는 불필요합니다. Lombok이 자동으로 getAccess_token() 메서드를 생성하며, Line 116에서 access_token 필드를 그대로 반환하고 있어 중복입니다.

필드명을 Java 관례에 맞게 accessToken으로 변경하고 @JsonProperty("access_token")을 사용하는 것을 권장합니다.

♻️ 개선안
 @Getter
 @Setter
 @NoArgsConstructor
 public static class TokenResponse {
+    @JsonProperty("access_token")
-    private String access_token;
+    private String accessToken;
+    
+    @JsonProperty("token_type")
     private String token_type;
+    
+    @JsonProperty("refresh_token")
     private String refresh_token;
+    
+    @JsonProperty("expires_in")
     private Long expires_in;
+    
     private String scope;
-
-    public String getAccessToken() {
-        return access_token;
-    }
 }

이렇게 하면 Line 68에서 response.getAccessToken()을 그대로 사용할 수 있습니다.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 89c44a8 and b9c7ab9.

📒 Files selected for processing (3)
  • src/main/java/com/ureca/unity/domain/auth/dto/KakaoUserResponse.java
  • src/main/java/com/ureca/unity/domain/auth/service/oauth/KakaoOAuthClient.java
  • src/main/java/com/ureca/unity/global/config/WebMvcConfig.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/com/ureca/unity/global/config/WebMvcConfig.java

import com.ureca.unity.domain.auth.constant.OAuthProvider;
import com.ureca.unity.domain.auth.dto.KakaoUserResponse;
import com.ureca.unity.domain.auth.dto.OAuthUserInfo;
import com.ureca.unity.domain.auth.dto.TokenResponse;

Copy link
Copy Markdown

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

🏁 Script executed:

# KakaoOAuthClient.java 파일 확인
fd -t f "KakaoOAuthClient.java" -x cat -n {} | head -120

Repository: Ureca3/Ureca-server

Length of output: 4920


내부 TokenResponse 클래스를 삭제하세요 - 사용되지 않는 코드입니다.

Line 6의 import는 실제로 Line 61에서 TokenResponse.class로 사용되고 있습니다. 하지만 Lines 108-118의 내부 static class TokenResponse는 Java의 import 우선순위 규칙에 의해 코드에서 전혀 참조되지 않는 dead code입니다.

이 내부 클래스는:

  • 불필요한 getAccessToken() 메서드를 정의하고 있음 (getter annotation이 이미 있음)
  • 유지보수 시 혼란을 야기함
  • 코드 중복을 초래함

해결책: Lines 108-118의 내부 TokenResponse 클래스를 완전히 삭제하고, 외부 DTO인 com.ureca.unity.domain.auth.dto.TokenResponse만 사용하세요. 외부 DTO에서 Jackson이 snake_case 필드명(access_token)을 제대로 매핑할 수 있도록 @JsonProperty 애너테이션 설정이 되어있는지 확인하세요.

🤖 Prompt for AI Agents
In
@src/main/java/com/ureca/unity/domain/auth/service/oauth/KakaoOAuthClient.java
at line 6, Remove the dead inner static TokenResponse class (the one defined
around lines 108-118) so the code relies solely on the imported
com.ureca.unity.domain.auth.dto.TokenResponse; delete that inner class
declaration and any redundant getAccessToken() method, leaving the existing
import on line 6 and usages like TokenResponse.class intact, and then verify the
external DTO has Jackson annotations (e.g., @JsonProperty("access_token")) or
proper naming strategy so the access_token field maps correctly.

@Value("${oauth.kakao.redirect-uri}")
private String redirectUri;

private final RestTemplate restTemplate = new RestTemplate();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

RestTemplate에 타임아웃 설정이 필요합니다.

외부 API(카카오) 호출 시 네트워크 지연이나 장애 상황에서 무한 대기할 수 있습니다. RestTemplate에 connection timeout과 read timeout을 설정하지 않으면 스레드가 블로킹되어 서비스 전체에 영향을 줄 수 있습니다.

Spring의 RestTemplateBuilder를 사용하거나, SimpleClientHttpRequestFactory로 타임아웃을 설정하세요. 일반적으로 connection timeout 3초, read timeout 5초가 권장됩니다.

⚙️ 타임아웃 설정 예시

Config 클래스에서 빈으로 등록하는 방법:

@Configuration
public class RestTemplateConfig {
    
    @Bean
    public RestTemplate oAuthRestTemplate() {
        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
        factory.setConnectTimeout(3000);
        factory.setReadTimeout(5000);
        return new RestTemplate(factory);
    }
}

그리고 KakaoOAuthClient에서 주입받아 사용:

 @Component("kakao")
 @RequiredArgsConstructor
 public class KakaoOAuthClient implements OAuthClient {
     // ...
-    private final RestTemplate restTemplate = new RestTemplate();
+    private final RestTemplate oAuthRestTemplate;

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
@src/main/java/com/ureca/unity/domain/auth/service/oauth/KakaoOAuthClient.java
at line 33, The RestTemplate in KakaoOAuthClient is created without timeouts
(private final RestTemplate restTemplate = new RestTemplate()), which can cause
blocking on external Kakao calls; replace this direct instantiation by injecting
a configured RestTemplate bean (or build one with RestTemplateBuilder) that sets
connection timeout ~3000ms and read timeout ~5000ms (or use
SimpleClientHttpRequestFactory with setConnectTimeout and setReadTimeout) and
update KakaoOAuthClient to accept the RestTemplate via constructor injection and
remove the direct new RestTemplate() field initialization.

Comment on lines +36 to 42
public OAuthUserInfo getUserInfo(String code) {
String accessToken = getAccessToken(code);
if (accessToken == null || accessToken.isEmpty()) {
throw new RuntimeException("Failed to get Kakao access token");
}
return getUser(accessToken);
}

Copy link
Copy Markdown

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

🏁 Script executed:

#!/bin/bash
# Check for state validation logic in OAuth/Auth related files
rg -n "state" --type java -g "**/*OAuth*" -g "**/*Auth*" -A 3 -B 3

echo "---"

# Search for redirect URI validation
rg -n "redirectUri\|redirect_uri" --type java -A 2 -B 2 | head -100

Repository: Ureca3/Ureca-server

Length of output: 66


🏁 Script executed:

#!/bin/bash
# Check the full KakaoOAuthClient implementation
find . -type f -name "KakaoOAuthClient.java" | xargs cat -n

Repository: Ureca3/Ureca-server

Length of output: 4920


🏁 Script executed:

#!/bin/bash
# Search for OAuth related configuration or parameter handling
rg -n "state\|redirectUri" --type java -g "**/*Controller*" -g "**/*Config*" -A 2 -B 2 | head -80

Repository: Ureca3/Ureca-server

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Find Controller files that might handle OAuth
find . -type f -name "*Controller.java" | xargs grep -l -i "oauth\|kakao" | head -10

Repository: Ureca3/Ureca-server

Length of output: 138


🏁 Script executed:

#!/bin/bash
# Search for state generation or storage logic
rg -n "new.*state\|generateState\|saveState\|UUID.*generate" --type java | head -50

Repository: Ureca3/Ureca-server

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Look for OAuth related services
find . -type f -name "*OAuthService*" -o -name "*AuthService*" | xargs cat -n 2>/dev/null | head -200

Repository: Ureca3/Ureca-server

Length of output: 3203


🏁 Script executed:

#!/bin/bash
# Check for session or storage of state values
rg -n "session\|state" --type java -g "**/*Auth*" -g "**/*Oauth*" | grep -i "session\|store\|save\|put" | head -40

Repository: Ureca3/Ureca-server

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Read OAuthController to see full OAuth flow implementation
cat -n ./src/main/java/com/ureca/unity/domain/auth/controller/OAuthController.java

Repository: Ureca3/Ureca-server

Length of output: 1202


🏁 Script executed:

#!/bin/bash
# Search for any state-related logic in entire auth directory
find ./src/main/java/com/ureca/unity/domain/auth -type f -name "*.java" | xargs grep -l "state" 2>/dev/null

Repository: Ureca3/Ureca-server

Length of output: 47


🏁 Script executed:

#!/bin/bash
# Check issue #6 or related documentation
find . -type f \( -name "*.md" -o -name "*.txt" \) | xargs grep -l "issue.*6\|#6\|state.*검증\|CSRF" 2>/dev/null | head -10

Repository: Ureca3/Ureca-server

Length of output: 45


OAuth 보안 검증이 완전히 누락되었습니다.

OAuth 흐름 전체에서 필수 보안 검사가 부재합니다:

  • State 검증 없음: Authorization 요청 시 state를 생성하거나 콜백에서 검증하지 않아 CSRF 공격에 취약합니다.
  • Redirect URI 검증 없음: 콜백 핸들러에서 redirectUri를 명시적으로 검증하지 않습니다(설정에서만 사용 중).
  • Authorization 요청 생성 로직 누락: 현재 구현은 코드 교환 단계만 있으며, 사용자를 카카오 인증 페이지로 리다이렉트하는 단계가 없습니다.

카카오 공식 문서(보안 가이드)에 따라:

  1. Authorization 요청 시 UUID 기반 state 생성 및 세션/캐시 저장
  2. 콜백 수신 시 code와 함께 전달된 state를 저장된 값과 비교
  3. 요청된 redirectUri가 등록된 값과 일치하는지 확인

Comment on lines +38 to +40
if (accessToken == null || accessToken.isEmpty()) {
throw new RuntimeException("Failed to get Kakao access token");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

더 구체적인 예외 타입을 사용하세요.

RuntimeException은 너무 포괄적이어서 호출부에서 적절한 에러 핸들링이 어렵습니다. OAuth 인증 실패, 네트워크 오류, API 응답 오류 등을 구분할 수 있는 커스텀 예외를 정의하세요.

예: OAuthAuthenticationException, OAuthTokenException

♻️ 커스텀 예외 예시
// domain/auth/exception/OAuthTokenException.java
public class OAuthTokenException extends RuntimeException {
    public OAuthTokenException(String message) {
        super(message);
    }
    
    public OAuthTokenException(String message, Throwable cause) {
        super(message, cause);
    }
}

사용:

 if (accessToken == null || accessToken.isEmpty()) {
-    throw new RuntimeException("Failed to get Kakao access token");
+    throw new OAuthTokenException("Failed to get Kakao access token");
 }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
@src/main/java/com/ureca/unity/domain/auth/service/oauth/KakaoOAuthClient.java
around lines 38 - 40, Replace the generic RuntimeException thrown in
KakaoOAuthClient when the access token is missing with a specific custom
exception (e.g., OAuthTokenException or OAuthAuthenticationException): create
the custom exception class (extending RuntimeException or a project base
exception) with message and cause constructors, then change the throw in the
method that obtains the token inside KakaoOAuthClient (the accessToken
null/empty check) to throw the new OAuthTokenException with a descriptive
message; update any callers or method signatures if they expect this specific
exception for clearer error handling.

Comment on lines +71 to +103
private OAuthUserInfo getUser(String accessToken) {
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(accessToken);

HttpEntity<Void> request = new HttpEntity<>(headers);

ResponseEntity<Map> response = restTemplate.exchange(
userInfoUri,
ResponseEntity<KakaoUserResponse> response = restTemplate.exchange(
"https://kapi.kakao.com/v2/user/me",
HttpMethod.GET,
request,
Map.class
KakaoUserResponse.class
);

if (response.getBody() == null || response.getBody().get("id") == null) {
throw new IllegalArgumentException("Failed to retrieve Kakao user info");
}
KakaoUserResponse body = response.getBody();
if (body == null) throw new RuntimeException("Kakao user response is null");

Map<String, Object> body = response.getBody();
Map<String, Object> kakaoAccount =
(Map<String, Object>) body.get("kakao_account");
String email = Optional.ofNullable(body.getKakaoAccount())
.map(KakaoUserResponse.KakaoAccount::getEmail)
.orElse(null);

String email = null;
String nickname = null;
String nickname = Optional.ofNullable(body.getKakaoAccount())
.map(KakaoUserResponse.KakaoAccount::getProfile)
.map(KakaoUserResponse.Profile::getNickname)
.orElse("Unknown");;

if (kakaoAccount != null) {
email = (String) kakaoAccount.get("email");

Map<String, Object> profile =
(Map<String, Object>) kakaoAccount.get("profile");
if (profile != null) {
nickname = (String) profile.get("nickname");
}
}
return new OAuthUserInfo(
OAuthProvider.KAKAO.value(),
String.valueOf(body.getId()),
body.getKakaoAccount().getEmail(),
body.getKakaoAccount().getProfile().getNickname()
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

중복된 Optional 체인과 일관성 없는 null 처리를 개선하세요.

Lines 87-94에서 body.getKakaoAccount()를 두 번 반복 조회하고 있습니다. 또한 Lines 97-102의 생성자에서는 Optional 없이 직접 접근하여 Lines 87-94의 null 체크가 무의미해집니다.

Line 100에서 body.getKakaoAccount().getEmail()getKakaoAccount()가 null일 경우 NPE가 발생합니다.

🔧 개선된 null-safe 구현
 private OAuthUserInfo getUser(String accessToken) {
     // ... (request 생성 부분 동일)
     
     KakaoUserResponse body = response.getBody();
     if (body == null) throw new RuntimeException("Kakao user response is null");
 
-    String email = Optional.ofNullable(body.getKakaoAccount())
-        .map(KakaoUserResponse.KakaoAccount::getEmail)
-        .orElse(null);
-
-    String nickname = Optional.ofNullable(body.getKakaoAccount())
-        .map(KakaoUserResponse.KakaoAccount::getProfile)
-        .map(KakaoUserResponse.Profile::getNickname)
-        .orElse("Unknown");;
-
+    KakaoUserResponse.KakaoAccount account = body.getKakaoAccount();
+    if (account == null) {
+        throw new OAuthUserInfoException("Kakao account information is missing");
+    }
+    
+    String email = account.getEmail();
+    String nickname = Optional.ofNullable(account.getProfile())
+        .map(KakaoUserResponse.Profile::getNickname)
+        .orElse("Unknown");
 
     return new OAuthUserInfo(
             OAuthProvider.KAKAO.value(),
             String.valueOf(body.getId()),
-            body.getKakaoAccount().getEmail(),
-            body.getKakaoAccount().getProfile().getNickname()
+            email,
+            nickname
     );
 }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
@src/main/java/com/ureca/unity/domain/auth/service/oauth/KakaoOAuthClient.java
around lines 71 - 103, In getUser, avoid repeating null-unsafe calls to
body.getKakaoAccount() and directly accessing nested fields; compute local
variables for kakaoAccount, email and nickname using Optional (or null checks)
once (e.g., var account = body.getKakaoAccount(); String email =
Optional.ofNullable(account).map(KakaoUserResponse.KakaoAccount::getEmail).orElse(null);
String nickname =
Optional.ofNullable(account).map(KakaoUserResponse.KakaoAccount::getProfile).map(KakaoUserResponse.Profile::getNickname).orElse("Unknown");)
and then pass those locals into the OAuthUserInfo constructor instead of calling
body.getKakaoAccount().getEmail() and
body.getKakaoAccount().getProfile().getNickname(), ensuring null-safety and
removing the duplicated optional chains and the stray extra semicolon.

@40food 40food closed this Jan 12, 2026
@40food
40food deleted the URECA-22/Feat/kakao-login branch January 12, 2026 07:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[URECA-22] Feat: 카카오 로그인

2 participants