Skip to content

[FEAT] 인가 코드 하이브리드 방식 - #67

Closed
kimyw1018 wants to merge 13 commits into
developfrom
feat/#64-social-token-exchange
Closed

[FEAT] 인가 코드 하이브리드 방식#67
kimyw1018 wants to merge 13 commits into
developfrom
feat/#64-social-token-exchange

Conversation

@kimyw1018

@kimyw1018 kimyw1018 commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

📍 개요

소셜 로그인 구현 (카카오/구글 OAuth 인가 코드 및 하이브리드 인증 방식 도입)

⛓️‍💥 관련 이슈


🛠️ 작업 내용

  • 소셜 로그인 / 회원가입 API
  • 인가 코드(Authorization Code) ↔ 소셜 Access Token 교환 로직 구축
  • 카카오 및 구글 토큰 엔드포인트 통신 로직 추가
  • 인가 코드 하이브리드 수용 구조 적용

🔥 리뷰 요청 사항

리뷰어가 중점적으로 확인해주었으면 하는 내용을 작성해주세요.

  • 인가 코드 교환 및 하이브리드 요청 구조
  • 인가코드, 액세스 토큰 둘다 가능

✅ 체크리스트

  • 코드 컨벤션을 준수했습니다.
  • 불필요한 코드 및 import를 제거했습니다.
  • 예외 처리를 적용했습니다.
  • 테스트를 완료했습니다.
  • 관련 Issue를 연결했습니다.

📎 참고 사항

Summary by CodeRabbit

  • 새로운 기능

    • 소셜 로그인에 OAuth 인증 시작 및 콜백 엔드포인트를 추가했습니다.
    • Authorization Code/Access Token 기반 처리를 모두 지원하며, 필요 시 코드 교환 후 로그인합니다.
    • 제공자별 허용 리디렉션 URI를 기준으로 인증 URL 및 프론트엔드 리다이렉트 URL을 생성합니다.
    • Google 사용자 응답에 이름을 포함합니다.
    • OAuth 토큰 응답 정보를 지원합니다.
  • 버그 수정

    • 허용되지 않은 리디렉션 URI 요청 시 올바른 오류를 반환하도록 개선했습니다.
    • OAuth 설정 누락 또는 인증 정보 부재 시 적절한 오류를 반환합니다.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

소셜 로그인에 OAuth 인가 코드 콜백과 제공자별 토큰 교환이 추가되었습니다. 자격 증명과 리다이렉트 URI 검증이 도입되었고, 로그인 결과는 설정된 프론트엔드 주소로 전달됩니다.

Changes

OAuth 로그인 흐름

Layer / File(s) Summary
OAuth 입력·설정 계약
src/main/java/com/mr/domain/auth/dto/*, src/main/java/com/mr/domain/auth/dto/req/*, src/main/java/com/mr/domain/auth/dto/res/*, src/main/java/com/mr/global/config/OAuthProperties.java
로그인 요청이 인가 코드 또는 액세스 토큰을 선택하고, OAuth 자격 증명·허용 리다이렉트 URI·토큰 및 제공자 응답 DTO가 변경되었습니다.
제공자 토큰 교환과 HTTP 구성
src/main/java/com/mr/domain/auth/service/OAuthClientService.java, src/main/java/com/mr/global/config/OAuthRestClientConfig.java, src/test/java/com/mr/domain/auth/service/OAuthClientServiceTest.java
Kakao·Google 인가 코드 교환과 사용자 정보 조회에 허용 URI 검증을 적용하고, Authorization URL 및 프론트엔드 Redirect URL 생성을 추가했습니다. 관련 오류 매핑 테스트도 추가되었습니다.
로그인·계정 연동 전달 흐름
src/main/java/com/mr/domain/auth/controller/AuthController.java, src/main/java/com/mr/domain/auth/service/AuthService.java, src/main/java/com/mr/global/security/SecurityConfig.java
로그인 시작·콜백 엔드포인트를 추가하고, 로그인 및 계정 연동이 OAuth 자격 증명과 리다이렉트 URI를 전달하도록 변경했습니다. 콜백 경로는 인증 없이 접근할 수 있습니다.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant AuthController
  participant AuthService
  participant OAuthClientService
  participant OAuthProvider
  Client->>AuthController: OAuth callback code
  AuthController->>AuthService: socialLogin(credential, redirectUri)
  AuthService->>OAuthClientService: getUserInfo(credential, redirectUri)
  OAuthClientService->>OAuthProvider: exchange code for access token
  OAuthProvider-->>OAuthClientService: OAuthTokenResponse
  OAuthClientService->>OAuthProvider: request user information
  OAuthProvider-->>AuthService: OAuthUserInfo
  AuthController-->>Client: frontend redirect with login result
Loading

Possibly related PRs

  • Musereview/BE#41: 소셜 로그인 컨트롤러·요청 DTO·인증 서비스의 초기 구조와 동일한 인증 API를 다룹니다.
  • Musereview/BE#49: 동일한 AuthController.socialLogin(...) 메서드의 보안 설정을 변경합니다.
  • Musereview/BE#61: 동일한 소셜 로그인 스택에서 자격 증명과 OAuth 서비스 API 전달을 조정합니다.

Poem

코드가 토큰으로, 토큰이 JWT로
콜백 길 따라 로그인 오네
URI도 검증하고
프론트로 슝—
OAuth 배가 닻을 내리네 🚢

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목이 인가 코드와 토큰을 함께 수용하는 하이브리드 소셜 로그인 변경을 잘 요약합니다.
Linked Issues check ✅ Passed 인가 코드 교환, 사용자 조회, JWT 발급·응답 파이프라인이 관련 컨트롤러와 서비스에 구현되어 있습니다.
Out of Scope Changes check ✅ Passed 리다이렉트 URI 검증, 콜백 엔드포인트, DTO·설정·테스트 변경이 모두 목표 기능 범위 안에 있습니다.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/#64-social-token-exchange

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@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: 2

🧹 Nitpick comments (3)
src/test/java/com/mr/domain/auth/service/OAuthClientServiceTest.java (1)

30-35: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

핵심 신규 로직(인가 코드 교환)에 대한 테스트가 없습니다.

setUp()에서 OAuthPropertiesnull로 주입하기 때문에, exchangeKakaoCode/exchangeGoogleCode는 매 테스트에서 clientId 체크로 즉시 early-return하고 원본 코드 값을 그대로 access token으로 사용합니다. 결과적으로 이번 PR의 핵심 기능인 "인가 코드 → 토큰 교환" 흐름(카카오/구글 토큰 엔드포인트 POST, 응답 검증, 예외 시 폴백)은 어떤 테스트에서도 실제로 실행되지 않습니다.

OAuthProperties를 채운 인스턴스로 별도 테스트를 추가하고 MockRestServiceServer로 카카오/구글 토큰 엔드포인트 응답을 목킹해서 성공/실패 케이스를 검증하는 걸 추천합니다. 원하시면 테스트 코드를 같이 작성해드릴 수 있어요!

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/test/java/com/mr/domain/auth/service/OAuthClientServiceTest.java` around
lines 30 - 35, Update OAuthClientServiceTest so setUp initializes
OAuthProperties with non-null Kakao and Google client IDs, then construct
OAuthClientService with those properties. Add MockRestServiceServer tests for
exchangeKakaoCode and exchangeGoogleCode covering successful token-endpoint POST
responses, response validation, and fallback behavior on endpoint failures,
ensuring the actual authorization-code-to-access-token flow executes.
src/main/java/com/mr/domain/auth/service/OAuthClientService.java (1)

55-92: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Kakao/Google 교환 로직 중복.

exchangeKakaoCodeexchangeGoogleCode는 provider 설정 조회 → redirectUri 결정 → form body 구성 → POST → accessToken 검증 → catch-fallback까지 구조가 거의 동일합니다. 공통 헬퍼(토큰 엔드포인트 URL, client_secret 처리 방식만 매개변수화)로 추출하면 유지보수가 쉬워집니다. 참고로 Kakao는 client_secret이 blank가 아닐 때만 추가하고 Google은 항상 추가하는 처리 방식 차이도 함께 정리할 좋은 기회입니다.

Also applies to: 129-163

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/mr/domain/auth/service/OAuthClientService.java` around
lines 55 - 92, Extract the duplicated provider token-exchange flow shared by
exchangeKakaoCode and exchangeGoogleCode into a common helper. Parameterize the
token endpoint and client_secret inclusion policy, preserving Kakao’s
non-blank-only behavior and Google’s always-included behavior, while retaining
each method’s provider configuration, redirect URI selection, access-token
validation, and code fallback.
src/main/java/com/mr/domain/auth/service/AuthService.java (1)

39-42: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

redirectUri 분기 로직 중복.

OAuthClientService.getUserInfo(socialType, codeOrToken, customRedirectUri)가 이미 customRedirectUri가 null/blank일 때 2-arg 버전으로 위임하므로, AuthService에서 동일한 조건을 다시 검사할 필요가 없습니다. socialLoginlinkSocialAccount 모두 항상 3-arg 오버로드만 호출하도록 단순화할 수 있습니다.

♻️ 개선 제안 예시 (socialLogin)
     public AuthResponseDTO.LoginResponse socialLogin(SocialType socialType, String codeOrToken, String redirectUri, String deviceInfo) {
-        OAuthUserInfo userInfo = (redirectUri != null && !redirectUri.isBlank())
-                ? oAuthClientService.getUserInfo(socialType, codeOrToken, redirectUri)
-                : oAuthClientService.getUserInfo(socialType, codeOrToken);
+        OAuthUserInfo userInfo = oAuthClientService.getUserInfo(socialType, codeOrToken, redirectUri);

Also applies to: 136-139

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/mr/domain/auth/service/AuthService.java` around lines 39 -
42, Remove the redirectUri null/blank branching in AuthService methods
socialLogin and linkSocialAccount, and always call
OAuthClientService.getUserInfo(socialType, codeOrToken, redirectUri) using the
3-argument overload. Preserve the existing arguments and flow while relying on
OAuthClientService to delegate when redirectUri is absent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/main/java/com/mr/domain/auth/dto/req/AuthRequestDTO.java`:
- Around line 9-27: Update SocialLoginRequest and getEffectiveCodeOrToken so the
selected input type is preserved alongside its trimmed value, distinguishing
code/authorizationCode from accessToken with an explicit enum or result type.
Update OAuthClientService to use that type and skip the provider code-exchange
request for access-token inputs while retaining exchange behavior for code-based
inputs.

In `@src/main/java/com/mr/domain/auth/service/OAuthClientService.java`:
- Around line 76-92: Update the exception handling around the OAuth token
exchange in OAuthClientService so the caught exception e is included in the
warning log with stack-trace details. Distinguish expected invalid-code/provider
4xx failures from configuration, network, and 5xx failures: only fall back to
returning code for the former, while propagating the latter instead of treating
them as AccessTokens; apply the same behavior to the corresponding catch block
noted around the alternate provider flow.

---

Nitpick comments:
In `@src/main/java/com/mr/domain/auth/service/AuthService.java`:
- Around line 39-42: Remove the redirectUri null/blank branching in AuthService
methods socialLogin and linkSocialAccount, and always call
OAuthClientService.getUserInfo(socialType, codeOrToken, redirectUri) using the
3-argument overload. Preserve the existing arguments and flow while relying on
OAuthClientService to delegate when redirectUri is absent.

In `@src/main/java/com/mr/domain/auth/service/OAuthClientService.java`:
- Around line 55-92: Extract the duplicated provider token-exchange flow shared
by exchangeKakaoCode and exchangeGoogleCode into a common helper. Parameterize
the token endpoint and client_secret inclusion policy, preserving Kakao’s
non-blank-only behavior and Google’s always-included behavior, while retaining
each method’s provider configuration, redirect URI selection, access-token
validation, and code fallback.

In `@src/test/java/com/mr/domain/auth/service/OAuthClientServiceTest.java`:
- Around line 30-35: Update OAuthClientServiceTest so setUp initializes
OAuthProperties with non-null Kakao and Google client IDs, then construct
OAuthClientService with those properties. Add MockRestServiceServer tests for
exchangeKakaoCode and exchangeGoogleCode covering successful token-endpoint POST
responses, response validation, and fallback behavior on endpoint failures,
ensuring the actual authorization-code-to-access-token flow executes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 28f01d6d-cc1d-4437-9f55-9d9ac7a2ac03

📥 Commits

Reviewing files that changed from the base of the PR and between ae31523 and 1270274.

📒 Files selected for processing (11)
  • src/main/java/com/mr/domain/auth/controller/AuthController.java
  • src/main/java/com/mr/domain/auth/dto/req/AuthRequestDTO.java
  • src/main/java/com/mr/domain/auth/dto/res/GoogleUserResponse.java
  • src/main/java/com/mr/domain/auth/dto/res/KakaoUserResponse.java
  • src/main/java/com/mr/domain/auth/dto/res/OAuthTokenResponse.java
  • src/main/java/com/mr/domain/auth/service/AuthService.java
  • src/main/java/com/mr/domain/auth/service/OAuthClientService.java
  • src/main/java/com/mr/domain/notification/service/NotificationEventListener.java
  • src/main/java/com/mr/global/config/OAuthProperties.java
  • src/main/java/com/mr/global/config/OAuthRestClientConfig.java
  • src/test/java/com/mr/domain/auth/service/OAuthClientServiceTest.java

Comment thread src/main/java/com/mr/domain/auth/dto/req/AuthRequestDTO.java
Comment thread src/main/java/com/mr/domain/auth/service/OAuthClientService.java

@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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/java/com/mr/domain/auth/service/OAuthClientService.java (1)

77-82: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

clientId 미설정 시 인가 코드를 그대로 반환하는 fallback, 설정 오류를 숨깁니다.

exchangeKakaoCode(그리고 동일 패턴의 exchangeGoogleCode, 153-158번째 줄)는 resolveAccessToken에서 credential.type() == AUTHORIZATION_CODE일 때만 호출됩니다. 즉 이 시점에는 이미 "토큰 교환이 필요한 인가 코드"라는 사실이 확정된 상태인데, clientId가 비어 있으면 조용히 code를 그대로 반환해 뒤이어 Kakao/Google 사용자 정보 API에 Bearer 토큰처럼 전송됩니다. 실제로는 401 등 원격 API 오류로만 드러나 "OAuth 설정 누락"이라는 진짜 원인을 파악하기 어렵게 만듭니다.

설정 누락은 조용히 넘기지 말고 명확한 예외로 실패시키는 것이 디버깅과 운영에 유리합니다.

🛡️ 제안 수정
     private String exchangeKakaoCode(String code, String customRedirectUri) {
         OAuthProperties.ProviderProperties kakaoProps = oAuthProperties != null ? oAuthProperties.kakao() : null;
         String clientId = kakaoProps != null ? kakaoProps.clientId() : null;
         if (clientId == null || clientId.isBlank()) {
-            return code;
+            throw new GeneralException(AuthErrorStatus.OAUTH_CLIENT_ERROR);
         }

exchangeGoogleCode(153-158번째 줄)에도 동일하게 적용해 주세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/mr/domain/auth/service/OAuthClientService.java` around
lines 77 - 82, Update exchangeKakaoCode and exchangeGoogleCode so a missing or
blank clientId throws a clear configuration-related exception instead of
returning the authorization code; preserve the existing token-exchange flow when
clientId is configured.
🧹 Nitpick comments (3)
src/main/java/com/mr/domain/auth/service/AuthService.java (2)

50-61: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

fetchOAuthUserInfo의 분기 로직, OAuthClientService와 중복됩니다.

OAuthClientService.getUserInfo(SocialType, OAuthCredential, String) (3-인자 버전)은 이미 credential.type() == ACCESS_TOKEN 여부와 customRedirectUri의 null/blank 여부를 내부에서 완전히 처리합니다(예: exchangeKakaoCode/exchangeGoogleCode 내부의 customRedirectUri != null && !customRedirectUri.isBlank() 검사). 그래서 fetchOAuthUserInfo가 동일한 조건을 다시 검사해 4갈래로 분기할 필요가 없습니다. redirectUri가 null이든 빈 문자열이든 결과는 동일하므로, 아래처럼 한 줄로 줄여도 동작이 같습니다.

♻️ 제안 리팩터
     private OAuthUserInfo fetchOAuthUserInfo(SocialType socialType, OAuthCredential credential, String redirectUri) {
-        if (credential.type() == OAuthCredential.CredentialType.ACCESS_TOKEN) {
-            if (redirectUri != null && !redirectUri.isBlank()) {
-                return oAuthClientService.getUserInfo(socialType, credential.value(), redirectUri);
-            }
-            return oAuthClientService.getUserInfo(socialType, credential.value());
-        }
-        if (redirectUri != null && !redirectUri.isBlank()) {
-            return oAuthClientService.getUserInfo(socialType, credential, redirectUri);
-        }
-        return oAuthClientService.getUserInfo(socialType, credential);
+        return oAuthClientService.getUserInfo(socialType, credential, redirectUri);
     }

DRY(Don't Repeat Yourself) 원칙 관점에서 서비스 계층의 책임을 클라이언트 계층에 위임하는 편이 유지보수에 유리합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/mr/domain/auth/service/AuthService.java` around lines 50 -
61, Update fetchOAuthUserInfo to delegate directly to
OAuthClientService.getUserInfo(SocialType, OAuthCredential, String) without
branching on credential.type() or redirectUri null/blank status. Preserve the
existing inputs so OAuthClientService remains responsible for selecting the
credential flow and handling redirect URI normalization.

63-69: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

레거시 codeOrToken 계열 메서드들을 제거하세요.

현재 컨트롤러는 모두 OAuthCredential 계열 시그니처로 호출합니다. 이 오버로드들은 codeOrToken 이름과 인증 코드 문맥을 유지하지만 CredentialType.ACCESS_TOKEN으로 강제 랩핑하기 때문에, 만약 실제 인가 코드가 들어오면 토큰 교환 없이 사용자 정보 조회 경로를 타게 됩니다. OAuth 인가코드/엑세스토큰 처리를 명확히 구분한 하나의 메서드 흐름을 남겨 두는 편이 좋습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/mr/domain/auth/service/AuthService.java` around lines 63 -
69, Remove the legacy socialLogin overloads accepting codeOrToken from
AuthService, including the variants with and without redirectUri. Preserve the
OAuthCredential-based socialLogin flow as the sole entry point, and update any
remaining callers to construct and pass the appropriate credential type
explicitly.
src/main/java/com/mr/domain/auth/dto/OAuthCredential.java (1)

3-11: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

정적 팩토리 메서드로 반복 생성 패턴 정리 추천 (칭찬 먼저: 하이브리드 인증 계약을 record 하나로 깔끔하게 표현한 점 좋습니다 👍)

new OAuthCredential(CredentialType.ACCESS_TOKEN, value) 형태가 AuthService.java(64, 196번째 줄)와 OAuthClientService.java(58, 59번째 줄)에서 반복 등장합니다. ofAccessToken/ofAuthorizationCode 같은 정적 팩토리 메서드를 추가하면 호출부 가독성이 좋아지고, 향후 CredentialType 처리 로직이 바뀌어도 한 곳만 수정하면 됩니다.

♻️ 제안 리팩터
 public record OAuthCredential(
         CredentialType type,
         String value
 ) {
     public enum CredentialType {
         AUTHORIZATION_CODE,
         ACCESS_TOKEN
     }
+
+    public static OAuthCredential ofAccessToken(String value) {
+        return new OAuthCredential(CredentialType.ACCESS_TOKEN, value);
+    }
+
+    public static OAuthCredential ofAuthorizationCode(String value) {
+        return new OAuthCredential(CredentialType.AUTHORIZATION_CODE, value);
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/mr/domain/auth/dto/OAuthCredential.java` around lines 3 -
11, Update the OAuthCredential record with static factory methods such as
ofAccessToken and ofAuthorizationCode that create credentials with the
corresponding CredentialType, then replace direct constructor calls in
AuthService and OAuthClientService with these methods.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/main/java/com/mr/domain/auth/service/OAuthClientService.java`:
- Around line 110-116: Update the warning logs in the Kakao authorization-code
exchange catch block and the corresponding exchangeGoogleCode catch block to
stop recording the raw code value. Preserve the original exception details and
replace the code placeholder with non-sensitive context, such as a generic
exchange-failure message or a safely redacted identifier.

---

Outside diff comments:
In `@src/main/java/com/mr/domain/auth/service/OAuthClientService.java`:
- Around line 77-82: Update exchangeKakaoCode and exchangeGoogleCode so a
missing or blank clientId throws a clear configuration-related exception instead
of returning the authorization code; preserve the existing token-exchange flow
when clientId is configured.

---

Nitpick comments:
In `@src/main/java/com/mr/domain/auth/dto/OAuthCredential.java`:
- Around line 3-11: Update the OAuthCredential record with static factory
methods such as ofAccessToken and ofAuthorizationCode that create credentials
with the corresponding CredentialType, then replace direct constructor calls in
AuthService and OAuthClientService with these methods.

In `@src/main/java/com/mr/domain/auth/service/AuthService.java`:
- Around line 50-61: Update fetchOAuthUserInfo to delegate directly to
OAuthClientService.getUserInfo(SocialType, OAuthCredential, String) without
branching on credential.type() or redirectUri null/blank status. Preserve the
existing inputs so OAuthClientService remains responsible for selecting the
credential flow and handling redirect URI normalization.
- Around line 63-69: Remove the legacy socialLogin overloads accepting
codeOrToken from AuthService, including the variants with and without
redirectUri. Preserve the OAuthCredential-based socialLogin flow as the sole
entry point, and update any remaining callers to construct and pass the
appropriate credential type explicitly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ca1bbbe0-f2e3-4bf1-a6f5-7050c027e69f

📥 Commits

Reviewing files that changed from the base of the PR and between 1270274 and 6f8a91c.

📒 Files selected for processing (5)
  • src/main/java/com/mr/domain/auth/controller/AuthController.java
  • src/main/java/com/mr/domain/auth/dto/OAuthCredential.java
  • src/main/java/com/mr/domain/auth/dto/req/AuthRequestDTO.java
  • src/main/java/com/mr/domain/auth/service/AuthService.java
  • src/main/java/com/mr/domain/auth/service/OAuthClientService.java
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main/java/com/mr/domain/auth/controller/AuthController.java
  • src/main/java/com/mr/domain/auth/dto/req/AuthRequestDTO.java

Comment thread src/main/java/com/mr/domain/auth/service/OAuthClientService.java
Comment thread src/main/java/com/mr/domain/auth/service/OAuthClientService.java Outdated
Comment thread src/main/java/com/mr/domain/auth/service/OAuthClientService.java

@on1yoneprivate on1yoneprivate left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

코멘트 확인 부탁드립니다~

@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: 1

🧹 Nitpick comments (1)
src/test/java/com/mr/domain/auth/service/OAuthClientServiceTest.java (1)

158-168: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Google 분기와 실제 토큰 교환 성공 경로도 테스트로 고정해 주세요.

현재 신규 테스트는 Kakao의 설정 누락만 검증합니다. Google의 동일한 client_id 누락 분기와 authorization-code 요청의 redirect_uri, form body, access_token 응답 매핑은 회귀 시 감지되지 않습니다. Google 대응 테스트와 각 provider의 성공적인 MockRestServiceServer 교환 테스트를 추가해 주세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/test/java/com/mr/domain/auth/service/OAuthClientServiceTest.java` around
lines 158 - 168, OAuthClientServiceTest의
exchangeCode_missingConfig_throwsOauthServerError만으로는 Google 분기와 성공 경로가 검증되지
않으므로, Google의 client_id 누락 시 OAUTH_SERVER_ERROR가 발생하는 테스트를 추가하세요. 또한 Kakao와
Google 각각에 MockRestServiceServer를 사용한 authorization-code 토큰 교환 성공 테스트를 추가해
redirect_uri와 form body가 올바르게 전송되고 access_token 응답이 정상 매핑되는지 검증하세요.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/main/java/com/mr/domain/auth/service/OAuthClientService.java`:
- Around line 81-82: OAuth token exchange must reject missing redirect URIs. In
the flows around the Kakao and Google redirectUri calculations, validate the
resolved redirectUri immediately afterward for null or blank values, log the
configuration error consistently, and throw AuthErrorStatus.OAUTH_SERVER_ERROR
before issuing the token request.

---

Nitpick comments:
In `@src/test/java/com/mr/domain/auth/service/OAuthClientServiceTest.java`:
- Around line 158-168: OAuthClientServiceTest의
exchangeCode_missingConfig_throwsOauthServerError만으로는 Google 분기와 성공 경로가 검증되지
않으므로, Google의 client_id 누락 시 OAUTH_SERVER_ERROR가 발생하는 테스트를 추가하세요. 또한 Kakao와
Google 각각에 MockRestServiceServer를 사용한 authorization-code 토큰 교환 성공 테스트를 추가해
redirect_uri와 form body가 올바르게 전송되고 access_token 응답이 정상 매핑되는지 검증하세요.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b412b0b3-aa8a-42d3-807c-5706e252c4b8

📥 Commits

Reviewing files that changed from the base of the PR and between 6f8a91c and ac1469b.

📒 Files selected for processing (2)
  • src/main/java/com/mr/domain/auth/service/OAuthClientService.java
  • src/test/java/com/mr/domain/auth/service/OAuthClientServiceTest.java

Comment thread src/main/java/com/mr/domain/auth/service/OAuthClientService.java
-social-token-exchange

# Conflicts:
#	src/main/java/com/mr/domain/notification/service/NotificationEventListener.java

@p1001q p1001q left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

코멘트 확인 부탁드려용~

Comment thread src/main/java/com/mr/domain/auth/service/OAuthClientService.java Outdated
Comment thread src/main/java/com/mr/domain/auth/dto/req/AuthRequestDTO.java Outdated

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/main/java/com/mr/domain/auth/service/AuthService.java (2)

64-69: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

두 String 오버로드가 인가 코드 입력을 지원하지 않습니다. 두 메서드 모두 codeOrToken을 무조건 ACCESS_TOKEN으로 포장하므로, 인가 코드가 전달되면 잘못된 제공자 API 경로로 라우팅됩니다.

  • src/main/java/com/mr/domain/auth/service/AuthService.java#L64-L69: Access Token 전용 API로 명확히 하거나 OAuthCredential 오버로드를 사용하세요.
  • src/main/java/com/mr/domain/auth/service/AuthService.java#L196-L197: 계정 연동도 동일하게 OAuthCredentialredirectUri를 유지하세요.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/mr/domain/auth/service/AuthService.java` around lines 64 -
69, Update AuthService.socialLogin at
src/main/java/com/mr/domain/auth/service/AuthService.java:64-69 to make the
String overload explicitly access-token-only or delegate through the
OAuthCredential overload without claiming authorization-code support; update the
account-linking flow at
src/main/java/com/mr/domain/auth/service/AuthService.java:196-197 to preserve
OAuthCredential and redirectUri so authorization-code requests use the correct
provider API path.

40-48: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

OAuth 호출과 retry를 비트랜잭션으로 분리하세요.

socialLogin에 붙은 @Transactional은 클래스의 @Transactional(readOnly = true)를 덮어쓰지만, 여전히 TransactionTemplate의 기본 전파 REQUIRED와 같은 트랜잭션에 참여합니다. 여기서 중복 소셜 ID가 발생하면 DataIntegrityViolationException 후 해당 트랜잭션이 rollback-only 상태가 되므로, catch 후 실행되는 기존 사용자 retry도 같은 트랜잭션 커밋 시점에 UnexpectedRollbackException으로 실패할 수 있습니다.

@Transactional(propagation = Propagation.NOT_SUPPORTED)으로 오버런 메서드를 비트랜잭션으로 두고, 각 저장/재시도 경로를 독립 TransactionTemplate(REQUIRES_NEW) 또는 별도 트랜잭션 경계로 구성하세요. 해당 트랜잭션 전파 설정과 중복 소셜 가입 fallback은 통합 테스트로 확인해 주세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/mr/domain/auth/service/AuthService.java` around lines 40 -
48, Update AuthService.socialLogin to use `@Transactional`(propagation =
Propagation.NOT_SUPPORTED), keeping OAuth retrieval and retry execution outside
a surrounding transaction. Configure the transaction boundaries used by
executeSocialLogin and executeSocialLoginForExistingUser to run independently
with REQUIRES_NEW, so a DataIntegrityViolationException cannot mark the fallback
transaction rollback-only. Add an integration test covering duplicate social
signup fallback and successful retry.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/main/java/com/mr/domain/auth/service/AuthService.java`:
- Around line 64-69: Update AuthService.socialLogin at
src/main/java/com/mr/domain/auth/service/AuthService.java:64-69 to make the
String overload explicitly access-token-only or delegate through the
OAuthCredential overload without claiming authorization-code support; update the
account-linking flow at
src/main/java/com/mr/domain/auth/service/AuthService.java:196-197 to preserve
OAuthCredential and redirectUri so authorization-code requests use the correct
provider API path.
- Around line 40-48: Update AuthService.socialLogin to use
`@Transactional`(propagation = Propagation.NOT_SUPPORTED), keeping OAuth retrieval
and retry execution outside a surrounding transaction. Configure the transaction
boundaries used by executeSocialLogin and executeSocialLoginForExistingUser to
run independently with REQUIRES_NEW, so a DataIntegrityViolationException cannot
mark the fallback transaction rollback-only. Add an integration test covering
duplicate social signup fallback and successful retry.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 2e12a57e-8ece-437c-b73a-30a770335384

📥 Commits

Reviewing files that changed from the base of the PR and between ac1469b and 5d5866e.

📒 Files selected for processing (1)
  • src/main/java/com/mr/domain/auth/service/AuthService.java

@kimyw1018 kimyw1018 closed this Jul 30, 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: 3

🧹 Nitpick comments (5)
src/main/java/com/mr/domain/auth/service/OAuthClientService.java (2)

40-66: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

getUserInfo 오버로드 간 분기 로직이 중복됩니다.

2-인자 getUserInfo(SocialType, OAuthCredential)(라인 50-55)가 credential.type()을 다시 검사한 뒤 3-인자 버전으로 위임하는데, 3-인자 getUserInfo(SocialType, OAuthCredential, String)(라인 40-48)가 이미 동일한 타입 분기를 내부적으로 처리합니다. customRedirectUrinull이어도 resolveRedirectUri가 기본값(allowedUris.get(0))으로 폴백하므로, 그냥 null을 넘겨 3-인자 버전에 위임해도 동작은 동일합니다. 지금처럼 같은 로직이 두 곳에 있으면 한쪽만 수정되고 다른 쪽이 방치되는 사고가 나기 쉽습니다.

♻️ 개선 제안
     public OAuthUserInfo getUserInfo(SocialType socialType, OAuthCredential credential) {
-        if (credential.type() == OAuthCredential.CredentialType.ACCESS_TOKEN) {
-            return getUserInfo(socialType, credential.value());
-        }
-        return getUserInfo(socialType, credential, null);
+        return getUserInfo(socialType, credential, null);
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/mr/domain/auth/service/OAuthClientService.java` around
lines 40 - 66, Remove the redundant credential-type branch from the two-argument
getUserInfo(SocialType, OAuthCredential) overload and delegate directly to the
three-argument getUserInfo(SocialType, OAuthCredential, String) with a null
customRedirectUri, preserving the existing default redirect resolution.

264-277: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

FQN을 매번 풀네임으로 쓰는 대신 import를 추가하는 게 어떨까요?

org.springframework.web.util.UriComponentsBuilder, com.mr.domain.auth.dto.res.AuthResponseDTO가 파일 곳곳에서 완전정규명으로 인라인 사용됩니다. 기능엔 영향 없지만 가독성을 위해 상단에 import로 올려주시면 좋겠습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/mr/domain/auth/service/OAuthClientService.java` around
lines 264 - 277, Replace the fully qualified AuthResponseDTO and
UriComponentsBuilder references used in OAuthClientService, including
buildFrontendRedirectUrl, with imports declared at the top of the file; preserve
the existing behavior and signatures.
src/test/java/com/mr/domain/auth/service/OAuthClientServiceTest.java (1)

159-190: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

redirect_uri 화이트리스트 테스트, 꼼꼼하게 잘 짜셨네요! 👍

missingConfig/invalidCustomRedirectUri 케이스를 정확히 짚었습니다. 다만 이번에 새로 추가된 getAuthorizationUrl, buildFrontendRedirectUrl에 대한 테스트가 보이지 않습니다. 특히 getAuthorizationUrl은 URL 인코딩(scope 공백)과 (향후 추가될) state 파라미터 검증에, buildFrontendRedirectUrl은 쿼리파라미터 구성 검증에 활용도가 높으니 추가해 주시면 회귀 방지에 도움이 될 것 같습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/test/java/com/mr/domain/auth/service/OAuthClientServiceTest.java` around
lines 159 - 190, OAuthClientServiceTest에 새로 추가된 getAuthorizationUrl과
buildFrontendRedirectUrl 테스트를 보강하세요. getAuthorizationUrl은 scope의 공백이 올바르게 URL
인코딩되는지와 state 파라미터가 포함되는지 검증하고, buildFrontendRedirectUrl은 반환 URL에 필요한 쿼리 파라미터가
정확히 구성되는지 검증하는 테스트를 추가하세요.
src/main/java/com/mr/domain/auth/service/AuthService.java (2)

52-63: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

fetchOAuthUserInfo의 분기가 OAuthClientService.getUserInfo와 중복됩니다.

credential.type()redirectUri 공백 여부로 4갈래 분기를 하고 있는데, oAuthClientService.getUserInfo(socialType, credential, redirectUri) (3-인자, OAuthCredential 버전)가 이미 ACCESS_TOKEN/AUTHORIZATION_CODE 분기와 redirectUrinull/blank일 때의 기본값 처리를 내부적으로 다 수행합니다. 그냥 위임해도 동일하게 동작합니다.

♻️ 개선 제안
     private OAuthUserInfo fetchOAuthUserInfo(SocialType socialType, OAuthCredential credential, String redirectUri) {
-        if (credential.type() == OAuthCredential.CredentialType.ACCESS_TOKEN) {
-            if (redirectUri != null && !redirectUri.isBlank()) {
-                return oAuthClientService.getUserInfo(socialType, credential.value(), redirectUri);
-            }
-            return oAuthClientService.getUserInfo(socialType, credential.value());
-        }
-        if (redirectUri != null && !redirectUri.isBlank()) {
-            return oAuthClientService.getUserInfo(socialType, credential, redirectUri);
-        }
-        return oAuthClientService.getUserInfo(socialType, credential);
+        return oAuthClientService.getUserInfo(socialType, credential, redirectUri);
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/mr/domain/auth/service/AuthService.java` around lines 52 -
63, Update fetchOAuthUserInfo to delegate directly to the three-argument
OAuthClientService.getUserInfo(socialType, credential, redirectUri) overload,
removing the credential type and redirectUri branching while preserving the
service’s existing default handling.

65-67: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

self-invocation에서 @Transactional을 회피하지 않도록 분리하세요.

3-인자 socialLogin(SocialType, String, String)은 내부 4-인자 메서드로 this.socialLogin(...)를 호출합니다. Spring의 AOP 프록시는 다른 내부 메서드 호출에서는 동작하지 않으므로, 이 경로가 실행되면 4-인자 @Transactional 설정이 적용되지 않고 클래스 기본값처럼 동작할 수 있습니다. 분리 서비스 빈 호출이나 자기 주입 자원을 통해 외부 호출처럼 프록시를 타게 하면 Spring AOP 트랜잭션 문서에서 권장하는 방식으로 안전하게 고칠 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/mr/domain/auth/service/AuthService.java` around lines 65 -
67, Update the 3-argument socialLogin(SocialType, String, String) path so it
delegates to the transactional 4-argument socialLogin through a separate Spring
service bean or injected self-proxy, rather than direct self-invocation.
Preserve the existing OAuthCredential construction and arguments while ensuring
the `@Transactional` method is invoked through the AOP proxy.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/main/java/com/mr/domain/auth/controller/AuthController.java`:
- Around line 62-86: Update AuthController.oAuthCallback so invalid OAuth
callbacks (non-null error or missing/blank code) redirect to the frontend
instead of throwing GeneralException, passing an appropriate error code as a
query parameter while preserving normal login redirects. Follow the existing
startOAuthLogin redirect/error-handling pattern and keep the state-parameter
validation concern separate.

In `@src/main/java/com/mr/domain/auth/service/OAuthClientService.java`:
- Around line 264-277: Update buildFrontendRedirectUrl so it never places
accessToken or refreshToken in the redirect query parameters. Replace the
raw-token parameters with a single-use exchange code that the frontend can
redeem through the established secure HTTPS POST flow, or deliver refreshToken
through an HttpOnly Secure cookie while retaining only non-sensitive response
data in the URL.
- Around line 235-262: Update getAuthorizationUrl to generate a
cryptographically random, unpredictable state before building the Kakao or
Google authorization URL, store it with a short TTL keyed to the login attempt,
and include it as the state query parameter. Extend oAuthCallback to retrieve
and compare the stored state with the callback value, rejecting missing or
mismatched values before processing the authorization code.

---

Nitpick comments:
In `@src/main/java/com/mr/domain/auth/service/AuthService.java`:
- Around line 52-63: Update fetchOAuthUserInfo to delegate directly to the
three-argument OAuthClientService.getUserInfo(socialType, credential,
redirectUri) overload, removing the credential type and redirectUri branching
while preserving the service’s existing default handling.
- Around line 65-67: Update the 3-argument socialLogin(SocialType, String,
String) path so it delegates to the transactional 4-argument socialLogin through
a separate Spring service bean or injected self-proxy, rather than direct
self-invocation. Preserve the existing OAuthCredential construction and
arguments while ensuring the `@Transactional` method is invoked through the AOP
proxy.

In `@src/main/java/com/mr/domain/auth/service/OAuthClientService.java`:
- Around line 40-66: Remove the redundant credential-type branch from the
two-argument getUserInfo(SocialType, OAuthCredential) overload and delegate
directly to the three-argument getUserInfo(SocialType, OAuthCredential, String)
with a null customRedirectUri, preserving the existing default redirect
resolution.
- Around line 264-277: Replace the fully qualified AuthResponseDTO and
UriComponentsBuilder references used in OAuthClientService, including
buildFrontendRedirectUrl, with imports declared at the top of the file; preserve
the existing behavior and signatures.

In `@src/test/java/com/mr/domain/auth/service/OAuthClientServiceTest.java`:
- Around line 159-190: OAuthClientServiceTest에 새로 추가된 getAuthorizationUrl과
buildFrontendRedirectUrl 테스트를 보강하세요. getAuthorizationUrl은 scope의 공백이 올바르게 URL
인코딩되는지와 state 파라미터가 포함되는지 검증하고, buildFrontendRedirectUrl은 반환 URL에 필요한 쿼리 파라미터가
정확히 구성되는지 검증하는 테스트를 추가하세요.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ee83c145-fe38-406a-acf5-94c90e370f3e

📥 Commits

Reviewing files that changed from the base of the PR and between 5d5866e and 7f0259c.

📒 Files selected for processing (7)
  • src/main/java/com/mr/domain/auth/controller/AuthController.java
  • src/main/java/com/mr/domain/auth/dto/req/AuthRequestDTO.java
  • src/main/java/com/mr/domain/auth/service/AuthService.java
  • src/main/java/com/mr/domain/auth/service/OAuthClientService.java
  • src/main/java/com/mr/global/config/OAuthProperties.java
  • src/main/java/com/mr/global/security/SecurityConfig.java
  • src/test/java/com/mr/domain/auth/service/OAuthClientServiceTest.java

Comment on lines +62 to +86
@GetMapping("/{socialType}/callback")
public void oAuthCallback(
@Parameter(description = "소셜 로그인 제공자 (KAKAO, GOOGLE)", example = "KAKAO")
@PathVariable(name = "socialType") SocialType socialType,
@RequestParam(name = "code", required = false) String code,
@RequestParam(name = "error", required = false) String error,
@RequestParam(name = "redirectUri", required = false) String customRedirectUri,
@RequestHeader(value = HttpHeaders.USER_AGENT, defaultValue = "Unknown Device") String deviceInfo,
HttpServletResponse response
) throws IOException {
if (error != null || code == null || code.isBlank()) {
throw new GeneralException(AuthErrorStatus.INVALID_AUTH_REQUEST);
}

OAuthCredential credential = new OAuthCredential(OAuthCredential.CredentialType.AUTHORIZATION_CODE, code);
AuthResponseDTO.LoginResponse loginResponse = authService.socialLogin(
socialType,
credential,
customRedirectUri,
deviceInfo
);

String targetFrontendUrl = oAuthClientService.buildFrontendRedirectUrl(loginResponse);
response.sendRedirect(targetFrontendUrl);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

콜백 실패 시에도 프론트엔드로 리다이렉트해 주세요.

error가 있거나 code가 비어있을 때 GeneralException을 던지는데, 이 엔드포인트는 사용자의 브라우저가 직접 GET으로 도달하는 리다이렉트 타겟입니다(302로 이 URL에 도착). 예외를 던지면 브라우저에 원시 JSON 에러 응답이 그대로 표시되어(전역 예외 핸들러 응답 형식 그대로), 사용자는 "로그인 취소" 같은 정상적인 흐름에서도 개발자용 에러 화면을 보게 됩니다.

startOAuthLogin처럼 프론트엔드 URL로 sendRedirect하되, 에러 코드를 쿼리파라미터로 실어 보내는 방식을 권장합니다.

또한 이 콜백은 OAuth 표준의 state 파라미터 검증이 빠져 있어 인가 코드 CSRF에 취약합니다(별도로도 짚었습니다).

🛠️ 개선 제안 예시
         if (error != null || code == null || code.isBlank()) {
-            throw new GeneralException(AuthErrorStatus.INVALID_AUTH_REQUEST);
+            String errorRedirectUrl = oAuthClientService.buildFrontendErrorRedirectUrl(error != null ? error : "invalid_request");
+            response.sendRedirect(errorRedirectUrl);
+            return;
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/mr/domain/auth/controller/AuthController.java` around lines
62 - 86, Update AuthController.oAuthCallback so invalid OAuth callbacks
(non-null error or missing/blank code) redirect to the frontend instead of
throwing GeneralException, passing an appropriate error code as a query
parameter while preserving normal login redirects. Follow the existing
startOAuthLogin redirect/error-handling pattern and keep the state-parameter
validation concern separate.

Comment on lines +235 to +262
public String getAuthorizationUrl(SocialType socialType, String customRedirectUri) {
OAuthProperties.ProviderProperties props = oAuthProperties != null ? switch (socialType) {
case KAKAO -> oAuthProperties.kakao();
case GOOGLE -> oAuthProperties.google();
} : null;

String clientId = props != null ? props.clientId() : null;
if (clientId == null || clientId.isBlank()) {
log.error("{} OAuth configuration is missing (client_id is not set)", socialType);
throw new GeneralException(AuthErrorStatus.OAUTH_SERVER_ERROR);
}

String redirectUri = resolveRedirectUri(props, customRedirectUri, socialType.name());

return switch (socialType) {
case KAKAO -> org.springframework.web.util.UriComponentsBuilder.fromUriString("https://kauth.kakao.com/oauth/authorize")
.queryParam("response_type", "code")
.queryParam("client_id", clientId)
.queryParam("redirect_uri", redirectUri)
.build().toUriString();
case GOOGLE -> org.springframework.web.util.UriComponentsBuilder.fromUriString("https://accounts.google.com/o/oauth2/v2/auth")
.queryParam("response_type", "code")
.queryParam("client_id", clientId)
.queryParam("redirect_uri", redirectUri)
.queryParam("scope", "email profile openid")
.build().toUriString();
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🌐 Web query:

OAuth 2.0 authorization code flow state parameter CSRF protection best practice

💡 Result:

To protect against Cross-Site Request Forgery (CSRF) in the OAuth 2.0 authorization code flow, the industry-standard best practice is to ensure that every authorization request is cryptographically bound to the user-agent session [1][2][3]. According to the latest OAuth 2.0 Security Best Current Practice (RFC 9700), clients must implement one of the following mechanisms to prevent CSRF [1][2][3]: 1. Proof Key for Code Exchange (PKCE): If a client has confirmed that the authorization server supports PKCE, it may rely on the CSRF protection provided by PKCE [1][2][3]. The PKCE code challenge and verifier process ensures that the authorization code is bound to the specific client transaction [4]. 2. OpenID Connect Nonce: In OpenID Connect (OIDC) flows, the 'nonce' parameter serves as a cryptographically bound, transaction-specific value that provides CSRF protection [1][2][3]. 3. State Parameter: If PKCE or OIDC 'nonce' is not used (or if the authorization server does not support them), the client must use the 'state' parameter [1][2][3]. When using the 'state' parameter, follow these best practices: - Uniqueness and Opacity: The value must be a high-entropy, unpredictable, and transaction-specific string (a CSRF token) [5][6]. - Binding: The value must be securely bound to the user-agent session (e.g., stored in a cryptographically signed cookie or an encrypted server-side session) [1][7][5]. - Validation: Upon receiving the authorization response, the client must verify that the 'state' value returned by the authorization server matches the value it stored in the user-agent session [5][6][8]. If the values do not match, the client must reject the request as a potential CSRF attack [5][6]. - Tamper Resistance: If the 'state' parameter is used to carry additional application state, ensure the integrity of the data is protected against tampering and swapping, for example, by signing or encrypting the contents [2][9]. In summary, while the 'state' parameter was the traditional method for CSRF protection, modern implementations favor PKCE as a robust, built-in mechanism for securing the authorization code flow [1][7][2]. Always ensure the authorization server supports the chosen protection mechanism before relying on it [1][2][9].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate relevant files =="
fd -i 'OAuthClientService.java|OauthCallback|OAuth.*Callback|Auth|Security|Config' . | sed -n '1,120p'

echo
echo "== search state/code/callback usages =="
rg -n "OAuthClientService|getAuthorizationUrl|redirect_uri|authorization_code|state|scope|code|oauth|oauth2|OauthCallback|OAuthCallback" -S . \
  -g '!build' -g '!target' -g '!node_modules' | sed -n '1,240p'

echo
echo "== Inspect OAuthClientService relevant section =="
FILE="$(fd -i 'OAuthClientService.java' . | head -n 1 || true)"
if [ -n "${FILE:-}" ]; then
  echo "-- $FILE lines 1-340 --"
  cat -n "$FILE" | sed -n '1,340p'
fi

echo
echo "== Inspect callback/controller/service candidates =="
for f in $(fd -i '*Callback*.java|*OAuth*Controller.java|*OAuth*Service.java|*AuthService.java' . | sed -n '1,40p'); do
  echo "-- $f line count --"
  wc -l "$f"
  echo "-- $f outline --"
  ast-grep outline "$f" --view compact 2>/dev/null | sed -n '1,120p' || true
done

Repository: Musereview/BE

Length of output: 2008


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== pwd =="
pwd
echo
echo "== top-level entries =="
ls -la | sed -n '1,120p'
echo
echo "== git availability =="
git status || true
echo
echo "== locate relevant files =="
fd -i 'OAuthClientService.java|OauthCallback|OAuth.*Callback|Auth|Security|Config' . | sed -n '1,160p' || true

echo
echo "== candidate Java files for OAuth handling =="
fd -i '(OAuthClientService|OAuth.*Callback|Oauth.*Callback|.*OAuth.*Service|.*OAuth.*Controller|.*Auth.*Service|.*Auth.*Controller).*\.java' . | sed -n '1,200p' || true

echo
echo "== exact target =="
if [ -f src/main/java/com/mr/domain/auth/service/OAuthClientService.java ]; then
  wc -l src/main/java/com/mr/domain/auth/service/OAuthClientService.java
  cat -n src/main/java/com/mr/domain/auth/service/OAuthClientService.java | sed -n '1,360p'
else
  echo "target file not found at expected path or repository unavailable"
fi

Repository: Musereview/BE

Length of output: 2008


🌐 Web query:

Spring Framework UriComponentsBuilder queryParam encode queryParameters build encode documentation

💡 Result:

In the Spring Framework, UriComponentsBuilder provides flexible ways to handle URI encoding, particularly when working with query parameters. Encoding Options UriComponentsBuilder exposes two primary levels of encoding, which determine how and when characters are processed: 1. UriComponentsBuilder#encode: This is generally recommended for most use cases [1][2]. It pre-encodes the URI template at build time and applies strict encoding to URI variables separately when they are expanded [3][1]. Because it treats URI variables as opaque data, it ensures they are fully encoded, including characters with reserved meaning [1][4]. 2. UriComponents#encode: This method is called after building the components (post-expansion) [1][2]. It encodes URI components after URI variables have been expanded [4]. This approach is preferred if you intentionally need to include reserved characters within your URI variables or if you are not expanding variables at all [1][5]. Handling Query Parameters When using queryParam(String name, Object... values), you can include URI template variables (e.g., "?q={value}") [3][6]. The encoding behavior depends on the chosen method: - If you call UriComponentsBuilder#encode, Spring will perform pre-encoding on the template and then strictly encode the expanded variable values [1][4]. - Note that standard encoding applied via the builder only targets characters illegal in a query parameter name or value (such as "=" or "&") [3][7]. Characters that are legal according to RFC 3986 are typically not encoded automatically, which includes the "+" character [3][6]. To achieve stricter encoding for specific values, you should use URI template variables [3][7]. Common Workflow The standard pattern for building a URI with encoded query parameters is: UriComponents uriComponents = UriComponentsBuilder.fromUriString("https://example.com/search").queryParam("q", "{queryValue}").encode // Pre-encodes template and sets strategy for strict variable encoding.buildAndExpand("my search value").toUri; DefaultUriBuilderFactory In many Spring applications (such as those using WebClient or RestTemplate), the DefaultUriBuilderFactory is used to manage encoding consistently [1][4]. It allows you to set an EncodingMode, such as TEMPLATE_AND_VALUES (which uses UriComponentsBuilder#encode) or URI_COMPONENT (which uses UriComponents#encode) [4][8]. Defaulting behavior may vary by Spring version and client implementation [8].

Citations:


🌐 Web query:

Spring Framework UriComponentsBuilder encode queryParam queryParameters spaces percent encoding Java documentation

💡 Result:

In the Spring Framework, UriComponentsBuilder handles query parameter encoding according to RFC 3986 [1]. When using queryParam, the default encoding behavior is selective: it only encodes characters that are considered illegal in a query parameter name or value (such as "=" or "&") [2][3]. Important details regarding spaces and character encoding include: 1. Selective Encoding: Many characters that are technically legal within the RFC 3986 syntax for query components are not automatically encoded [2][3]. Because the "+" character is considered legal in query parameters, it is not automatically converted to an encoded space (e.g., %20) [2][3]. This can lead to ambiguity if the receiving end interprets "+" as a space [4]. 2. Stricter Encoding: To achieve stricter encoding—where URI variables are treated as opaque data and fully encoded—you should use URI template variables in conjunction with the encode method [5][2][6]. Example: UriComponents uri = UriComponentsBuilder.fromUriString("https://example.com/search").queryParam("q", "{q}").encode.buildAndExpand("a space").toUri; This approach ensures that the variable "a space" is properly encoded (e.g., as "a%20space") [5][6]. 3. Manual vs. Framework Encoding: Spring expects you to be aware of whether the data you provide to queryParam is already encoded [4]. If you provide raw, unencoded strings that contain characters requiring specific encoding, using URI template variables combined with.encode is the recommended practice for robust, framework-managed encoding [5][4]. For further guidance, consult the URI Encoding section of the official Spring Framework reference documentation [5][4].

Citations:


🌐 Web query:

Java URI class equals query parameter spaces raw URI malformed URI exception

💡 Result:

When using the java.net.URI class, characters that are not legal URI characters, such as spaces, must be properly encoded to avoid a URISyntaxException [1][2][3]. Malformed URI Exceptions If you pass a string containing unencoded spaces (or other illegal characters) directly into the single-argument URI constructor (e.g., new URI("http://example.com/search?q=my query")), the constructor will throw a URISyntaxException because it attempts to parse the string as a strict RFC 2396/3986 URI [3][4]. To avoid this, you should either encode the input string before passing it to the constructor or use the multi-argument URI constructors, which perform the necessary encoding automatically [3][4][5]. Multi-Argument Constructors The recommended approach for building URIs in Java is to use the multi-argument constructors [4][5]. These constructors accept the individual components of the URI—such as scheme, authority, path, query, and fragment—as separate arguments [6][5]. The URI class then handles the correct percent-encoding for each component [7][5]. For example, to include a space in a query parameter, you should separate the components rather than constructing the entire URL string yourself: URI uri = new URI("http", "example.com", "/search", "q=my query", null); The resulting URI string will automatically represent the space as %20, producing: http://example.com/search?q=my%20query [7][5]. Equality and Comparison The equals method in the java.net.URI class compares URIs based on their raw, unencoded components [1][8]. For two hierarchical URIs to be considered equal, their paths must be equal and their queries must either both be undefined or else be equal [1][9]. Because the comparison happens on the raw forms (rather than the encoded forms), the URI class effectively compares the underlying data before any percent-encoding is applied [1][10]. Therefore, two URI objects are considered equal if their components are identical, regardless of the percent-encoding scheme used for those components [1][8]. Distinction from URLEncoder It is important to note that java.net.URLEncoder is a legacy class designed primarily for application/x-www-form-urlencoded data (such as HTML form submissions) [7][5]. A key difference is that URLEncoder converts spaces to plus signs (+), whereas the URI class, which follows RFC 3986, correctly converts spaces to %20 [7][5]. For REST APIs and modern web standards, you should prefer the java.net.URI class [5].

Citations:


OAuth 인가 요청에 CSRF 보호용 state를 포함시키세요.

getAuthorizationUrl은 카카오/구글 인가 URL을 만들지만 CSRF 보호를 위한 state를 전혀 포함하지 않습니다. OAuth 2.0 인가 코드 흐름에서는 state가 요청과 콜백을 바인딩해 피크업 계정으로 로그인하는 공격을 막는 표준 기제입니다(RFC 6749 §10.12, OAuth Security Best Current Practice).

개선 방향: 인가 요청 전에 랜덤하고 예측 불가능한 state를 생성해 짧은 TTL로 저장한 뒤, 카카오/구글 반환 URL의 state를 같은 값과 비교하는 검증 로직을 oAuthCallback 에 추가하세요. PKCE나 OpenID Connect nonce를 사용할 수도 있지만, 이 흐름에서는 state가 최소 요건입니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/mr/domain/auth/service/OAuthClientService.java` around
lines 235 - 262, Update getAuthorizationUrl to generate a cryptographically
random, unpredictable state before building the Kakao or Google authorization
URL, store it with a short TTL keyed to the login attempt, and include it as the
state query parameter. Extend oAuthCallback to retrieve and compare the stored
state with the callback value, rejecting missing or mismatched values before
processing the authorization code.

Comment on lines +264 to +277
public String buildFrontendRedirectUrl(com.mr.domain.auth.dto.res.AuthResponseDTO.LoginResponse loginResponse) {
String baseUrl = (oAuthProperties != null && oAuthProperties.frontendRedirectUri() != null && !oAuthProperties.frontendRedirectUri().isBlank())
? oAuthProperties.frontendRedirectUri()
: "http://localhost:3000/oauth/callback";

return org.springframework.web.util.UriComponentsBuilder.fromUriString(baseUrl)
.queryParam("accessToken", loginResponse.tokenInfo().accessToken())
.queryParam("refreshToken", loginResponse.tokenInfo().refreshToken())
.queryParam("userId", loginResponse.userId())
.queryParam("isNewUser", loginResponse.isNewUser())
.queryParam("isOnboardingCompleted", loginResponse.isOnboardingCompleted())
.build()
.toUriString();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🔴 Critical | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate OAuthClientService.java =="
fd -a 'OAuthClientService\.java$' .

echo "== relevant lines =="
file="$(fd 'OAuthClientService\.java$' . | head -n 1)"
if [ -n "${file:-}" ]; then
  wc -l "$file"
  sed -n '230,285p' "$file" | cat -n
  sed -n '1,90p' "$file" | cat -n
fi

echo "== search frontendRedirectUri usages =="
rg -n "frontendRedirectUri|buildFrontendRedirectUrl|refreshToken|accessToken|OAuthServer|OAUTH_SERVER_ERROR|resolveRedirectUri" .

Repository: Musereview/BE

Length of output: 2008


🌐 Web query:

RFC 6749 section 10.16 access token in query string fragment URL exposure OAuth tokens

💡 Result:

RFC 6749 Section 10.16 addresses the "Misuse of Access Token to Impersonate Resource Owner in Implicit Flow," noting that when access tokens are exposed in the URI (such as via the fragment component in the Implicit Grant flow), they are susceptible to leakage through various channels [1][2]. The security concerns regarding access tokens in URLs—whether in query parameters or fragments—are well-documented and represent a significant security risk [3][4]. Placing sensitive tokens in URIs exposes them to unintended discovery through [5][6]: - Browser History: Tokens contained in a URL remain in the browser's history, where they can be accessed by other users of the same device or via local malicious software [3][4]. - Server and Proxy Logs: URIs (including query parameters) are frequently logged by web servers, load balancers, and reverse proxies, making the tokens visible to administrators and any entity with access to these logs [7][5]. - Referer Headers: When a page containing a token in its URL navigates to another site, the full URL—including the token—may be sent to the destination server via the HTTP Referer header [5][6]. Because of these risks, modern OAuth 2.0 security standards, including RFC 9700, strictly advise against passing access tokens in URI query parameters [3][4]. Furthermore, the Implicit Grant flow (which relied on URL fragments to return tokens) is now deprecated and removed from OAuth 2.1 in favor of the Authorization Code Grant with PKCE, which avoids exposing access tokens in the front-channel URL [8][9][6]. Developers are strongly encouraged to transmit access tokens using secure mechanisms, such as the Authorization request header (e.g., Authorization: Bearer ) [5][6].

Citations:


🔴 토큰 원문을 URL 쿼리파라미터로 넘기지 마세요.

buildFrontendRedirectUrlaccessToken/refreshToken을 프론트엔드 리다이렉트 URL에 노출하므로 브라우저 히스토리·서버/프록시 로그·Referer 헤더 경로를 통해 유출될 수 있습니다. refreshToken은 특히 장수명 시신분 계정의 직접 탈취로 이어질 수 있어 보안상 허용되지 않습니다(OAuth 2.0은 URI에서 토큰을 넘기는 방식 사용을 권장하지 않습니다; RFC 6749 §10.16, OWASP OAuth 2.0 Cheat Sheet 참고).

토큰 원문 대신 1회용 교환 코드를 쿼리로 전달하고, 프론트엔드가 별도 HTTPS + 검증된 POST API로 토큰을 교환하도록 하는 방향, 또는 refreshTokenHttpOnly Secure 쿠키로 전달하는 방식을 사용하는 편이 안전합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/mr/domain/auth/service/OAuthClientService.java` around
lines 264 - 277, Update buildFrontendRedirectUrl so it never places accessToken
or refreshToken in the redirect query parameters. Replace the raw-token
parameters with a single-use exchange code that the frontend can redeem through
the established secure HTTPS POST flow, or deliver refreshToken through an
HttpOnly Secure cookie while retaining only non-sensitive response data in the
URL.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

✨ Feature - 인가 코드 토큰 변환 api 구현

3 participants