Skip to content

refactor(server): update auth, user, and recruit member domains with …#277

Merged
CSE-Shaco merged 1 commit intoGDGoCINHA:developfrom
CSE-Shaco:develop
Feb 9, 2026
Merged

refactor(server): update auth, user, and recruit member domains with …#277
CSE-Shaco merged 1 commit intoGDGoCINHA:developfrom
CSE-Shaco:develop

Conversation

@CSE-Shaco
Copy link
Copy Markdown
Contributor

@CSE-Shaco CSE-Shaco commented Feb 9, 2026

…new requirements

📌 연관된 이슈

ex) #이슈번호, #이슈번호

✨ 작업 내용

이번 PR에서 작업한 내용을 간략히 설명해주세요

💬 리뷰 요구사항(선택)

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 회원가입 시 이메일, 학번, 전화번호 중복 확인 기능 추가
    • 쿠키 기반 세션 관리 도입 (액세스/리프레시 토큰)
    • 사용자 멤버십 상태 및 프로필 이미지 추적 기능 추가
  • 개선 사항

    • JWT 토큰에 세션 ID 포함
    • 쿠키 보안 설정 강화 (Secure, SameSite, Domain, Path)
    • 토큰 검증 및 인증 로직 개선
  • 설정 변경

    • 쿠키 기반 인증으로 전환
    • 환경 설정에 JWT 오디언스 및 액세스 토큰 유효시간 추가

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 9, 2026

Walkthrough

이 PR은 JWT 토큰 생성에 sessionId를 도입하고 Redis 기반 리프레시 토큰 관리를 추가하며, Google ID 토큰 검증 워크플로우를 구현합니다. 또한 사용자 관리 도메인을 재구성하고, 쿠키 기반 인증으로 전환하며, 회원 가입 및 이메일/전화번호 중복 확인 엔드포인트를 추가합니다.

Changes

Cohort / File(s) Summary
CI/CD 워크플로우 설정
.github/workflows/ci.yml, .github/workflows/deploy-dev.yml, .github/workflows/deploy-prod.yml
JWT_AUDIENCE 시크릿을 추가하고 Google OAuth 환경변수를 제거. 리프레시 쿠키 설정(SECURE, SAME_SITE, DOMAIN)을 추가하여 더 세밀한 쿠키 관리 구성 도입.
빌드 설정
build.gradle
Google API Client 버전을 중앙화하여 관리할 수 있도록 googleApiClientVersion 속성 추가.
사용자 관리 도메인 재구성
src/main/java/inha/gdgoc/domain/admin/user/controller/UserAdminController.java, src/main/java/inha/gdgoc/domain/admin/user/dto/request/*, src/main/java/inha/gdgoc/domain/admin/user/dto/response/UserSummaryResponse.java, src/main/java/inha/gdgoc/domain/admin/user/service/UserAdminService.java, src/main/java/inha/gdgoc/domain/user/dto/request/UpdateUserRoleTeamRequest.java
사용자 관리 관련 클래스들을 inha.gdgoc.domain.admin.user 패키지로 이동하여 도메인 구조를 정리. 기존 위치의 UpdateUserRoleTeamRequest는 삭제되고 새 위치에 재생성.
인증 시스템 업그레이드
src/main/java/inha/gdgoc/domain/auth/controller/AuthController.java, src/main/java/inha/gdgoc/domain/auth/service/AuthService.java
Google ID 토큰 검증, Redis 기반 리프레시 토큰 관리, 세션ID 기반 액세스 토큰 생성을 도입. 학번/전화번호 중복 확인 엔드포인트 추가. 로그인/회원가입 응답에 쿠키 설정 로직 통합.
인증 DTO 및 메시지
src/main/java/inha/gdgoc/domain/auth/dto/GoogleUserInfo.java, src/main/java/inha/gdgoc/domain/auth/dto/response/AccessTokenResponse.java, src/main/java/inha/gdgoc/domain/auth/dto/response/AuthUserResponse.java, src/main/java/inha/gdgoc/domain/auth/dto/response/CheckPhoneNumberResponse.java, src/main/java/inha/gdgoc/domain/auth/dto/response/CheckStudentIdResponse.java, src/main/java/inha/gdgoc/domain/auth/dto/response/LoginSuccessResponse.java, src/main/java/inha/gdgoc/domain/auth/dto/response/SignupNeededResponse.java, src/main/java/inha/gdgoc/domain/auth/controller/message/AuthMessage.java
새로운 응답 DTO(CheckStudentIdResponse, CheckPhoneNumberResponse) 추가. AuthUserResponse로 클래스 명칭 변경하고 user role/status 정보 확장. AccessTokenResponse에 user 정보 포함. GoogleUserInfo에 givenName/familyName 필드 및 Builder 지원 추가.
회원 모집 기능 확장
src/main/java/inha/gdgoc/domain/recruit/member/controller/RecruitMemberController.java, src/main/java/inha/gdgoc/domain/recruit/member/service/RecruitMemberService.java, src/main/java/inha/gdgoc/domain/recruit/member/repository/RecruitMemberRepository.java, src/main/java/inha/gdgoc/domain/recruit/member/dto/response/CheckEmailResponse.java, src/main/java/inha/gdgoc/domain/recruit/member/controller/message/RecruitMemberMessage.java
이메일 중복 확인 기능 추가. Multipart 지원 /apply 엔드포인트 추가. 컨트롤러 경로 구조를 /api/v1/recruit/member로 통합. 인증 접근 제어 로직 업데이트.
JWT 및 토큰 관리
src/main/java/inha/gdgoc/global/config/jwt/JwtProperties.java, src/main/java/inha/gdgoc/global/config/jwt/TokenProvider.java
JwtProperties에 audience 속성 추가. TokenProvider에서 sessionId를 포함한 액세스 토큰 생성, 새로운 클레임(uid, sid), 서명 키 캐싱, Google ID 토큰 검증 로직 도입.
보안 설정
src/main/java/inha/gdgoc/global/security/SecurityConfig.java, src/main/java/inha/gdgoc/global/security/TokenAuthenticationFilter.java
Authorization 헤더에서 쿠키 기반 access_token 추출로 변경. 보안 필터 경로 패턴을 recruit/member 구조에 맞게 업데이트. CORS 헤더 노출 설정 변경(Authorization 제거, Set-Cookie 유지).
사용자 엔티티 및 저장소
src/main/java/inha/gdgoc/domain/user/entity/User.java, src/main/java/inha/gdgoc/domain/user/repository/UserRepository.java
User 엔티티에 userRole과 membershipStatus 필드에 @Default 주석과 기본값 추가. oauth_subject 컬럼 매핑 명시. UserRepository에 existsByPhoneNumber 메서드 추가. UserSummaryResponse 임포트 경로를 admin.user 패키지로 업데이트.
애플리케이션 설정
src/main/resources/application-local.yml, src/main/resources/application-dev.yml, src/main/resources/application-prod.yml
리프레시/액세스 쿠키 설정 섹션 추가(secure, same-site, domain, path). JWT audience 및 accessTokenValidity 설정 추가. Google OAuth 관련 설정 제거.
데이터베이스 마이그레이션
src/main/resources/db/migration/V20260117__add_membership_status_to_users.sql, src/main/resources/db/migration/V20260118__ensure_oauth_subject_column.sql
membership_status 컬럼 추가 마이그레이션(기본값: PENDING). oauth_subject 컬럼 정규화 및 유니크 제약 추가 마이그레이션.

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant AuthCtrl as AuthController
    participant AuthSvc as AuthService
    participant GoogleAPI as Google API
    participant UserRepo as UserRepository
    participant Redis as Redis
    participant TokenProv as TokenProvider

    Client->>AuthCtrl: POST /login (idTokenString)
    AuthCtrl->>AuthSvc: authenticate(idTokenString)
    AuthSvc->>GoogleAPI: verifyGoogleToken(idTokenString)
    GoogleAPI-->>AuthSvc: GoogleUserInfo payload
    AuthSvc->>UserRepo: findByOAuthSubject()
    alt User exists
        UserRepo-->>AuthSvc: User
        AuthSvc->>TokenProv: generateTokens(user)
        TokenProv->>Redis: storeRefreshSession(refreshToken, sessionId)
        TokenProv-->>AuthSvc: TokenDto (accessToken, refreshToken)
        AuthSvc-->>AuthCtrl: LoginSuccessResponse
        AuthCtrl-->>Client: Set-Cookie + Response
    else User not found
        UserRepo-->>AuthSvc: null
        AuthSvc-->>AuthCtrl: SignupNeededResponse
        AuthCtrl-->>Client: Response (requires signup)
    end
Loading
sequenceDiagram
    actor Client
    participant AuthCtrl as AuthController
    participant AuthSvc as AuthService
    participant Redis as Redis
    participant TokenProv as TokenProvider
    participant UserRepo as UserRepository

    Client->>AuthCtrl: POST /refresh (refreshToken from cookie)
    AuthCtrl->>AuthSvc: refresh(refreshToken)
    AuthSvc->>Redis: resolveRefreshSession(refreshToken)
    alt Session found
        Redis-->>AuthSvc: RefreshSession (sessionId, userId)
        AuthSvc->>UserRepo: findById(userId)
        UserRepo-->>AuthSvc: User
        AuthSvc->>TokenProv: createAccessToken(user, sessionId)
        TokenProv-->>AuthSvc: new accessToken
        AuthSvc-->>AuthCtrl: RefreshResult (accessToken, user)
        AuthCtrl-->>Client: Set-Cookie (accessToken) + AccessTokenResponse
    else Session not found or invalid
        Redis-->>AuthSvc: null
        AuthSvc-->>AuthCtrl: Exception
        AuthCtrl-->>Client: 401 Unauthorized
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested labels

refactor, authentication, feature


🐰 세션 ID 줍줍, Redis에 쏙쏙
구글 검증 후 쿠키 냠냠
토큰 새 옷 입고 춤을 춘다네
리프레시 흐름 앗싸 반짝반짝 ✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Title check ⚠️ Warning PR 제목이 불완전합니다. 말줄임표로 끝나며 실제 변경 내용의 주요 부분이 누락되었습니다. 제목을 완성하여 인증, 사용자, 모집 멤버 도메인의 주요 변경 사항을 명확히 반영하도록 수정하십시오. 예: 'refactor(server): update auth, user, and recruit member domains with JWT token and cookie-based authentication'
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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 and usage tips.

Copy link
Copy Markdown

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

Caution

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

⚠️ Outside diff range comments (4)
src/main/java/inha/gdgoc/global/security/TokenAuthenticationFilter.java (1)

56-67: ⚠️ Potential issue | 🟠 Major

sanitizeToken에서 발생하는 예외가 catch되지 않아 500 에러가 발생합니다.

getAccessToken (Line 56)은 try 블록 바깥에서 호출되므로, sanitizeTokenIllegalArgumentException을 던지면 필터에서 처리되지 않고 500 응답이 반환됩니다. 토큰 취득과 인증을 모두 try-catch 안에서 처리하거나, sanitizeToken이 예외 대신 null을 반환하도록 수정해야 합니다.

🐛 수정 제안 (Option 1: try-catch 범위 확장)
-    String token = getAccessToken(request);
-    log.info("요청 URI: {}, access token 존재 여부: {}", request.getRequestURI(), token != null);
-
-    if (token != null) {
-        try {
-            Authentication authentication = tokenProvider.getAuthentication(token);
+    try {
+        String token = getAccessToken(request);
+        log.info("요청 URI: {}, access token 존재 여부: {}", request.getRequestURI(), token != null);
+
+        if (token != null) {
+            Authentication authentication = tokenProvider.getAuthentication(token);
             SecurityContextHolder.getContext().setAuthentication(authentication);
             log.info("JWT 인증 성공: {}", authentication.getName());
-        } catch (Exception e) {
-            SecurityContextHolder.clearContext();
-            log.warn("JWT 인증 실패: {}", e.getMessage());
+        } else {
+            log.info("access token 없음 → 인증 시도 안함");
         }
-    } else {
-        log.info("access token 없음 → 인증 시도 안함");
+    } catch (Exception e) {
+        SecurityContextHolder.clearContext();
+        log.warn("JWT 인증 실패: {}", e.getMessage());
     }
src/main/java/inha/gdgoc/domain/user/entity/User.java (1)

34-39: ⚠️ Potential issue | 🔴 Critical

클래스와 생성자에 @Builder가 중복 선언되어 @Default가 무시될 수 있습니다.

Line 34의 클래스 레벨 @Builder와 Line 102의 생성자 레벨 @Builder가 동시에 존재합니다. Lombok은 생성자 레벨 @Builder를 우선하므로, 필드에 선언된 @Default (Line 70, 79)가 무시됩니다. 그 결과:

  1. membershipStatus: 생성자(Line 102-122)에 해당 파라미터가 없어 빌더로 생성 시 null이 되며, DB의 NOT NULL 제약 조건 위반으로 런타임 에러가 발생합니다.
  2. userRole: 생성자에서 파라미터 값을 그대로 할당(Line 116)하므로 null이 전달되면 GUEST 기본값이 적용되지 않습니다.

클래스 레벨 @Builder를 제거하고 생성자에서 기본값을 직접 처리하거나, 반대로 생성자 레벨 @Builder를 제거하고 클래스 레벨만 유지하세요.

🐛 수정 제안 (생성자 레벨 `@Builder` 유지 시)
-@Builder
 `@Getter`
 `@Entity`
 `@Table`(name = "users")
 `@NoArgsConstructor`(access = AccessLevel.PROTECTED)
 `@AllArgsConstructor`
 public class User extends BaseEntity {

그리고 생성자에서 기본값을 직접 설정:

     `@Builder`
     public User(
             String name, String oauthSubject, String major, String studentId, String phoneNumber,
-            String email, UserRole userRole,
+            String email, UserRole userRole, MembershipStatus membershipStatus,
             TeamType team,
             String image, SocialUrls social, Careers careers
     ) {
         this.oauthSubject = oauthSubject;
         this.name = name;
         this.major = major;
         this.studentId = studentId;
         this.phoneNumber = phoneNumber;
         this.email = email;
-        this.userRole = userRole;
+        this.userRole = (userRole != null ? userRole : UserRole.GUEST);
+        this.membershipStatus = (membershipStatus != null ? membershipStatus : MembershipStatus.PENDING);
         this.team = team;
         this.image = image;
         this.social = (social != null ? social : new SocialUrls());
         this.careers = (careers != null ? careers : new Careers());
     }

Also applies to: 70-71, 78-80, 102-122

src/main/java/inha/gdgoc/domain/admin/user/service/UserAdminService.java (1)

146-183: ⚠️ Potential issue | 🟠 Major

updateUserRoleWithRules에서 me (CustomUserDetails)의 role/team을 직접 사용하지만, updateRoleAndTeam에서는 DB에서 다시 조회합니다.

updateUserRoleWithRules는 토큰의 me.getRole()/me.getTeam()을 신뢰하고, updateRoleAndTeamgetEditor(editor)로 DB에서 최신 상태를 조회합니다. 토큰 발급 후 역할/팀이 변경되었을 경우, updateUserRoleWithRules는 stale 데이터로 권한을 판단할 수 있습니다. 두 메서드 모두 일관되게 DB 조회를 사용하는 것이 더 안전합니다.

src/main/java/inha/gdgoc/domain/auth/controller/AuthController.java (1)

131-151: ⚠️ Potential issue | 🟡 Minor

Refresh 엔드포인트에서 Exception 전체를 catch하고 있습니다.

Line 147에서 모든 Exception을 잡아 INVALID_REFRESH_TOKEN으로 변환하면, 실제 서버 오류(DB 장애, NullPointerException 등)도 "유효하지 않은 토큰" 오류로 노출됩니다. 이는 디버깅을 어렵게 만들고 실제 문제를 숨길 수 있습니다. 특정 예외 타입만 catch하는 것을 권장합니다.

🤖 Fix all issues with AI agents
In @.github/workflows/deploy-dev.yml:
- Around line 42-45: Add the missing ACCESS_COOKIE_* environment variables to
the deploy workflows: ensure ACCESS_COOKIE_SECURE, ACCESS_COOKIE_SAME_SITE, and
ACCESS_COOKIE_DOMAIN are exported into the environment block (same style as
REFRESH_COOKIE_SECURE, REFRESH_COOKIE_SAME_SITE, REFRESH_COOKIE_DOMAIN) in both
deploy-dev.yml and deploy-prod.yml so that application-prod.yml can read
explicit values; use the corresponding secrets (e.g.,
secrets.ACCESS_COOKIE_SECURE, secrets.ACCESS_COOKIE_SAME_SITE,
secrets.ACCESS_COOKIE_DOMAIN) and fallbacks consistent with REFRESH_COOKIE_*
conventions.

In @.github/workflows/deploy-prod.yml:
- Line 46: The REFRESH_COOKIE_SECURE environment variable currently defaults to
'false' in the deploy workflow; change its default to 'true' so production
deployments set the refresh-token cookie Secure flag by default, while
preserving the ability for secrets to override it (i.e., use
REFRESH_COOKIE_SECURE=${{ secrets.REFRESH_COOKIE_SECURE || 'true' }}). Update
the workflow entry that references REFRESH_COOKIE_SECURE to use the new default
and ensure any downstream logic that reads REFRESH_COOKIE_SECURE treats the
value as a boolean/string accordingly.

In `@src/main/java/inha/gdgoc/domain/auth/controller/AuthController.java`:
- Around line 196-199: The closing brace for the method checkRoleOrTeam is
mis-indented (it appears at class-level indentation); move the `}` so it aligns
with the method's opening brace and ensure braces are balanced for
checkRoleOrTeam (verify the method body ends right after the
GlobalErrorCode.FORBIDDEN_USER block), then run the project's Java formatter to
confirm consistent indentation.
- Around line 109-129: The controller-level bean validation is not active
because the AuthController class lacks the `@Validated` annotation; add `@Validated`
to the AuthController class declaration (next to existing annotations like
`@RestController/`@RequestMapping/@RequiredArgsConstructor/@Slf4j) so method
parameter constraints on duplicatedStudentIdDetails and
duplicatedPhoneNumberDetails (`@NotBlank`, `@Pattern`) are enforced at runtime.

In `@src/main/java/inha/gdgoc/domain/auth/service/AuthService.java`:
- Line 63: The if (user == null) block in AuthService.java is mis-indented
compared to the surrounding 2-space style; reformat the block (inside the
AuthService class/method where the if (user == null) appears) to use consistent
2-space indentation and run the project formatter so all surrounding lines match
the repository style.
- Around line 260-281: The deriveNameParts method currently splits space-free
names by taking substring(0,1) as family and substring(1) as given, which
assumes a single-character family name (common in Korean) and is
locale-dependent; add a concise comment above deriveNameParts documenting this
assumption, noting that Google-provided given_name/family_name are preferred
when available, that the substring fallback may mis-handle compound surnames
(e.g., "남궁", "선우") and foreign names, and suggest using a locale-aware parser or
configurable policy in future if stricter correctness is required.
- Around line 193-214: In resolveRefreshSession, remove or disable the automatic
legacy-format migration (the block that calls userRepository.findByOauthSubject,
creates a new RefreshSession, and calls storeRefreshSession) because it allows
arbitrary Redis values to be upgraded into valid sessions; instead, treat
storedValue without SESSION_VALUE_DELIMITER as invalid and throw an exception
(or require an explicit migration flag/mechanism), ensuring only
decodeSessionValue(...) handles valid session strings; reference
resolveRefreshSession, refreshTokenKey, decodeSessionValue, storeRefreshSession,
userRepository.findByOauthSubject, and RefreshSession when making the change.
- Around line 48-77: The login method currently returns Object; introduce a
sealed interface LoginResponse that permits SignupNeededResponse and
LoginSuccessResponse, have those two response classes implement/extend
LoginResponse, change AuthService.login(String idToken) to return LoginResponse,
and update any callers (e.g., controller) to accept LoginResponse instead of
Object so callers no longer need instanceof checks and gain compile-time type
safety; ensure SignupNeededResponse.builder() and LoginSuccessResponse.of(...)
produce types that implement LoginResponse.
- Around line 79-107: The signup() flow currently only checks studentId
uniqueness; add service-level checks for phone and email before saving: after
computing cleanPhone use userRepository.existsByPhoneNumber(cleanPhone) and
throw IllegalArgumentException("이미 존재하는 전화번호입니다.") if true, and use
userRepository.existsByEmail(request.getEmail()) and throw
IllegalArgumentException("이미 존재하는 이메일입니다.") if true; keep the rest of the flow
(User.builder(...), userRepository.save(newUser), generateTokens(newUser), and
returning LoginSuccessResponse.of(tokens, AuthUserResponse.from(newUser)))
unchanged.

In
`@src/main/java/inha/gdgoc/domain/recruit/member/controller/RecruitMemberController.java`:
- Around line 75-83: The multipart endpoint recruitMemberAddMultipart accepts a
MultipartFile file but currently ignores it; either pass the file into the
service (e.g., update recruitMemberService.addRecruitMember(applicationRequest,
file) and implement file handling in the service), or remove the unused
`@RequestPart`("file") MultipartFile file parameter; if file handling is planned
later, add a clear TODO comment in recruitMemberAddMultipart referencing
MultipartFile file and/or create an overloaded service method
(recruitMemberService.addRecruitMember) that will accept and process the file
when implemented.
- Line 114: The email regex in the `@Pattern` annotation inside
RecruitMemberController (the string
"^[a-zA-Z0-9+-\\_.]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$") incorrectly allows a
character range because the hyphen in the local-part character class is not
escaped; update the regex in the `@Pattern` on RecruitMemberController to treat
'-' literally by either escaping it (e.g., '\-') or placing it at the start or
end of the character class so the local-part class only permits the intended
characters.

In `@src/main/java/inha/gdgoc/global/config/jwt/TokenProvider.java`:
- Around line 40-43: initSigningKey currently calls
buildSigningKey(jwtProperties.getSecretKey()) without validating the secret; add
a null/empty check for jwtProperties.getSecretKey() in initSigningKey and fail
fast with a clear exception (e.g., throw IllegalStateException with a
descriptive message) or log error and avoid calling buildSigningKey to prevent
NPE/invalid key creation; reference initSigningKey, buildSigningKey,
jwtProperties.getSecretKey(), and cachedSigningKey when making the change so the
validation wraps the call that sets cachedSigningKey.
- Around line 115-132: The buildSigningKey method in TokenProvider currently
replaces short keys by hashing them (candidateKey.length < 32) which hides
configuration errors; instead, change TokenProvider.buildSigningKey to validate
the decoded/bytes candidateKey length and throw a clear exception (e.g.,
IllegalArgumentException or IllegalStateException) when it's shorter than the
required 32 bytes, removing the SHA-256 fallback so misconfigured/too-short
secrets fail fast and surface the error.

In `@src/main/java/inha/gdgoc/global/security/SecurityConfig.java`:
- Around line 50-51: SecurityConfig permitAll() paths were changed to
"/api/v1/recruit/member/apply/**" and "/api/v1/recruit/member/check/**" but
TokenAuthenticationFilter.shouldNotFilter() still checks the old
"/api/v1/apply/**" and "/api/v1/check/**"; update the path patterns inside
TokenAuthenticationFilter.shouldNotFilter() to match the new patterns (e.g.,
"/api/v1/recruit/member/apply/**" and "/api/v1/recruit/member/check/**") so the
filter excludes the same public endpoints that SecurityConfig permits, keeping
behavior consistent between SecurityConfig and TokenAuthenticationFilter.

In `@src/main/resources/application-dev.yml`:
- Around line 60-70: Update the production cookie defaults so they are secure
even if env vars are missing: in the production config change the
auth.refresh-cookie and auth.access-cookie entries (properties named
refresh-cookie, access-cookie under auth) to set secure: true and same-site:
Strict (or same-site: None if you will enforce HTTPS and need cross-site use);
ensure these are hard-coded defaults in application-prod.yml rather than relying
solely on environment variable fallbacks.

In `@src/main/resources/application-prod.yml`:
- Around line 60-70: The production YAML sets REFRESH_COOKIE_SECURE and
ACCESS_COOKIE_SECURE defaulting to false; change the defaults so cookies are
secure by default in production by updating the auth.refresh-cookie.secure and
auth.access-cookie.secure entries to default to true (i.e., change
${REFRESH_COOKIE_SECURE:false} and ${ACCESS_COOKIE_SECURE:false} to default to
true) so that when those environment variables are missing the cookies will be
sent only over HTTPS.
- Around line 79-80: Add support for configuring JWT_ACCESS_TOKEN_VALIDITY via
CI secrets so accessTokenValidity in application-prod.yml can be set per
environment: update the deploy-prod.yml and deploy-dev.yml workflows to export
JWT_ACCESS_TOKEN_VALIDITY from secrets (e.g., add an env entry mapping
JWT_ACCESS_TOKEN_VALIDITY: ${{ secrets.JWT_ACCESS_TOKEN_VALIDITY }} alongside
JWT_AUDIENCE/GOOGLE_ISSUER/SELF_ISSUER), ensure the corresponding secret is
created in the target repo/environment, and keep the existing
${JWT_ACCESS_TOKEN_VALIDITY:3600000} fallback in application-prod.yml unchanged
so deployments without the secret still work.
🧹 Nitpick comments (23)
src/main/java/inha/gdgoc/domain/auth/dto/response/CheckPhoneNumberResponse.java (1)

1-4: recruit.member 패키지의 동일 DTO와 중복됩니다.

inha.gdgoc.domain.recruit.member.dto.response.CheckPhoneNumberResponse와 완전히 동일한 레코드가 존재합니다. 두 도메인 모두에서 필요하다면 공통 패키지로 추출하거나, 한쪽에서 다른 쪽을 재사용하는 것을 검토해 주세요.

#!/bin/bash
# 두 CheckPhoneNumberResponse의 사용처 확인
rg -rn "CheckPhoneNumberResponse" --type java
src/main/java/inha/gdgoc/domain/auth/dto/response/CheckStudentIdResponse.java (1)

1-4: recruit.member 패키지의 동일 DTO와 중복됩니다.

CheckPhoneNumberResponse와 마찬가지로 inha.gdgoc.domain.recruit.member.dto.response.CheckStudentIdResponse와 동일한 레코드입니다. 위 리뷰 코멘트와 동일하게 공통화를 검토해 주세요.

src/main/java/inha/gdgoc/domain/admin/user/dto/request/UpdateUserRoleTeamRequest.java (1)

6-9: null 의미를 설명하는 주석이 누락되었습니다.

기존 코드에는 roleteam 필드에 // null 이면 변경 안 함 주석이 있었습니다. 이 DTO의 null 허용 계약은 UserAdminService.updateRoleAndTeam()의 핵심 동작이므로, 주석을 유지하는 것이 좋습니다.

📝 주석 복원 제안
 public record UpdateUserRoleTeamRequest(
-        UserRole role,
-        TeamType team
+        UserRole role,   // null 이면 변경 안 함
+        TeamType team    // null 이면 변경 안 함
 ) {}
src/main/java/inha/gdgoc/global/security/TokenAuthenticationFilter.java (1)

75-85: HEADER_AUTHORIZATIONTOKEN_PREFIX가 미사용 상태입니다.

쿠키 기반으로 전환하면서 Line 76-77의 상수가 더 이상 사용되지 않습니다. 제거하세요.

♻️ 수정 제안
 private String getAccessToken(HttpServletRequest request) {
-    final String HEADER_AUTHORIZATION = "Authorization";
-    final String TOKEN_PREFIX = "Bearer ";
-
     String cookieToken = readCookieToken(request, "access_token");
     if (cookieToken != null) {
         return sanitizeToken(cookieToken.trim());
     }
 
     return null;
 }
src/main/java/inha/gdgoc/domain/user/entity/User.java (1)

151-151: MembershipStatus enum을 별도 파일로 분리하는 것을 권장합니다.

엔티티 내부에 중첩된 enum은 다른 패키지에서 참조 시 User.MembershipStatus로 접근해야 하므로 불필요한 결합이 생깁니다. DTO, 서비스, 마이그레이션 등 여러 곳에서 사용되므로 inha.gdgoc.domain.user.enums 패키지에 독립 enum으로 분리하면 UserRole, TeamType과 일관성도 유지됩니다.

src/main/java/inha/gdgoc/domain/admin/user/service/UserAdminService.java (2)

206-207: ADMIN case 블록에 의도를 명시하는 주석 추가를 권장합니다.

빈 블록은 "ADMIN은 rank 체크를 이미 통과했으므로 추가 제한 없음"이라는 의도이지만, 유지보수 시 실수로 누락된 것으로 오인될 수 있습니다.

♻️ 제안
-            case ADMIN -> {
-            }
+            case ADMIN -> {
+                // rank 검증만으로 충분 — 추가 제한 없음
+            }

232-235: getEditor 헬퍼 메서드 추출은 좋은 리팩토링입니다.

deleteUserWithRules(라인 187-188)도 동일 패턴을 사용하고 있으므로, 이 메서드를 재사용하면 중복을 줄일 수 있습니다.

♻️ 제안
     public void deleteUserWithRules(CustomUserDetails me, Long targetUserId) {
-        User editor = userRepository.findById(me.getUserId())
-                .orElseThrow(() -> new BusinessException(GlobalErrorCode.UNAUTHORIZED_USER));
+        User editor = getEditor(me);
         User target = userRepository.findById(targetUserId)
src/main/java/inha/gdgoc/domain/auth/dto/response/AuthUserResponse.java (1)

20-23: from(null)null을 반환하면 downstream에서 NPE 가능성이 있습니다.

LoginSuccessResponse.of(tokens, AuthUserResponse.from(user))에서 user가 null이면 응답의 user 필드가 null이 되어 클라이언트 측에서 예기치 않은 오류를 발생시킬 수 있습니다. null user는 호출 전에 검증하고, 여기서는 예외를 던지는 것이 더 안전할 수 있습니다.

src/main/java/inha/gdgoc/domain/admin/user/controller/UserAdminController.java (1)

54-57: size 파라미터에 상한이 없어 과도한 메모리 사용이 가능합니다.

악의적인 요청으로 size=100000 같은 값을 전달하면 대량의 데이터를 한 번에 조회할 수 있습니다. 상한값 제한을 권장합니다.

♻️ 제안
     `@RequestParam`(defaultValue = "0") int page,
     `@RequestParam`(defaultValue = "20") int size,
...
-    Pageable pageable = PageRequest.of(page, size, Sort.by(direction, sort));
+    int safeSize = Math.min(size, 100);
+    Pageable pageable = PageRequest.of(page, safeSize, Sort.by(direction, sort));
src/main/java/inha/gdgoc/global/config/jwt/JwtProperties.java (1)

17-17: audience 속성에 대한 유효성 검증이 없습니다.

JWT audience는 토큰 검증에 중요한 값입니다. @NotBlank 등의 어노테이션을 추가하면 애플리케이션 시작 시 누락된 설정을 빠르게 감지할 수 있습니다. 다른 필드(secretKey, selfIssuer 등)에도 동일하게 적용하면 좋지만, 기존 패턴과의 일관성을 고려하여 선택적으로 적용하시면 됩니다.

src/main/java/inha/gdgoc/domain/auth/dto/response/AccessTokenResponse.java (2)

7-7: DTO가 BaseEntity를 상속하고 있습니다.

AccessTokenResponse는 API 응답 DTO인데 JPA 엔티티용 BaseEntity를 상속하고 있습니다. DTO에 createdAt/updatedAt 같은 영속성 관련 필드가 JSON 응답에 불필요하게 포함될 수 있습니다. 이 상속을 제거하는 것을 권장합니다.


8-8: 필드명이 Java 네이밍 컨벤션을 따르지 않습니다.

access_token은 snake_case로, Java에서는 accessToken으로 선언하고 @JsonProperty("access_token")을 사용하는 것이 관례입니다. 같은 파일 내 LoginSuccessResponse에서는 accessToken (camelCase)을 사용하고 있어 일관성이 없습니다.

src/main/java/inha/gdgoc/domain/auth/controller/AuthController.java (3)

46-69: 8개의 @Value 필드를 @ConfigurationProperties 클래스로 통합하는 것을 권장합니다.

Refresh 쿠키 4개 + Access 쿠키 4개의 @Value 필드가 컨트롤러에 직접 주입되어 있습니다. @ConfigurationProperties로 바인딩하면 타입 안전성, 유효성 검증, 테스트 용이성이 향상되고 컨트롤러가 간결해집니다.

♻️ 제안: CookieProperties 클래스 분리
`@ConfigurationProperties`(prefix = "app.auth")
`@Validated`
public record AuthCookieProperties(
    CookieConfig refreshCookie,
    CookieConfig accessCookie
) {
    public record CookieConfig(
        boolean secure,
        String sameSite,
        String domain,
        String path
    ) {}
}

컨트롤러에서는 AuthCookieProperties를 주입받아 사용:

- `@Value`("${app.auth.refresh-cookie.domain:}")
- private String refreshCookieDomain;
- // ... 7개 더
+ private final AuthCookieProperties cookieProperties;

73-88: loginsignup의 쿠키 설정 로직이 중복됩니다.

Lines 76-82와 95-101이 거의 동일합니다. 공통 헬퍼 메서드로 추출하면 유지보수성이 향상됩니다.

또한, Object response 타입 사용으로 인해 타입 안전성이 없습니다. authService.login()LoginSuccessResponse 또는 SignupNeededResponse를 반환하는 것이라면, sealed interface나 공통 인터페이스를 도입하여 instanceof 체크를 대체하는 것을 고려해 주세요.


201-227: 쿠키 생성/삭제 로직이 컨트롤러에 과도하게 포함되어 있습니다.

약 60줄의 쿠키 헬퍼 메서드가 컨트롤러에 있습니다. 별도 유틸리티/서비스 클래스로 추출하면 컨트롤러의 단일 책임 원칙을 지킬 수 있고, 다른 컨트롤러에서도 재사용 가능합니다.

src/main/java/inha/gdgoc/global/config/jwt/TokenProvider.java (3)

46-63: subjectuid 클레임이 중복됩니다.

Line 55에서 subjectuser.getId()로, Line 57에서 uid 클레임도 동일하게 user.getId()로 설정하고 있습니다. 그런데 getAuthentication에서는 uid 클레임만 사용합니다. subject에 userId를 넣을 필요가 있는지 확인해 주세요. 불필요하다면 제거하여 토큰 페이로드를 간결하게 유지하는 것이 좋습니다.


73-100: 매 요청마다 DB 조회가 발생하여 성능 부담이 될 수 있습니다.

getAuthentication이 호출될 때마다 userRepository.findById(userId)로 DB를 조회합니다. 인증 필터에서 모든 요청에 대해 이 메서드가 호출되면, DB 부하가 상당할 수 있습니다. 짧은 TTL의 로컬 캐시(예: Caffeine)를 도입하거나, JWT 클레임에 role/team 정보를 포함하여 매번 DB를 조회하지 않도록 개선을 고려해 주세요.


163-185: CustomUserDetails에 빈 문자열 패스워드 사용 — 의도된 설계인지 확인 필요.

Line 179에서 super(username, "", authorities)로 빈 패스워드를 전달하고 있습니다. JWT 기반 인증이므로 패스워드가 필요 없는 것은 이해하지만, Spring Security의 User 클래스를 상속하는 대신 직접 UserDetails를 구현하면 패스워드 필드 자체를 피할 수 있어 더 명확합니다.

src/main/java/inha/gdgoc/domain/auth/service/AuthService.java (5)

166-185: GoogleIdTokenVerifier를 매 로그인 호출마다 새로 생성하고 있습니다.

NetHttpTransportGsonFactory는 thread-safe한 객체로, GoogleIdTokenVerifier를 필드로 캐싱하거나 @Bean으로 등록하면 불필요한 객체 생성을 줄일 수 있습니다.

♻️ 제안: 필드로 초기화
+  private GoogleIdTokenVerifier googleIdTokenVerifier;
+
+  `@jakarta.annotation.PostConstruct`
+  void initGoogleVerifier() {
+      this.googleIdTokenVerifier = new GoogleIdTokenVerifier.Builder(
+              new NetHttpTransport(), GsonFactory.getDefaultInstance())
+          .setAudience(Collections.singletonList(googleClientId))
+          .build();
+  }
+
   private GoogleUserInfo verifyGoogleToken(String idTokenString) {
     try {
-      GoogleIdTokenVerifier verifier =
-          new GoogleIdTokenVerifier.Builder(new NetHttpTransport(), new GsonFactory())
-              .setAudience(Collections.singletonList(googleClientId))
-              .build();
-
-      GoogleIdToken idToken = verifier.verify(idTokenString);
+      GoogleIdToken idToken = googleIdTokenVerifier.verify(idTokenString);

55-57: 도메인 검증에 IllegalArgumentException을 사용하고 있습니다.

프로젝트 전반에 BusinessExceptionErrorCode 패턴이 있음에도 불구하고, 여기서는 IllegalArgumentException을 사용합니다. 일관된 예외 처리를 위해 전용 ErrorCode를 정의하고 BusinessException을 사용하는 것이 좋습니다. 이 파일 내 다른 IllegalArgumentException 사용처(Line 84, 128, 147, 179, 183, 198, 209, 219, 225)에도 동일하게 적용되어야 합니다.


37-39: REFRESH_TOKEN_TTL이 하드코딩되어 있습니다.

14일 TTL이 코드에 직접 하드코딩되어 있습니다. JwtProperties나 별도 설정에서 관리하면 재배포 없이 변경할 수 있어 운영 유연성이 높아집니다.


122-133: refresh 메서드에서 Refresh Token rotation이 적용되지 않습니다.

현재 구현에서는 Access Token만 재발급하고 Refresh Token은 그대로 유지합니다. Refresh Token이 탈취된 경우 14일간 계속 사용될 수 있습니다. 보안 강화를 위해 refresh 시 Refresh Token도 함께 재발급(rotation)하고, 기존 토큰을 무효화하는 패턴을 고려해 주세요.


135-140: 로그아웃 시 삭제 결과를 확인하지 않습니다.

redisTemplate.delete()는 삭제된 키 수를 반환하지만, 현재 코드에서는 이를 무시합니다. 이미 로그아웃된 토큰이나 존재하지 않는 토큰에 대해서도 성공으로 처리됩니다. 멱등성 관점에서는 문제없지만, 로깅이라도 추가하면 디버깅에 도움이 됩니다.

Comment on lines +42 to +45
JWT_AUDIENCE=${{ secrets.JWT_AUDIENCE }}
REFRESH_COOKIE_SECURE=${{ secrets.REFRESH_COOKIE_SECURE || 'false' }}
REFRESH_COOKIE_SAME_SITE=${{ secrets.REFRESH_COOKIE_SAME_SITE || 'Lax' }}
REFRESH_COOKIE_DOMAIN=${{ secrets.REFRESH_COOKIE_DOMAIN }}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, search for ACCESS_COOKIE references across workflows
rg -n "ACCESS_COOKIE" .github/workflows/

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 53


🏁 Script executed:

# Check the full deploy-dev.yml file to see all env vars defined
cat -n .github/workflows/deploy-dev.yml

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 3598


🏁 Script executed:

# Check deploy-prod.yml for comparison
cat -n .github/workflows/deploy-prod.yml

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 3669


🏁 Script executed:

# Look for application-prod.yml to see what variables it references
fd "application-prod.yml" --type f

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 110


🏁 Script executed:

# Check if application-prod.yml references ACCESS_COOKIE_* variables
cat src/main/resources/application-prod.yml

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 2010


ACCESS_COOKIE_* 환경 변수가 두 배포 워크플로우에서 누락되었습니다.

application-prod.yml에서 ACCESS_COOKIE_SECURE, ACCESS_COOKIE_SAME_SITE, ACCESS_COOKIE_DOMAIN을 참조하고 있으나, deploy-dev.ymldeploy-prod.yml 모두에서 .env에 포함되지 않았습니다. REFRESH_COOKIE_* 환경 변수는 명시적으로 추가되었지만, 동일한 수준의 쿠키 설정인 ACCESS_COOKIE_*는 누락되어 있어 설정 관리의 일관성이 부족합니다. 애플리케이션은 기본값(secure: false, same-site: Lax)으로 정상 작동하겠지만, 명시적 관리를 위해 두 워크플로우에 모두 ACCESS_COOKIE_SECURE, ACCESS_COOKIE_SAME_SITE, ACCESS_COOKIE_DOMAIN을 추가해 주세요.

🤖 Prompt for AI Agents
In @.github/workflows/deploy-dev.yml around lines 42 - 45, Add the missing
ACCESS_COOKIE_* environment variables to the deploy workflows: ensure
ACCESS_COOKIE_SECURE, ACCESS_COOKIE_SAME_SITE, and ACCESS_COOKIE_DOMAIN are
exported into the environment block (same style as REFRESH_COOKIE_SECURE,
REFRESH_COOKIE_SAME_SITE, REFRESH_COOKIE_DOMAIN) in both deploy-dev.yml and
deploy-prod.yml so that application-prod.yml can read explicit values; use the
corresponding secrets (e.g., secrets.ACCESS_COOKIE_SECURE,
secrets.ACCESS_COOKIE_SAME_SITE, secrets.ACCESS_COOKIE_DOMAIN) and fallbacks
consistent with REFRESH_COOKIE_* conventions.

GOOGLE_ISSUER=${{ secrets.GOOGLE_ISSUER }}
SELF_ISSUER=${{ secrets.SELF_ISSUER }}
JWT_AUDIENCE=${{ secrets.JWT_AUDIENCE }}
REFRESH_COOKIE_SECURE=${{ secrets.REFRESH_COOKIE_SECURE || 'false' }}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

프로덕션 환경에서 REFRESH_COOKIE_SECURE의 기본값이 false입니다 — 보안 위험.

리프레시 토큰 쿠키가 Secure 플래그 없이 전송되면 HTTP를 통해 탈취될 수 있습니다. 프로덕션 배포에서는 기본값을 true로 설정해야 합니다.

🔒 제안
-          REFRESH_COOKIE_SECURE=${{ secrets.REFRESH_COOKIE_SECURE || 'false' }}
+          REFRESH_COOKIE_SECURE=${{ secrets.REFRESH_COOKIE_SECURE || 'true' }}
📝 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
REFRESH_COOKIE_SECURE=${{ secrets.REFRESH_COOKIE_SECURE || 'false' }}
REFRESH_COOKIE_SECURE=${{ secrets.REFRESH_COOKIE_SECURE || 'true' }}
🤖 Prompt for AI Agents
In @.github/workflows/deploy-prod.yml at line 46, The REFRESH_COOKIE_SECURE
environment variable currently defaults to 'false' in the deploy workflow;
change its default to 'true' so production deployments set the refresh-token
cookie Secure flag by default, while preserving the ability for secrets to
override it (i.e., use REFRESH_COOKIE_SECURE=${{ secrets.REFRESH_COOKIE_SECURE
|| 'true' }}). Update the workflow entry that references REFRESH_COOKIE_SECURE
to use the new default and ensure any downstream logic that reads
REFRESH_COOKIE_SECURE treats the value as a boolean/string accordingly.

Comment on lines +109 to +129
@GetMapping("/check/student-id")
public ResponseEntity<ApiResponse<CheckStudentIdResponse, Void>> duplicatedStudentIdDetails(
@RequestParam
@NotBlank(message = "학번은 필수 입력 값입니다.")
@Pattern(regexp = "^12[0-9]{6}$", message = "유효하지 않은 학번 값입니다.")
String studentId
) {
CheckStudentIdResponse response = authService.isRegisteredStudentId(studentId);
return ResponseEntity.ok(ApiResponse.ok(STUDENT_ID_DUPLICATION_CHECK_SUCCESS, response));
}

@GetMapping("/check/phone-number")
public ResponseEntity<ApiResponse<CheckPhoneNumberResponse, Void>> duplicatedPhoneNumberDetails(
@RequestParam
@NotBlank(message = "전화번호는 필수 입력 값입니다.")
@Pattern(regexp = "^010-\\d{4}-\\d{4}$", message = "전화번호 형식은 010-XXXX-XXXX 이어야 합니다.")
String phoneNumber
) {
CheckPhoneNumberResponse response = authService.isRegisteredPhoneNumber(phoneNumber);
return ResponseEntity.ok(ApiResponse.ok(PHONE_NUMBER_DUPLICATION_CHECK_SUCCESS, response));
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

rg -n "@Validated" src/main/java/inha/gdgoc/domain/auth/controller/AuthController.java

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 53


🏁 Script executed:

head -30 src/main/java/inha/gdgoc/domain/auth/controller/AuthController.java

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 1580


🏁 Script executed:

rg -n "class AuthController" src/main/java/inha/gdgoc/domain/auth/controller/AuthController.java -A 5

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 301


🏁 Script executed:

rg -B 5 "class AuthController" src/main/java/inha/gdgoc/domain/auth/controller/AuthController.java

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 181


메서드 파라미터 검증을 활성화하려면 @Validated 어노테이션이 필요합니다.

@NotBlank@Pattern 유효성 검증이 메서드 파라미터에 적용되어 있으나, 이들이 실제로 작동하려면 클래스 레벨에 @Validated 어노테이션을 추가해야 합니다. 현재 클래스에는 이 어노테이션이 없어 검증이 수행되지 않습니다.

`@Validated`
`@Slf4j`
`@RequestMapping`("/api/v1/auth")
`@RestController`
`@RequiredArgsConstructor`
public class AuthController {
🤖 Prompt for AI Agents
In `@src/main/java/inha/gdgoc/domain/auth/controller/AuthController.java` around
lines 109 - 129, The controller-level bean validation is not active because the
AuthController class lacks the `@Validated` annotation; add `@Validated` to the
AuthController class declaration (next to existing annotations like
`@RestController/`@RequestMapping/@RequiredArgsConstructor/@Slf4j) so method
parameter constraints on duplicatedStudentIdDetails and
duplicatedPhoneNumberDetails (`@NotBlank`, `@Pattern`) are enforced at runtime.

Comment on lines 196 to +199
GlobalErrorCode.FORBIDDEN_USER.getMessage(),
null
));
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

checkRoleOrTeam 메서드의 닫는 괄호 들여쓰기가 어긋나 있습니다.

Line 199의 }가 메서드 본문과 같은 레벨에 있어야 하는데, 클래스 레벨 들여쓰기로 보입니다.

🔧 수정 제안
-}
+    }
📝 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
GlobalErrorCode.FORBIDDEN_USER.getMessage(),
null
));
}
GlobalErrorCode.FORBIDDEN_USER.getMessage(),
null
));
}
🤖 Prompt for AI Agents
In `@src/main/java/inha/gdgoc/domain/auth/controller/AuthController.java` around
lines 196 - 199, The closing brace for the method checkRoleOrTeam is
mis-indented (it appears at class-level indentation); move the `}` so it aligns
with the method's opening brace and ensure braces are balanced for
checkRoleOrTeam (verify the method body ends right after the
GlobalErrorCode.FORBIDDEN_USER block), then run the project's Java formatter to
confirm consistent indentation.

Comment on lines +48 to +77
// 로그인
@Transactional
public Object login(String idToken) {
// Google ID Token 검증
GoogleUserInfo googleUser = verifyGoogleToken(idToken);

// 도메인 검증 (인하대 메일만 허용)
if (!googleUser.getEmail().endsWith("@inha.edu")) {
throw new IllegalArgumentException("인하대학교(@inha.edu) 계정만 이용 가능합니다.");
}
// 도메인 검증 (인하대 메일만 허용)
if (!googleUser.getEmail().endsWith("@inha.edu")) {
throw new IllegalArgumentException("인하대학교(@inha.edu) 계정만 이용 가능합니다.");
}

// DB에서 유저 조회 (OAuth Subject 기준)
User user = userRepository.findByOauthSubject(googleUser.getSub()).orElse(null);
// DB에서 유저 조회 (OAuth Subject 기준)
User user = userRepository.findByOauthSubject(googleUser.getSub()).orElse(null);

// 신규 유저 -> 회원가입 필요 응답 (202 or 200 with isNewUser=true)
// 신규 유저 -> 회원가입 필요 응답 (202 or 200 with isNewUser=true)
if (user == null) {
return SignupNeededResponse.builder()
.isNewUser(true)
.oauthSubject(googleUser.getSub())
.email(googleUser.getEmail())
.name(googleUser.getName())
.build();
}

// 기존 유저 -> 토큰 발급 및 로그인 성공 응답
TokenDto tokens = generateTokens(user);
return LoginSuccessResponse.of(user, tokens);
String preferredName =
hasText(googleUser.getFamilyName()) ? googleUser.getFamilyName() : googleUser.getName();
return SignupNeededResponse.builder()
.isNewUser(true)
.oauthSubject(googleUser.getSub())
.email(googleUser.getEmail())
.name(preferredName)
.build();
}

//회원가입
@Transactional
public LoginSuccessResponse signup(SignupRequest request) {
// 학번 중복 체크
if (userRepository.existsByStudentId(request.getStudentId())) {
throw new IllegalArgumentException("이미 존재하는 학번입니다.");
}

// 전화번호 정규화 (숫자만 남김)
String cleanPhone = request.getPhoneNumber().replaceAll("[^0-9]", "");

// 유저 엔티티 생성 및 저장
User newUser = User.builder()
.oauthSubject(request.getOauthSubject()) // 구글 sub
.email(request.getEmail())
.name(request.getName())
.studentId(request.getStudentId())
.major(request.getMajor())
.phoneNumber(cleanPhone)
// Role(GUEST), Status(PENDING) 등은 User 엔티티 생성자에서 기본값 처리됨
.build();

userRepository.save(newUser);

// 토큰 발급
TokenDto tokens = generateTokens(newUser);
return LoginSuccessResponse.of(newUser, tokens);
// 기존 유저 -> 토큰 발급 및 로그인 성공 응답
TokenDto tokens = generateTokens(user);
return LoginSuccessResponse.of(tokens, AuthUserResponse.from(user));
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

login 메서드의 반환 타입이 Object로 선언되어 타입 안전성이 결여됩니다.

SignupNeededResponse 또는 LoginSuccessResponse를 반환하는데, 반환 타입이 Object이면 호출자(Controller)에서 instanceof 체크가 필요하고 컴파일 타임에 오류를 잡을 수 없습니다. sealed interface 또는 공통 상위 타입을 도입하여 타입 안전성을 확보하는 것을 권장합니다.

♻️ 예시: sealed interface 활용
public sealed interface LoginResponse permits SignupNeededResponse, LoginSuccessResponse {}

그 후 반환 타입을 LoginResponse로 변경:

-  public Object login(String idToken) {
+  public LoginResponse login(String idToken) {
🤖 Prompt for AI Agents
In `@src/main/java/inha/gdgoc/domain/auth/service/AuthService.java` around lines
48 - 77, The login method currently returns Object; introduce a sealed interface
LoginResponse that permits SignupNeededResponse and LoginSuccessResponse, have
those two response classes implement/extend LoginResponse, change
AuthService.login(String idToken) to return LoginResponse, and update any
callers (e.g., controller) to accept LoginResponse instead of Object so callers
no longer need instanceof checks and gain compile-time type safety; ensure
SignupNeededResponse.builder() and LoginSuccessResponse.of(...) produce types
that implement LoginResponse.

Comment on lines +115 to +132
private SecretKey buildSigningKey(String rawSecret) {
byte[] candidateKey;
try {
candidateKey = Decoders.BASE64.decode(rawSecret);
} catch (IllegalArgumentException ignore) {
candidateKey = rawSecret.getBytes(StandardCharsets.UTF_8);
}

if (candidateKey.length < 32) {
try {
candidateKey = MessageDigest.getInstance("SHA-256").digest(candidateKey);
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("SHA-256 algorithm not available", e);
}
}

return Keys.hmacShaKeyFor(candidateKey);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

짧은 시크릿 키를 SHA-256으로 자동 변환하면 설정 오류가 은폐됩니다.

candidateKey.length < 32인 경우 SHA-256 해시로 대체하는 로직은, 잘못되거나 너무 짧은 시크릿이 설정되었을 때 경고 없이 동작하게 만듭니다. 프로덕션에서 약한 키가 사용되는 것을 방지하려면, 최소 길이 미달 시 명시적으로 실패(예외)시키는 것이 더 안전합니다.

🛡️ 제안: 짧은 키에 대해 예외를 던지도록 변경
         if (candidateKey.length < 32) {
-            try {
-                candidateKey = MessageDigest.getInstance("SHA-256").digest(candidateKey);
-            } catch (NoSuchAlgorithmException e) {
-                throw new IllegalStateException("SHA-256 algorithm not available", e);
-            }
+            throw new IllegalStateException(
+                "JWT signing key must be at least 256 bits (32 bytes). " +
+                "Provided key is only " + candidateKey.length + " bytes.");
         }
📝 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
private SecretKey buildSigningKey(String rawSecret) {
byte[] candidateKey;
try {
candidateKey = Decoders.BASE64.decode(rawSecret);
} catch (IllegalArgumentException ignore) {
candidateKey = rawSecret.getBytes(StandardCharsets.UTF_8);
}
if (candidateKey.length < 32) {
try {
candidateKey = MessageDigest.getInstance("SHA-256").digest(candidateKey);
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("SHA-256 algorithm not available", e);
}
}
return Keys.hmacShaKeyFor(candidateKey);
}
private SecretKey buildSigningKey(String rawSecret) {
byte[] candidateKey;
try {
candidateKey = Decoders.BASE64.decode(rawSecret);
} catch (IllegalArgumentException ignore) {
candidateKey = rawSecret.getBytes(StandardCharsets.UTF_8);
}
if (candidateKey.length < 32) {
throw new IllegalStateException(
"JWT signing key must be at least 256 bits (32 bytes). " +
"Provided key is only " + candidateKey.length + " bytes.");
}
return Keys.hmacShaKeyFor(candidateKey);
}
🤖 Prompt for AI Agents
In `@src/main/java/inha/gdgoc/global/config/jwt/TokenProvider.java` around lines
115 - 132, The buildSigningKey method in TokenProvider currently replaces short
keys by hashing them (candidateKey.length < 32) which hides configuration
errors; instead, change TokenProvider.buildSigningKey to validate the
decoded/bytes candidateKey length and throw a clear exception (e.g.,
IllegalArgumentException or IllegalStateException) when it's shorter than the
required 32 bytes, removing the SHA-256 fallback so misconfigured/too-short
secrets fail fast and surface the error.

Comment on lines +50 to +51
"/api/v1/recruit/member/apply/**",
"/api/v1/recruit/member/check/**",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd "TokenAuthenticationFilter.java" --exec cat {} \;

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 3949


TokenAuthenticationFilter.shouldNotFilter()의 엔드포인트 경로도 함께 업데이트되어야 합니다.

이 프로젝트의 보안 아키텍처는 SecurityConfig의 .permitAll()TokenAuthenticationFilter.shouldNotFilter()에서 공개 엔드포인트를 이중으로 관리합니다. 그러나 현재 SecurityConfig는 /api/v1/apply/**, /api/v1/check/**에서 /api/v1/recruit/member/apply/**, /api/v1/recruit/member/check/**로 변경되었으나, TokenAuthenticationFilter의 shouldNotFilter() 메서드(40-41줄)는 여전히 기존 경로 패턴을 사용하고 있습니다.

결과적으로 새로운 엔드포인트 경로(/api/v1/recruit/member/apply/**, /api/v1/recruit/member/check/**)는 Spring Security에서는 인증 불필요로 설정되었지만, 토큰 필터링 단계에서 제외되지 않아 일관성이 깨집니다. 건축상 일관성을 유지하고 불필요한 필터 실행을 방지하려면 TokenAuthenticationFilter의 shouldNotFilter() 메서드도 동일하게 업데이트하세요.

🤖 Prompt for AI Agents
In `@src/main/java/inha/gdgoc/global/security/SecurityConfig.java` around lines 50
- 51, SecurityConfig permitAll() paths were changed to
"/api/v1/recruit/member/apply/**" and "/api/v1/recruit/member/check/**" but
TokenAuthenticationFilter.shouldNotFilter() still checks the old
"/api/v1/apply/**" and "/api/v1/check/**"; update the path patterns inside
TokenAuthenticationFilter.shouldNotFilter() to match the new patterns (e.g.,
"/api/v1/recruit/member/apply/**" and "/api/v1/recruit/member/check/**") so the
filter excludes the same public endpoints that SecurityConfig permits, keeping
behavior consistent between SecurityConfig and TokenAuthenticationFilter.

Comment on lines +60 to +70
auth:
refresh-cookie:
secure: ${REFRESH_COOKIE_SECURE:false}
same-site: ${REFRESH_COOKIE_SAME_SITE:Lax}
domain: ${REFRESH_COOKIE_DOMAIN:}
path: /
access-cookie:
secure: ${ACCESS_COOKIE_SECURE:false}
same-site: ${ACCESS_COOKIE_SAME_SITE:Lax}
domain: ${ACCESS_COOKIE_DOMAIN:}
path: /
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

fd "application-prod" --extension yml --exec cat {}

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 2010


프로덕션 환경에서 쿠키 보안 설정이 취약합니다.

application-prod.yml에서 쿠키 설정이 개발 환경과 동일하게 secure: falsesame-site: Lax의 기본값을 사용하고 있습니다. 환경 변수 설정에 의존하는 것은 설정 누락 시 보안 위험을 초래할 수 있습니다.

프로덕션 환경에서는 다음을 수정해 주세요:

  • secure: true (기본값으로 설정)
  • same-site: Strict 또는 None (HTTPS 필수)

프로덕션에서 안전한 기본값을 보장해야 합니다.

🤖 Prompt for AI Agents
In `@src/main/resources/application-dev.yml` around lines 60 - 70, Update the
production cookie defaults so they are secure even if env vars are missing: in
the production config change the auth.refresh-cookie and auth.access-cookie
entries (properties named refresh-cookie, access-cookie under auth) to set
secure: true and same-site: Strict (or same-site: None if you will enforce HTTPS
and need cross-site use); ensure these are hard-coded defaults in
application-prod.yml rather than relying solely on environment variable
fallbacks.

Comment on lines +60 to +70
auth:
refresh-cookie:
secure: ${REFRESH_COOKIE_SECURE:false}
same-site: ${REFRESH_COOKIE_SAME_SITE:Lax}
domain: ${REFRESH_COOKIE_DOMAIN:}
path: /
access-cookie:
secure: ${ACCESS_COOKIE_SECURE:false}
same-site: ${ACCESS_COOKIE_SAME_SITE:Lax}
domain: ${ACCESS_COOKIE_DOMAIN:}
path: /
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

프로덕션 환경에서 쿠키 secure 기본값이 false입니다.

프로덕션 설정 파일에서 REFRESH_COOKIE_SECUREACCESS_COOKIE_SECURE의 기본값이 false로 설정되어 있습니다. 환경 변수가 누락되거나 설정되지 않으면 쿠키가 HTTP를 통해 전송되어 중간자 공격(MITM)에 의한 토큰 탈취 위험이 있습니다. application-local.yml이나 application-dev.yml에서는 false가 적절하지만, 프로덕션 설정에서는 기본값을 true로 설정하는 것이 안전합니다.

🔒 제안된 수정
   auth:
     refresh-cookie:
-      secure: ${REFRESH_COOKIE_SECURE:false}
+      secure: ${REFRESH_COOKIE_SECURE:true}
       same-site: ${REFRESH_COOKIE_SAME_SITE:Lax}
       domain: ${REFRESH_COOKIE_DOMAIN:}
       path: /
     access-cookie:
-      secure: ${ACCESS_COOKIE_SECURE:false}
+      secure: ${ACCESS_COOKIE_SECURE:true}
       same-site: ${ACCESS_COOKIE_SAME_SITE:Lax}
       domain: ${ACCESS_COOKIE_DOMAIN:}
       path: /
🤖 Prompt for AI Agents
In `@src/main/resources/application-prod.yml` around lines 60 - 70, The production
YAML sets REFRESH_COOKIE_SECURE and ACCESS_COOKIE_SECURE defaulting to false;
change the defaults so cookies are secure by default in production by updating
the auth.refresh-cookie.secure and auth.access-cookie.secure entries to default
to true (i.e., change ${REFRESH_COOKIE_SECURE:false} and
${ACCESS_COOKIE_SECURE:false} to default to true) so that when those environment
variables are missing the cookies will be sent only over HTTPS.

Comment on lines +79 to +80
audience: ${JWT_AUDIENCE}
accessTokenValidity: ${JWT_ACCESS_TOKEN_VALIDITY:3600000}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's check if JWT_ACCESS_TOKEN_VALIDITY is referenced in GitHub workflows
rg -n "ACCESS_TOKEN_VALIDITY" .github/workflows/

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 53


🏁 Script executed:

# Also search for deploy-dev and deploy-prod files to understand .env creation
fd -e yml -e yaml | grep -E "deploy-(dev|prod)" | head -20

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 53


🏁 Script executed:

# Check how other JWT environment variables are configured in workflows
rg -n "JWT_" .github/workflows/ -A 2 -B 2

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 1640


🏁 Script executed:

# Look at the application-prod.yml file to see what JWT configs are there
cat -n src/main/resources/application-prod.yml | grep -A 10 -B 10 "JWT\|jwt" | head -50

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 696


배포 환경별로 JWT_ACCESS_TOKEN_VALIDITY를 설정할 수 있도록 구성 필요합니다.

application-prod.yml에서 accessTokenValidity는 기본값(3600000ms = 1시간)으로 설정되어 있으나, .github/workflows/deploy-prod.yml.github/workflows/deploy-dev.yml에서 JWT_ACCESS_TOKEN_VALIDITY 환경 변수가 secrets를 통해 전달되지 않고 있습니다. 다른 JWT 설정들(JWT_AUDIENCE, GOOGLE_ISSUER, SELF_ISSUER)은 이미 secrets로 관리되고 있는데, JWT_ACCESS_TOKEN_VALIDITY도 같은 방식으로 관리하면 배포 환경별로 토큰 유효성 시간을 유연하게 조정할 수 있습니다. 현재는 기본값만으로 동작하므로 필요시 워크플로우 파일에 이 환경 변수를 추가해 주시기 바랍니다.

🤖 Prompt for AI Agents
In `@src/main/resources/application-prod.yml` around lines 79 - 80, Add support
for configuring JWT_ACCESS_TOKEN_VALIDITY via CI secrets so accessTokenValidity
in application-prod.yml can be set per environment: update the deploy-prod.yml
and deploy-dev.yml workflows to export JWT_ACCESS_TOKEN_VALIDITY from secrets
(e.g., add an env entry mapping JWT_ACCESS_TOKEN_VALIDITY: ${{
secrets.JWT_ACCESS_TOKEN_VALIDITY }} alongside
JWT_AUDIENCE/GOOGLE_ISSUER/SELF_ISSUER), ensure the corresponding secret is
created in the target repo/environment, and keep the existing
${JWT_ACCESS_TOKEN_VALIDITY:3600000} fallback in application-prod.yml unchanged
so deployments without the secret still work.

@CSE-Shaco CSE-Shaco merged commit fdc9eae into GDGoCINHA:develop Feb 9, 2026
2 checks passed
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.

1 participant