Skip to content

[hotfix] 소셜 로그인 관련 코드 수정#217

Merged
seongjunnoh merged 5 commits intodevelopfrom
hotfix/#216-oauth-login
Aug 13, 2025
Merged

[hotfix] 소셜 로그인 관련 코드 수정#217
seongjunnoh merged 5 commits intodevelopfrom
hotfix/#216-oauth-login

Conversation

@seongjunnoh
Copy link
Collaborator

@seongjunnoh seongjunnoh commented Aug 13, 2025

#️⃣ 연관된 이슈

closes #216

📝 작업 내용

  • 소셜 로그인 이후 쿠키를 제외한 채로 redirect 하도록 수정
  • 토큰을 포함하는 쿠키를 세팅할 수 있는 api 추가
  • 임시토큰, access token 을 서버 내부에서 구분할 수 있도록 구현

📸 스크린샷

💬 리뷰 요구사항

리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요

📌 PR 진행 시 이러한 점들을 참고해 주세요

* P1 : 꼭 반영해 주세요 (Request Changes) - 이슈가 발생하거나 취약점이 발견되는 케이스 등
* P2 : 반영을 적극적으로 고려해 주시면 좋을 것 같아요 (Comment)
* P3 : 이런 방법도 있을 것 같아요~ 등의 사소한 의견입니다 (Chore)

Summary by CodeRabbit

  • 신기능

    • 소셜 로그인 성공 후 리다이렉트 URL에 loginTokenKey가 포함되며, /api/set-cookie 호출 시 브라우저에 인증 쿠키가 설정됩니다.
    • 두 종류의 인증 쿠키 지원: ACCESS(30일), TEMP(10분). 모두 Secure/HttpOnly/SameSite=None 적용.
  • 변경사항

    • 인증 필터가 Authorization 헤더뿐 아니라 쿠키에서도 토큰을 인식합니다.
    • /api/set-cookie 경로는 인증 필터 대상에서 제외되었습니다.

@seongjunnoh seongjunnoh linked an issue Aug 13, 2025 that may be closed by this pull request
2 tasks
@coderabbitai
Copy link

coderabbitai bot commented Aug 13, 2025

Walkthrough

소셜 로그인 이후 토큰 전달 방식을 쿠키 직접 설정에서 서버측 임시 저장소(LoginTokenStorage)와 /api/set-cookie 교환 방식으로 변경했다. JWT 필터는 새 쿠키 이름(access_token, temp_token)을 인식하고 /api/set-cookie 경로는 필터 제외 처리됐다. 관련 상수와 TokenType enum이 추가됐다.

Changes

Cohort / File(s) Summary
Auth parameter constants
src/main/java/konkuk/thip/common/security/constant/AuthParameters.java
Enum에 COOKIE_ACCESS_TOKEN("access_token"), COOKIE_TEMP_TOKEN("temp_token") 추가
JWT filter update
src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationFilter.java
토큰 추출 시 새 쿠키 이름 2종 허용, /api/set-cookie 필터 제외 추가
OAuth2 cookie exchange endpoint
src/main/java/konkuk/thip/common/security/oauth2/AuthCookieController.java
새 컨트롤러 추가. POST /api/set-cookie에서 loginTokenKey 소비 후 ACCESS/TEMP에 따라 쿠키 설정
OAuth2 success handler flow change
src/main/java/konkuk/thip/common/security/oauth2/CustomSuccessHandler.java
토큰을 쿠키로 직접 설정 대신 LoginTokenStorage에 5분 TTL로 저장하고 리다이렉트 URL에 loginTokenKey 전달
Login token storage
src/main/java/konkuk/thip/common/security/oauth2/LoginTokenStorage.java
키-엔트리(타입/토큰/만료시각) 기반 인메모리 저장소 추가, put/consume 제공
Token type enum
src/main/java/konkuk/thip/common/security/oauth2/TokenType.java
TEMP, ACCESS 상수와 value 필드 보유 enum 추가

Sequence Diagram(s)

sequenceDiagram
    participant User as 사용자 브라우저
    participant OAuth as OAuth2 Provider
    participant Server as 서버
    participant Store as LoginTokenStorage

    User->>OAuth: 소셜 로그인
    OAuth-->>Server: OAuth2 콜백
    Server->>Server: 사용자 신규/기존 판별, 토큰 생성
    Server->>Store: put(loginTokenKey, TokenType, token, 5분)
    Server-->>User: 클라이언트로 리다이렉트 (?loginTokenKey=...)
    User->>Server: POST /api/set-cookie {loginTokenKey}
    Server->>Store: consume(loginTokenKey)
    Store-->>Server: {type, token} 또는 null
    Server-->>User: Set-Cookie(access_token|temp_token), 응답 본문 {type}
    User->>Server: 이후 API 요청(Authorization 헤더 또는 쿠키)
    Server->>Server: JwtAuthenticationFilter가 헤더/쿠키에서 토큰 추출
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Assessment against linked issues

Objective Addressed Explanation
소셜 로그인 수정 전반(#216) 요구사항 상세가 없어 구체적 충족 여부 판단 불가
체크리스트 Task1/Task2(#216) 작업 내용이 명시되지 않아 반영 여부 확인 불가

Possibly related PRs

Poem

깡총, 리다이렉트 열쇠를 쥐고
쿠키는 나중에, 창고에 먼저 보관하고
다섯 분 마법이 똑딱—빛나네
access냐 temp냐, 이름표 달아 건네네
토큰 따라 봄길처럼 로그인이 열리네 🥕✨

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch hotfix/#216-oauth-login

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

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

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@github-actions
Copy link

Test Results

383 tests   383 ✅  31s ⏱️
113 suites    0 💤
113 files      0 ❌

Results for commit 857429a.

@seongjunnoh seongjunnoh merged commit 711b21c into develop Aug 13, 2025
3 of 4 checks passed
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🔭 Outside diff range comments (1)
src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationFilter.java (1)

52-63: TEMP 토큰에 대해 SecurityContext 설정을 피하세요.

임시 토큰까지 인증 컨텍스트에 올리면 권한 검사 방식에 따라 의도치 않은 접근 허용 가능성이 있습니다. userId가 존재(ACCESS 토큰)할 때만 Authentication을 세팅하세요.

-            CustomOAuth2User customOAuth2User = new CustomOAuth2User(loginUser);
-
-            Authentication authToken = new UsernamePasswordAuthenticationToken(customOAuth2User, null, customOAuth2User.getAuthorities());
-            SecurityContextHolder.getContext().setAuthentication(authToken);
+            if (loginUser.userId() != null) {
+                CustomOAuth2User customOAuth2User = new CustomOAuth2User(loginUser);
+                Authentication authToken =
+                        new UsernamePasswordAuthenticationToken(customOAuth2User, null, customOAuth2User.getAuthorities());
+                SecurityContextHolder.getContext().setAuthentication(authToken);
+            } else {
+                log.debug("TEMP token detected. Skipping SecurityContext authentication.");
+            }

또한 TEMP 토큰 허용 경로를 최소화하려면 화이트리스트에만 TEMP 사용을 기대하는 엔드포인트를 남기고, 일반 보호 API에서는 access_token만 사용하도록 위 쿠키 우선순위 변경과 함께 운용하는 것을 권장합니다.

🧹 Nitpick comments (9)
src/main/java/konkuk/thip/common/security/oauth2/TokenType.java (1)

6-13: 중복 상태 제거: value 필드 없이 enum 자체 이름 활용 권장.

현 구현은 enum 상수 이름과 동일한 문자열을 다시 보관합니다. 불필요한 상태/보일러플레이트를 제거해 단순화할 수 있습니다.

다음과 같이 정리하면 용도 유지하면서 코드/메모리 낭비를 줄일 수 있습니다.

-@Getter
-@RequiredArgsConstructor
-public enum TokenType {
-
-    TEMP("TEMP"), ACCESS("ACCESS");
-
-    private final String value;
-}
+public enum TokenType {
+    TEMP, ACCESS;
+
+    public String getValue() {
+        return name();
+    }
+}
src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationFilter.java (1)

74-76: 미세 최적화: split 대신 substring 사용 권장.

split은 배열 생성이 불필요합니다. 접두사 길이를 활용해 토큰을 잘라내면 가비지와 비용을 줄일 수 있습니다.

-            return authorization.split(" ")[1];
+            return authorization.substring(JWT_PREFIX.getValue().length()).trim();
src/main/java/konkuk/thip/common/security/oauth2/AuthCookieController.java (2)

28-35: DTO 사용으로 입력 검증/가독성 향상.

Map<String, String> 대신 전용 요청 DTO로 변경하면 필드 검증과 스키마 관리가 수월합니다.

// 신규 DTO
public record SetCookieRequest(String loginTokenKey) {}
-    public ResponseEntity<?> setCookie(@RequestBody Map<String, String> body, HttpServletResponse response) {
-        String loginTokenKey = body.get("loginTokenKey");
+    public ResponseEntity<?> setCookie(@RequestBody SetCookieRequest req, HttpServletResponse response) {
+        String loginTokenKey = req.loginTokenKey();

67-124: 대규모 주석 블록 제거 또는 이슈로 이관.

주석 처리된 교환 엔드포인트는 유지보수성 저하를 유발합니다. 필요 없다면 제거하고, 필요하다면 별 브랜치/이슈로 관리해 주세요.

원하시면 실제 구현 초안을 생성해 드릴 수 있습니다. 이슈를 열어드릴까요?

src/main/java/konkuk/thip/common/security/oauth2/CustomSuccessHandler.java (2)

26-27: 미사용 상수 제거.

COOKIE_MAX_AGE 상수는 현재 사용되지 않습니다. 혼란 방지를 위해 제거하세요.

-    private static final int COOKIE_MAX_AGE = 60 * 60 * 24; // 1일

65-77: 이전 쿠키 부여 로직 삭제 권장.

새 흐름에서는 서버 저장소 + /api/set-cookie를 사용합니다. addTokenCookie는 미사용이므로 제거해 코드를 간결히 유지하세요.

-    private void addTokenCookie(HttpServletResponse response, String token) {
-        Cookie cookie = new Cookie(JWT_HEADER_KEY.getValue(), token);
-        if(webRedirectUrl.startsWith(HTTPS_PREFIX.getValue())) {
-            cookie.setDomain(webRedirectUrl.replace(HTTPS_PREFIX.getValue(), ""));
-        } else {
-            cookie.setDomain("localhost");
-        }
-        cookie.setSecure(webRedirectUrl.startsWith(HTTPS_PREFIX.getValue()));
-        cookie.setHttpOnly(false);
-        cookie.setPath("/");
-        cookie.setMaxAge(COOKIE_MAX_AGE);
-        response.addCookie(cookie);
-    }

이와 함께 상단의 Cookie import도 제거 가능합니다.

src/main/java/konkuk/thip/common/security/oauth2/LoginTokenStorage.java (3)

45-46: 경계값/성능 미세개선: 만료 비교는 <=, Instant.now() 대신 System.currentTimeMillis()

동일 ms에 만료된 토큰을 허용하지 않도록 <= 비교를 권장합니다. 또한 로그인 한 건당 한 번 호출되는 코드이긴 하나, 객체 생성 없는 System.currentTimeMillis()가 더 저오버헤드입니다.

-        if (entry.expireAtEpochMillis < Instant.now().toEpochMilli()) return null;
+        long now = System.currentTimeMillis();
+        if (entry.expireAtEpochMillis <= now) return null;

15-26: DTO 성격의 Entry는 record로 간결화 가능 + Lombok 제거

불변 값 홀더는 record가 적합합니다. Lombok @Getter와 수동 생성자를 제거해 보일러플레이트를 없앨 수 있습니다.

-import lombok.Getter;
+// Lombok 불필요
-    @Getter
-    public static final class Entry {
-        private final TokenType type;
-        private final String token;
-        private final long expireAtEpochMillis;
-
-        private Entry(TokenType type, String token, long expireAtEpochMillis) {
-            this.type = type;
-            this.token = token;
-            this.expireAtEpochMillis = expireAtEpochMillis;
-        }
-    }
+    public static record Entry(TokenType type, String token, long expireAtEpochMillis) {}

참고: record로 전환 시 consume의 접근자도 메서드 형태로 바뀝니다.
예) entry.expireAtEpochMillis() (본 파일의 관련 참조도 함께 치환 필요)

Also applies to: 3-3


4-4: @requiredargsconstructor는 불필요합니다

주입받는 필드가 없고, final 필드(store)는 필드 초기화로 구성되어 생성자 생성이 무의미합니다. 혼란을 줄이기 위해 제거 권장합니다.

-import lombok.RequiredArgsConstructor;
@@
-@RequiredArgsConstructor

Also applies to: 12-12

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cfacbaa and 857429a.

📒 Files selected for processing (6)
  • src/main/java/konkuk/thip/common/security/constant/AuthParameters.java (1 hunks)
  • src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationFilter.java (2 hunks)
  • src/main/java/konkuk/thip/common/security/oauth2/AuthCookieController.java (1 hunks)
  • src/main/java/konkuk/thip/common/security/oauth2/CustomSuccessHandler.java (3 hunks)
  • src/main/java/konkuk/thip/common/security/oauth2/LoginTokenStorage.java (1 hunks)
  • src/main/java/konkuk/thip/common/security/oauth2/TokenType.java (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#113
File: src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/RecentSearchCommandPersistenceAdapter.java:38-44
Timestamp: 2025-07-30T14:05:04.945Z
Learning: seongjunnoh는 코드 최적화 제안에 대해 구체적인 기술적 근거와 효율성 차이를 이해하고 싶어하며, 성능 개선 방식에 대한 상세한 설명을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#93
File: src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java:49-114
Timestamp: 2025-07-28T16:44:31.224Z
Learning: seongjunnoh는 코드 중복 문제에 대한 리팩토링 제안을 적극적으로 수용하고 함수형 인터페이스를 활용한 해결책을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#195
File: src/main/java/konkuk/thip/feed/application/mapper/FeedQueryMapper.java:0-0
Timestamp: 2025-08-13T05:22:32.287Z
Learning: seongjunnoh는 데이터 무결성과 중복 방지에 대한 고민이 깊으며, LinkedHashSet을 활용한 중복 제거와 순서 보장을 동시에 달성하는 솔루션을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#112
File: src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java:272-272
Timestamp: 2025-07-30T10:44:34.115Z
Learning: seongjunnoh는 피드 커서 페이지네이션에서 LocalDateTime 단일 커서 방식을 선호하며, 복합 키 기반 커서보다 구현 단순성과 성능을 우선시한다.
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (4)
src/main/java/konkuk/thip/common/security/constant/AuthParameters.java (1)

20-21: 쿠키 이름 상수 추가는 적절합니다 (LGTM).

새 쿠키 네이밍을 상수화해 중복/실수를 줄였습니다. 이후 레이어에서 동일 값 하드코딩 금지 유지 부탁드립니다.

src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationFilter.java (1)

102-104: 화이트리스트 경로 검토 결과

현재 /api/exchange-temp-token 엔드포인트는
AuthCookieController에서 주석 처리되어 있어 활성화되지 않은 상태입니다.
따라서 JwtAuthenticationFiltershouldNotFilter 목록에 포함되어 있지 않아도 무방합니다.

다만, 해당 엔드포인트를 활성화할 경우 반드시 예외 경로로 추가해야 합니다:

  • 파일: src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationFilter.java
  • 메서드: shouldNotFilter(HttpServletRequest request) (약 102행)

활성화 시 복원할 코드:

- //                || path.equals("/api/exchange-temp-token")
+                || path.equals("/api/exchange-temp-token")
src/main/java/konkuk/thip/common/security/oauth2/AuthCookieController.java (1)

52-64: 쿠키 수명과 JWT 만료의 정합성 확인 요청.

access_token 쿠키는 30일, TEMP 쿠키는 10분입니다. JWT 자체 만료와 불일치하면 만료된 토큰이 쿠키에 남아 UX 저하(연속 401) 가능성이 있습니다. JWT 만료 ≤ 쿠키 만료로 정렬하거나, 만료 동기화 전략을 정의해 주세요.

src/main/java/konkuk/thip/common/security/oauth2/LoginTokenStorage.java (1)

41-43: 원타임 소비(atomic remove) 동시성 안전성은 적절합니다

ConcurrentHashMap에서 remove로 먼저 제거한 뒤 반환하는 방식은 중복 소비를 방지합니다. 이 부분의 동작은 의도에 부합합니다.

Comment on lines +81 to 83
// access_token 또는 temp_token 둘 다 허용
if (COOKIE_ACCESS_TOKEN.getValue().equals(cookie.getName()) || COOKIE_TEMP_TOKEN.getValue().equals(cookie.getName())) {
return cookie.getValue();
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

쿠키 우선순위 명시: access_token을 우선, 없을 때만 temp_token 허용.

현재 구현은 쿠키 배열 순서에 따라 temp_token이 먼저 선택될 수 있습니다. 일반 API 접근 시 임시 토큰 선택을 피하려면 access_token 우선 검색이 안전합니다.

-        if (request.getCookies() != null) {
-            for (Cookie cookie : request.getCookies()) {
-                // access_token 또는 temp_token 둘 다 허용
-                if (COOKIE_ACCESS_TOKEN.getValue().equals(cookie.getName()) || COOKIE_TEMP_TOKEN.getValue().equals(cookie.getName())) {
-                    return cookie.getValue();
-                }
-            }
-        }
+        if (request.getCookies() != null) {
+            String access = null;
+            String temp = null;
+            for (Cookie cookie : request.getCookies()) {
+                if (COOKIE_ACCESS_TOKEN.getValue().equals(cookie.getName())) {
+                    access = cookie.getValue();
+                } else if (COOKIE_TEMP_TOKEN.getValue().equals(cookie.getName())) {
+                    temp = cookie.getValue();
+                }
+            }
+            if (access != null) return access;
+            if (temp != null) return temp;
+        }
📝 Committable suggestion

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

Suggested change
// access_token 또는 temp_token 둘 다 허용
if (COOKIE_ACCESS_TOKEN.getValue().equals(cookie.getName()) || COOKIE_TEMP_TOKEN.getValue().equals(cookie.getName())) {
return cookie.getValue();
if (request.getCookies() != null) {
String access = null;
String temp = null;
for (Cookie cookie : request.getCookies()) {
if (COOKIE_ACCESS_TOKEN.getValue().equals(cookie.getName())) {
access = cookie.getValue();
} else if (COOKIE_TEMP_TOKEN.getValue().equals(cookie.getName())) {
temp = cookie.getValue();
}
}
if (access != null) return access;
if (temp != null) return temp;
}
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationFilter.java
around lines 81 to 83, the current cookie selection allows whichever matching
cookie appears first (so temp_token can be chosen before access_token); change
the logic to explicitly prefer COOKIE_ACCESS_TOKEN: first search cookies for a
cookie whose name equals COOKIE_ACCESS_TOKEN and return its value if found,
otherwise search for COOKIE_TEMP_TOKEN and return its value if found; if neither
exists, return null. Ensure you do not rely on array order and handle null/empty
cookie arrays safely.

Comment on lines +42 to +64
if (entry.getType() == TokenType.ACCESS) {
ResponseCookie cookie = ResponseCookie.from(COOKIE_ACCESS_TOKEN.getValue(), entry.getToken())
.httpOnly(true)
.secure(true)
.sameSite("None")
.path("/")
.maxAge(Duration.ofDays(30)) // 30일짜리 쿠키
.build();
response.addHeader("Set-Cookie", cookie.toString());

return ResponseEntity.ok(Map.of("type", TokenType.ACCESS.getValue()));
} else {
ResponseCookie cookie = ResponseCookie.from(COOKIE_TEMP_TOKEN.getValue(), entry.getToken())
.httpOnly(true)
.secure(true)
.sameSite("None")
.path("/") // 일단 모든
.maxAge(Duration.ofMinutes(10)) // 10분짜리 쿠키
.build();
response.addHeader("Set-Cookie", cookie.toString());

return ResponseEntity.ok(Map.of("type", TokenType.TEMP.getValue()));
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

쿠키 Domain 설정을 프로퍼티로 분리하여 환경별 호환성 확보.

현재 Domain 미설정으로 응답 호스트에 종속됩니다. 프론트-API가 서로 다른 서브도메인에 있는 경우, 공용 도메인 설정이 필요할 수 있습니다.

권장 접근:

  • application 설정에 security.cookie-domain(예: .thip.com) 도입
  • 로컬/테스트는 빈 값 허용, 빈 값일 때는 Domain 미지정

예시(필요한 위치에 추가):

@Value("${security.cookie-domain:}")
private String cookieDomain;

체이닝 대신 빌더를 변수로 받아 조건부 domain 설정:

ResponseCookie.ResponseCookieBuilder builder =
    ResponseCookie.from(COOKIE_ACCESS_TOKEN.getValue(), entry.getToken())
        .httpOnly(true)
        .secure(true)
        .sameSite("None")
        .path("/")
        .maxAge(Duration.ofDays(30));
if (cookieDomain != null && !cookieDomain.isBlank()) {
    builder.domain(cookieDomain);
}
response.addHeader("Set-Cookie", builder.build().toString());

TEMP 쿠키도 동일하게 적용하세요.

🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/common/security/oauth2/AuthCookieController.java
around lines 42 to 64, the cookie Domain is not set which ties cookies to the
response host; add a configurable property and conditionally set the domain on
the ResponseCookie builders. Inject a property like security.cookie-domain (e.g.
@Value("${security.cookie-domain:}") private String cookieDomain) and replace
the direct ResponseCookie.from(...).build() calls with a builder variable for
both ACCESS and TEMP branches; if cookieDomain is non-empty (not null/blank)
call builder.domain(cookieDomain) before builder.build(), otherwise leave domain
unset for local/testing. Ensure both cookies use the same conditional domain
logic and keep existing flags (httpOnly, secure, sameSite, path, maxAge).

public class CustomSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

private static final int COOKIE_MAX_AGE = 60 * 60 * 24; // 1일
private final LoginTokenStorage loginTokenStorage;
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

확장/배포 환경 고려: LoginTokenStorage의 인스턴스 간 일관성.

인메모리 저장소는 멀티 인스턴스/무상태 배포에서 다른 노드로 라우팅 시 토큰 검색 실패를 유발합니다. Sticky session을 강제하거나, Redis 같은 공유 스토리지로 전환하세요.

레포 내 구현체를 확인해 단일 노드 전제인지 점검해 주세요:


🏁 Script executed:

#!/bin/bash
# LoginTokenStorage 타입과 빈 스코프/설정 확인
rg -n "class LoginTokenStorage" -A 50
rg -n "@Bean|@Configuration|@Enable" -A 10 | rg -n "LoginTokenStorage"

Length of output: 3866


🏁 Script executed:

#!/bin/bash
# 1. LoginTokenStorage에 @Component 등 스프링 빈 어노테이션이 있는지 확인
rg -n "@Component" -A 1 src/main/java/konkuk/thip/common/security/oauth2/LoginTokenStorage.java

# 2. CustomSuccessHandler에서 LoginTokenStorage가 어떻게 주입되는지 확인
rg -n "class CustomSuccessHandler" -A 50 src/main/java/konkuk/thip/common/security/oauth2/CustomSuccessHandler.java

Length of output: 2681


인메모리 LoginTokenStorage는 단일 노드 전제

현재 src/main/java/konkuk/thip/common/security/oauth2/LoginTokenStorage.java
@component 싱글톤 빈으로 ConcurrentHashMap 기반 인메모리 스토어를 사용합니다.
멀티 인스턴스/무상태 배포 환경에서 다른 노드로 요청이 라우팅되면 저장된 토큰 조회가 실패하게 됩니다.

해결 방안:

  • 세션 어피니티(Sticky Session)를 적용하여 항상 동일 노드로 라우팅되도록 보장
  • Redis, Hazelcast 등 외부 공유 스토리지를 사용하도록 LoginTokenStorage 구현을 변경

Comment on lines +49 to +61
String loginTokenKey = UUID.randomUUID().toString();
loginTokenStorage.put(loginTokenKey, TokenType.TEMP, tempToken, Duration.ofMinutes(5)); // ttl 5분

getRedirectStrategy().sendRedirect(request, response, webRedirectUrl + REDIRECT_SIGNUP_URL.getValue() + "?loginTokenKey=" + loginTokenKey);
} else {
// 기존 유저 - 로그인용 액세스 토큰
String accessToken = jwtUtil.createAccessToken(loginUser.userId());
addTokenCookie(response, accessToken);
getRedirectStrategy().sendRedirect(request, response, webRedirectUrl + REDIRECT_HOME_URL.getValue());
// addTokenCookie(response, accessToken);

String loginTokenKey = UUID.randomUUID().toString();
loginTokenStorage.put(loginTokenKey, TokenType.ACCESS, accessToken, Duration.ofMinutes(5)); // ttl 5분

getRedirectStrategy().sendRedirect(request, response, webRedirectUrl + REDIRECT_HOME_URL.getValue() + "?loginTokenKey=" + loginTokenKey);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

loginTokenKey TTL(5분) 재검토: 사용자 흐름과 TEMP 쿠키(10분)와의 정합성.

웹 리디렉션 후 셋-쿠키 호출까지 5분을 넘길 경우 400이 발생합니다. TEMP 쿠키 TTL(10분)과 정렬하거나 환경 변수로 관리하는 편이 안전합니다.

-            loginTokenStorage.put(loginTokenKey, TokenType.TEMP, tempToken, Duration.ofMinutes(5));      // ttl 5분
+            loginTokenStorage.put(loginTokenKey, TokenType.TEMP, tempToken, Duration.ofMinutes(10));     // ttl 10분
...
-            loginTokenStorage.put(loginTokenKey, TokenType.ACCESS, accessToken, Duration.ofMinutes(5));      // ttl 5분
+            loginTokenStorage.put(loginTokenKey, TokenType.ACCESS, accessToken, Duration.ofMinutes(10));     // ttl 10분

추가로 application 설정(ex. security.login-token-ttl-minutes)으로 외부화하는 것을 추천합니다.

📝 Committable suggestion

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

Suggested change
String loginTokenKey = UUID.randomUUID().toString();
loginTokenStorage.put(loginTokenKey, TokenType.TEMP, tempToken, Duration.ofMinutes(5)); // ttl 5분
getRedirectStrategy().sendRedirect(request, response, webRedirectUrl + REDIRECT_SIGNUP_URL.getValue() + "?loginTokenKey=" + loginTokenKey);
} else {
// 기존 유저 - 로그인용 액세스 토큰
String accessToken = jwtUtil.createAccessToken(loginUser.userId());
addTokenCookie(response, accessToken);
getRedirectStrategy().sendRedirect(request, response, webRedirectUrl + REDIRECT_HOME_URL.getValue());
// addTokenCookie(response, accessToken);
String loginTokenKey = UUID.randomUUID().toString();
loginTokenStorage.put(loginTokenKey, TokenType.ACCESS, accessToken, Duration.ofMinutes(5)); // ttl 5분
getRedirectStrategy().sendRedirect(request, response, webRedirectUrl + REDIRECT_HOME_URL.getValue() + "?loginTokenKey=" + loginTokenKey);
String loginTokenKey = UUID.randomUUID().toString();
loginTokenStorage.put(loginTokenKey, TokenType.TEMP, tempToken, Duration.ofMinutes(10)); // ttl 10분
getRedirectStrategy().sendRedirect(request, response, webRedirectUrl + REDIRECT_SIGNUP_URL.getValue() + "?loginTokenKey=" + loginTokenKey);
} else {
// 기존 유저 - 로그인용 액세스 토큰
String accessToken = jwtUtil.createAccessToken(loginUser.userId());
// addTokenCookie(response, accessToken);
String loginTokenKey = UUID.randomUUID().toString();
loginTokenStorage.put(loginTokenKey, TokenType.ACCESS, accessToken, Duration.ofMinutes(10)); // ttl 10분
getRedirectStrategy().sendRedirect(request, response, webRedirectUrl + REDIRECT_HOME_URL.getValue() + "?loginTokenKey=" + loginTokenKey);
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/common/security/oauth2/CustomSuccessHandler.java
around lines 49–61, the loginTokenKey TTL is hardcoded to 5 minutes which can
expire before the TEMP cookie (10 minutes) and cause 400 errors; change both
places that call Duration.ofMinutes(5) to use a shared configurable value (e.g.
read from application properties security.login-token-ttl-minutes or an env var)
and ensure the TEMP cookie TTL and ACCESS/TEMP loginToken storage TTL are
aligned (use the same config value, default to 10 minutes if unspecified).
Update the class to inject the config (or a properties holder) and replace the
hardcoded Duration.ofMinutes(5) with Duration.ofMinutes(configValue) for both
branches so the redirect -> set-cookie flow uses consistent TTLs.

@@ -0,0 +1,48 @@
package konkuk.thip.common.security.oauth2;
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

보안/운영 검증 요청: key 생성 강도, 쿠키 속성, 멀티 인스턴스 대응

  • loginTokenKey 생성이 충분히 예측 불가한지(SecureRandom/UUID v4 이상) 확인 필요.
  • /api/set-cookie에서 발급하는 쿠키에 Secure, HttpOnly, SameSite=None이 설정되어 있는지 확인 필요.
  • 본 구현은 인메모리이므로 멀티 인스턴스 환경에서 set-cookie 요청이 다른 인스턴스로 라우팅되면 조회 실패합니다. Sticky session, Redis 등 중앙 저장소, 또는 토큰을 직접 서명해 전달하는 대안 고려가 필요합니다.

다음 스크립트로 PR 브랜치에서 관련 구현을 빠르게 점검할 수 있습니다.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== LoginTokenStorage 사용처 확인 =="
rg -n "LoginTokenStorage|loginTokenKey|TokenType|set-cookie" -A 5 -B 5

echo
echo "== key 생성 로직 후보 확인(SecureRandom/UUID v4 등) =="
rg -n "SecureRandom|UUID\.randomUUID|KeyGenerator|Base64" -A 2 -B 2

echo
echo "== AuthCookieController에서 쿠키 속성 확인(ResponseCookie/Cookie 사용 여부) =="
fd -a "AuthCookieController.java" | while read -r f; do
  echo "---- $f ----"
  rg -n "ResponseCookie|new Cookie|HttpOnly|SameSite|Secure" -A 2 -B 2 "$f" || true
done

echo
echo "== Security 설정에서 /api/set-cookie 필터/권한 예외 확인 =="
rg -n "/api/set-cookie|set-cookie" src/main/java -A 5 -B 5

echo
echo "== TTL 설정(예: 5분) 실제 전달 여부 확인 =="
fd -a "CustomSuccessHandler.java" | while read -r f; do
  echo "---- $f ----"
  rg -n "LoginTokenStorage|Duration|ofMinutes|put\(" -A 3 -B 3 "$f" || true
done

Length of output: 24513


보안/운영 검증 완료 (key 생성, 쿠키 속성, 보안 필터, TTL 확인) – 다중 인스턴스 저장소 검토 필요

  • Key 생성
    • UUID.randomUUID()를 사용해 버전4 UUID(≈122비트 랜덤)를 생성하므로 예측 불가능성 충분함
  • 쿠키 속성
    • ResponseCookie.from(...).httpOnly(true), .secure(true), .sameSite("None")가 모두 적용되어 있음
  • 보안 필터
    • JwtAuthenticationFilter에서 /api/set-cookie 경로를 path.equals("/api/set-cookie")로 skip 처리해 인가 예외가 확인됨
  • TTL 설정
    • CustomSuccessHandler에서 loginTokenStorage.put(..., Duration.ofMinutes(5))로 5분 TTL이 올바르게 적용됨

⚠️ 다중 인스턴스 환경 주의
현재 LoginTokenStorageConcurrentHashMap 기반의 인메모리 저장소이므로, 인스턴스 간 라우팅이 바뀔 경우 키 조회에 실패할 수 있습니다.

  • 스티키 세션 유지
  • Redis / 분산 캐시 등 중앙 저장소 도입
  • 또는 로그인 토큰에 서명을 붙여 자체 검증하는 방식

위 중 하나 이상의 대안을 검토·적용해 주세요.

🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/common/security/oauth2/LoginTokenStorage.java
around line 1, the current implementation uses an in-memory ConcurrentHashMap
which will fail in multi-instance deployments; replace or augment this
implementation with a distributed/session-consistent approach: switch storage to
a shared cache (e.g., Redis) and use its TTL feature when storing tokens, or
implement sticky sessions at the load-balancer level, or alternatively stop
storing state and embed a signed login token (JWT or HMAC-signed value) that can
be validated without central storage; ensure the chosen approach preserves the
current TTL semantics, concurrency safety, and proper
serialization/deserialization of keys/values and update callers to use the new
storage API.

}
}

private final ConcurrentHashMap<String, Entry> store = new ConcurrentHashMap<>();
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

만료 토큰이 영구 잔존 → OOM/DoS 리스크. 주기적 제거 또는 Caffeine/Redis로 전환 필요

현재 만료 처리는 consume 시점에만 이뤄져, 소비되지 않은 만료 토큰이 메모리에 무기한 남습니다. 로그인 시도 스팸으로 메모리를 지속적으로 증가시킬 수 있어 OOM/DoS 위험이 있습니다. 최소한 주기적 스위핑 또는 TTL/사이즈 기반의 자동 제거가 필요합니다.

옵션 A: 간단한 주기적 스위핑 추가(1분마다 만료 토큰 제거)

+import org.springframework.scheduling.annotation.Scheduled;
+    // 만료 토큰 주기적 제거 (DoS 방어의 1차 안전망)
+    @Scheduled(fixedDelay = 60_000)
+    void evictExpired() {
+        long now = System.currentTimeMillis();
+        store.entrySet().removeIf(e -> e.getValue().expireAtEpochMillis <= now);
+    }

옵션 B: Caffeine 캐시(권장)로 교체하여 TTL/사이즈 기반 자동 제거

  • 장점: 고성능, 자동 만료/용량 제한, 통계/모니터링 지원.
  • TTL이 호출마다 달라야 한다면 per-entry expiry(Expiry) 구현을 사용. TTL이 고정(예: 5분)이면 expireAfterWrite로 간단히 구성 가능.
  • 멀티 인스턴스 환경까지 고려하면 Redis 같은 중앙 스토리지로 전환하는 것이 가장 안전합니다(아래 다른 코멘트 참고).

Also applies to: 38-47

🤖 Prompt for AI Agents
src/main/java/konkuk/thip/common/security/oauth2/LoginTokenStorage.java around
lines 28 and 38-47: 현재 ConcurrentHashMap에 만료 토큰이 소비될 때만 제거되어 메모리에 무기한 잔존해
OOM/DoS 위험이 있으니, 수정방법은 두가지 중 하나를 선택하여 적용하세요 — (A) 간단한 주기 스위퍼를 추가:
ScheduledExecutorService로 1분 간격으로 맵을 순회해 만료된 Entry를 제거하도록 구현(동시성 고려하여 iterator
사용 또는 keySet을 복사한 후 제거), 또는 (B, 권장) Caffeine으로 교체:
Caffeine.newBuilder().expireAfterWrite(TTL) 및 maximumSize(...)를 설정해 자동 만료/용량제한을
적용(토큰별 가변 TTL이 필요하면 Expiry 구현 사용); 멀티인스턴스 환경에서는 중앙 저장소(예: Redis)로 대체하는 방안도
검토하세요.

Comment on lines +33 to +36
public void put(String key, TokenType type, String token, Duration ttl) {
long expiredAt = Instant.now().plus(ttl).toEpochMilli();
store.put(key, new Entry(type, token, expiredAt));
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

입력값 검증 누락(null/blank/비양수 TTL 허용) → 방어적 코딩 필요

키/토큰/타입/TTL에 대한 기본 검증이 없어 의도치 않은 상태가 저장될 수 있습니다. 최소한의 가드 추가를 권합니다.

-    public void put(String key, TokenType type, String token, Duration ttl) {
-        long expiredAt = Instant.now().plus(ttl).toEpochMilli();
-        store.put(key, new Entry(type, token, expiredAt));
-    }
+    public void put(String key, TokenType type, String token, Duration ttl) {
+        if (key == null || key.isBlank()) throw new IllegalArgumentException("key must not be blank");
+        if (type == null) throw new IllegalArgumentException("type must not be null");
+        if (token == null || token.isBlank()) throw new IllegalArgumentException("token must not be blank");
+        if (ttl == null || ttl.isZero() || ttl.isNegative()) throw new IllegalArgumentException("ttl must be positive");
+
+        long expiresAt = System.currentTimeMillis() + ttl.toMillis();
+        store.put(key, new Entry(type, token, expiresAt));
+    }

추가 제안(선택): 동일 key 재사용 방지를 위해 putIfAbsent로 단일 발급 보장, 필요 시 반환형을 boolean으로 변경해 중복 발급 시도를 감지할 수 있습니다.

📝 Committable suggestion

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

Suggested change
public void put(String key, TokenType type, String token, Duration ttl) {
long expiredAt = Instant.now().plus(ttl).toEpochMilli();
store.put(key, new Entry(type, token, expiredAt));
}
public void put(String key, TokenType type, String token, Duration ttl) {
if (key == null || key.isBlank()) {
throw new IllegalArgumentException("key must not be blank");
}
if (type == null) {
throw new IllegalArgumentException("type must not be null");
}
if (token == null || token.isBlank()) {
throw new IllegalArgumentException("token must not be blank");
}
if (ttl == null || ttl.isZero() || ttl.isNegative()) {
throw new IllegalArgumentException("ttl must be positive");
}
long expiresAt = System.currentTimeMillis() + ttl.toMillis();
store.put(key, new Entry(type, token, expiresAt));
}
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/common/security/oauth2/LoginTokenStorage.java
around lines 33 to 36, the put method lacks input validation allowing null/blank
key/token/type and non-positive TTL; add defensive checks at the start to throw
IllegalArgumentException for null or blank key/token, null type, and ttl == null
or ttl.isZero() or ttl.isNegative(); compute expiredAt only after validation and
then store. Optionally, to prevent duplicate issuance, replace store.put(...)
with store.putIfAbsent(...) and change the method to return boolean indicating
whether insertion succeeded.

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.

[THIP2025-265] [hotfix] 소셜 로그인 수정

1 participant