Skip to content

Fix/issue 172 oauth login#174

Merged
Kosw6 merged 4 commits intomainfrom
fix/issue-172-oauth-login
Dec 12, 2025
Merged

Fix/issue 172 oauth login#174
Kosw6 merged 4 commits intomainfrom
fix/issue-172-oauth-login

Conversation

@Kosw6
Copy link
Collaborator

@Kosw6 Kosw6 commented Dec 12, 2025

Summary by CodeRabbit

출시 노트

  • 새로운 기능

    • 소셜 로그인 흐름 개선: 기존 소셜 연동 계정 우선 탐색 후 이메일 기반 매칭, 없으면 신규 계정 생성 및 연동
    • 이메일 미제공 소셜에 대해 가상 이메일 생성으로 계정 생성 보장
  • 버그 수정

    • OAuth 계정 중복 생성 방지 및 경쟁 상태 처리로 연동 충돌 감소
  • 보안

    • 토큰 암호화 기본 키 값 강화

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

@Kosw6 Kosw6 requested a review from discipline24 as a code owner December 12, 2025 16:47
@coderabbitai
Copy link

coderabbitai bot commented Dec 12, 2025

Walkthrough

OAuth 인증 흐름이 재구성되어 (provider, providerUid) 우선 조회 후 이메일 기반 확인 및 신규 사용자 생성·링크 로직이 추가되었고, UserOauthAccountRepository에 existsByProviderAndUser 메서드가 추가되었으며 TokenEncryptor 기본 키값이 변경되었습니다.

Changes

Cohort / File(s) Summary
OAuth2 사용자 계정 리포지토리
backend/src/main/java/org/sejongisc/backend/auth/dao/UserOauthAccountRepository.java
제공자와 사용자로 기존 OAuth 계정 링크 존재 여부를 확인하는 새 메서드 existsByProviderAndUser(AuthProvider, User) 추가
OAuth2 사용자 서비스 로직
backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOAuth2UserService.java
OAuth 로그인 흐름 재구성: (provider, providerUid) → email → 신규 생성 순서로 사용자 조회/링크; AuthProvider 사용, 가상 이메일 생성, 동시성(DataIntegrityViolationException) 처리 및 OAuth2User 속성 확장
토큰 암호화 설정
backend/src/main/java/org/sejongisc/backend/common/auth/jwt/TokenEncryptor.java
환경변수 TOKEN_ENCRYPTION_KEY의 기본값을 mySecretKey1234에서 mySecretKey12345로 변경 (검증 규칙 유지)

Sequence Diagram(s)

sequenceDiagram
    participant OAuth as OAuth Provider
    participant Service as CustomOAuth2UserService
    participant OAuthRepo as UserOauthAccountRepository
    participant UserRepo as UserRepository
    participant DB as Database

    OAuth->>Service: 로그인 응답 (provider, providerUid, email?, name?)
    
    rect rgb(200,220,240)
    Note over Service: 1) provider+providerUid로 기존 링크 조회
    Service->>OAuthRepo: findByProviderAndProviderUid(provider, uid)
    OAuthRepo->>DB: 쿼리
    DB-->>OAuthRepo: 결과
    OAuthRepo-->>Service: Optional<UserOauthAccount>
    end

    alt 기존 링크가 존재
        Service-->>Service: 연관 User 로드 및 반환 (DefaultOAuth2User)
    else 링크 없음
        rect rgb(220,240,200)
        Note over Service: 2) 이메일로 사용자 조회
        Service->>UserRepo: findByEmail(email 또는 가상 이메일)
        UserRepo->>DB: 쿼리
        DB-->>UserRepo: 결과
        UserRepo-->>Service: Optional<User>
        end

        alt 이메일로 사용자 존재
            rect rgb(240,240,200)
            Note over Service: 3) 존재 사용자에 OAuth 링크 생성 시도
            Service->>OAuthRepo: save(new UserOauthAccount)
            OAuthRepo->>DB: insert
            alt 삽입 성공
                DB-->>OAuthRepo: 저장 완료
                OAuthRepo-->>Service: UserOauthAccount
            else DataIntegrityViolationException
                DB-->>OAuthRepo: 예외
                OAuthRepo-->>Service: 예외 전달
                Service->>OAuthRepo: existsByProviderAndUser(provider, user)
                OAuthRepo-->>Service: true/false
                Service-->>Service: 기존 링크 재확인 후 사용자 로드
            end
            end
        else 신규 사용자
            rect rgb(240,220,240)
            Note over Service: 4) 신규 User 생성 및 OAuth 링크 생성
            Service->>UserRepo: save(new User)
            UserRepo->>DB: insert
            DB-->>UserRepo: User 저장
            UserRepo-->>Service: User
            Service->>OAuthRepo: save(new UserOauthAccount)
            OAuthRepo->>DB: insert
            DB-->>OAuthRepo: UserOauthAccount 저장
            OAuthRepo-->>Service: UserOauthAccount
            end
        end
    end

    Service-->>OAuth: DefaultOAuth2User (provider, providerUid, email, name, userId, ROLE_TEAM_MEMBER)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20-25 minutes

  • 주의 필요 영역:
    • CustomOAuth2UserService에서 (provider, providerUid) → email → 신규 생성 흐름의 경계와 null 안전성
    • DataIntegrityViolationException 처리 로직 및 existsByProviderAndUser 호출의 동시성 보장
    • 가상 이메일 생성 방식이 사용자 식별 규칙과 충돌하지 않는지 검증

Possibly related PRs

Suggested reviewers

  • discipline24

Poem

🐰 토큰은 바삭, 계정은 포근,
프로바이더 따라 길을 닦았네.
이메일 없던 밤엔 가짜 주소로,
충돌 올 때도 슬기롭게 묶어,
새 길로 뛰어가는 우리 인증 한 줌.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 OAuth 로그인 관련 버그 수정이라는 주요 변경사항을 명확하게 요약하고 있으며, 실제 변경사항(UserOauthAccountRepository 메서드 추가, CustomOAuth2UserService 리팩토링, TokenEncryptor 기본값 변경)과 일치합니다.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/issue-172-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

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

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

Caution

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

⚠️ Outside diff range comments (2)
backend/src/main/java/org/sejongisc/backend/common/auth/jwt/TokenEncryptor.java (1)

24-30: 하드코딩된 기본 암호화 키는 프로덕션 보안 사고로 직결됩니다 (기본값 제거 권장).

환경변수가 누락되면 누구나 아는 키로 토큰을 복호화할 수 있습니다. 또한 key.length() == 16은 “문자” 기준이라 UTF-8 바이트 길이가 16이 아닐 수 있습니다(비-ASCII).
아래처럼 기본값을 빈 값으로 두고(byte 길이로) fail-fast 하는 쪽이 안전합니다.

-    public TokenEncryptor(@Value("${TOKEN_ENCRYPTION_KEY:mySecretKey12345}") String key) {
-        if (key == null || key.length() != 16) {
+    public TokenEncryptor(@Value("${TOKEN_ENCRYPTION_KEY:}") String key) {
+        byte[] keyBytes = (key == null) ? null : key.getBytes(StandardCharsets.UTF_8);
+        if (keyBytes == null || keyBytes.length != 16) {
             throw new IllegalStateException(
                     "유효한 16바이트 토큰 암호화 키가 설정되지 않았습니다. 환경변수 TOKEN_ENCRYPTION_KEY를 확인하세요.");
         }
-        this.secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), ALGORITHM);
+        this.secretKey = new SecretKeySpec(keyBytes, ALGORITHM);
     }
backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOAuth2UserService.java (1)

51-70: providerUid/email 추출에서 NPE/정책 공백(특히 email=null)을 명확히 처리해 주세요.

  • attrs.get("id").toString() / attrs.get("sub")는 provider 응답이 예상과 다르면 NPE가 날 수 있습니다.
  • GitHub/Kakao는 email == null이 현실적으로 자주 발생하는데, 아래 신규 생성 로직에서 email(null)User를 만들지/막을지 정책이 필요합니다(현재는 “DB 제약 있으면 정책 필요” 주석만 존재).

가능하면 “email이 없는 신규 유저 생성”을 명시적으로 차단하고 OAuth 인증 실패로 돌려 UX/운영 이슈를 줄이는 게 안전합니다(또는 GitHub의 경우 별도 이메일 API/스코프 요구).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ec2353d and 49002e7.

📒 Files selected for processing (3)
  • backend/src/main/java/org/sejongisc/backend/auth/dao/UserOauthAccountRepository.java (1 hunks)
  • backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOAuth2UserService.java (3 hunks)
  • backend/src/main/java/org/sejongisc/backend/common/auth/jwt/TokenEncryptor.java (1 hunks)
🔇 Additional comments (3)
backend/src/main/java/org/sejongisc/backend/auth/dao/UserOauthAccountRepository.java (1)

3-14: 메서드 추가는 적절합니다. 다만 (provider, user) 인덱스/유니크 제약은 확인해 주세요.

existsByProviderAndUser(...)는 로그인 핫패스에서 호출될 가능성이 높아서, DB에 (provider, user_id) 인덱스(또는 유니크)가 없으면 성능이 흔들릴 수 있습니다. 또한 중복 링크 방지는 궁극적으로 유니크 제약이 보장해주는지 확인이 필요합니다.

backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOAuth2UserService.java (2)

24-38: delegate/Optional 정리는 깔끔합니다.

가독성 개선 관점에서 변경 방향 좋습니다.


151-165: 반환 attributes에 providerUid/userId를 노출하는 의도가 맞는지 확인해 주세요.

providerUid는 외부 계정 식별자라, 프론트/클라이언트로 내려보내는 것이 요구사항이 아니라면 불필요한 노출이 될 수 있습니다. 또한 DefaultOAuth2User의 name key를 "userId"로 두는 변경은 downstream(Spring Security에서 principal name 사용처)이 "userId" 기준으로 동작하도록 바뀌는 점만 확인하면 좋겠습니다.

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

♻️ Duplicate comments (2)
backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOAuth2UserService.java (2)

98-106: 이전 리뷰에서 지적된 TOCTOU 레이스 조건이 여전히 남아있습니다.

existsByProviderAndUser 확인 후 save 사이에 동시 요청이 들어오면 DB 유니크 제약 위반으로 DataIntegrityViolationException이 발생할 수 있습니다. 이 부분에 예외 처리가 필요합니다.

다음과 같이 수정하세요:

                 boolean alreadyLinked = oauthAccountRepository.existsByProviderAndUser(authProvider, user);
                 if (!alreadyLinked) {
                     UserOauthAccount oauth = UserOauthAccount.builder()
                             .user(user)
                             .provider(authProvider)
                             .providerUid(providerUid)
                             .build();
-                    oauthAccountRepository.save(oauth);
+                    try {
+                        oauthAccountRepository.save(oauth);
+                    } catch (org.springframework.dao.DataIntegrityViolationException ignore) {
+                        // 동시성으로 이미 링크가 생성된 경우 무시
+                    }
                 }

133-140: catch 블록 내부의 OAuth 링크 저장도 동시성 예외가 발생할 수 있습니다.

133-140번 라인의 existsByProviderAndUser + save 패턴도 동일한 TOCTOU 레이스 조건을 가지고 있습니다. 다른 동시 요청이 같은 OAuth 링크를 먼저 생성하면 DataIntegrityViolationException이 발생하는데, 이것이 catch 블록 내부이므로 처리되지 않고 상위로 전파됩니다.

다음과 같이 수정하세요:

                     if (!oauthAccountRepository.existsByProviderAndUser(authProvider, existing)) {
                         UserOauthAccount oauth = UserOauthAccount.builder()
                                 .user(existing)
                                 .provider(authProvider)
                                 .providerUid(providerUid)
                                 .build();
-                        oauthAccountRepository.save(oauth);
+                        try {
+                            oauthAccountRepository.save(oauth);
+                        } catch (org.springframework.dao.DataIntegrityViolationException ignore) {
+                            // 동시성으로 이미 링크가 생성된 경우 무시
+                        }
                     }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 49002e7 and f81e004.

📒 Files selected for processing (1)
  • backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOAuth2UserService.java (2 hunks)
🔇 Additional comments (2)
backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOAuth2UserService.java (2)

23-23: 개선된 코드 구조입니다.

Optional 임포트 추가와 delegate 인스턴스 생성 간소화는 적절하며, 코드 가독성이 향상되었습니다.

Also applies to: 29-29, 37-37


60-62: Null-safe 처리가 적절히 추가되었습니다.

Kakao와 GitHub 제공자의 null 가능성에 대한 방어 코드와 주석이 잘 작성되었습니다.

Also applies to: 66-67

Comment on lines +88 to +92
// 2) 링크가 없으면 email로 기존 로컬/회원가입 유저를 찾는다 (핵심!)
Optional<User> userByEmailOpt = Optional.empty();
if (!email.isBlank()) {
userByEmailOpt = userRepository.findUserByEmail(email);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

중복된 blank 체크입니다.

74-77번 라인에서 이미 email이 null이거나 blank일 경우 가상 이메일로 대체했으므로, 90번 라인의 !email.isBlank() 체크는 항상 true입니다. 이 체크는 불필요하며 로직 흐름을 불명확하게 만듭니다.

다음과 같이 수정하세요:

-            // 2) 링크가 없으면 email로 기존 로컬/회원가입 유저를 찾는다 (핵심!)
-            Optional<User> userByEmailOpt = Optional.empty();
-            if (!email.isBlank()) {
-                userByEmailOpt = userRepository.findUserByEmail(email);
-            }
+            // 2) 링크가 없으면 email로 기존 로컬/회원가입 유저를 찾는다 (핵심!)
+            Optional<User> userByEmailOpt = userRepository.findUserByEmail(email);
📝 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
// 2) 링크가 없으면 email로 기존 로컬/회원가입 유저를 찾는다 (핵심!)
Optional<User> userByEmailOpt = Optional.empty();
if (!email.isBlank()) {
userByEmailOpt = userRepository.findUserByEmail(email);
}
// 2) 링크가 없으면 email로 기존 로컬/회원가입 유저를 찾는다 (핵심!)
Optional<User> userByEmailOpt = userRepository.findUserByEmail(email);
🤖 Prompt for AI Agents
In
backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOAuth2UserService.java
around lines 88 to 92, the check `!email.isBlank()` is redundant because earlier
(lines 74-77) email is already replaced with a non-blank virtual email; remove
the if block and directly call the repository so userByEmailOpt is assigned from
userRepository.findUserByEmail(email) unconditionally, simplifying the code and
clarifying control flow.

Comment on lines 153 to 157
return new DefaultOAuth2User(
List.of(new SimpleGrantedAuthority("ROLE_TEAM_MEMBER")),
attributes,
"userId" // 또는 "email" -> email null 이면 id가 더 안전
"userId"
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

하드코딩된 권한이 사용자의 실제 역할과 불일치합니다.

154번 라인에서 모든 사용자에게 ROLE_TEAM_MEMBER 권한을 부여하고 있지만, 실제 데이터베이스의 사용자는 다른 역할(예: ADMIN)을 가질 수 있습니다. 이는 권한 기반 접근 제어에서 잘못된 권한 검사를 초래할 수 있습니다.

사용자의 실제 역할을 사용하도록 수정하세요:

         return new DefaultOAuth2User(
-                List.of(new SimpleGrantedAuthority("ROLE_TEAM_MEMBER")),
+                List.of(new SimpleGrantedAuthority("ROLE_" + user.getRole().name())),
                 attributes,
                 "userId"
         );
📝 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
return new DefaultOAuth2User(
List.of(new SimpleGrantedAuthority("ROLE_TEAM_MEMBER")),
attributes,
"userId" // 또는 "email" -> email null 이면 id가 더 안전
"userId"
);
return new DefaultOAuth2User(
List.of(new SimpleGrantedAuthority("ROLE_" + user.getRole().name())),
attributes,
"userId"
);
🤖 Prompt for AI Agents
In
backend/src/main/java/org/sejongisc/backend/common/auth/config/CustomOAuth2UserService.java
around lines 153 to 157, the code currently hardcodes a single
SimpleGrantedAuthority("ROLE_TEAM_MEMBER") for every user; replace this by
loading the user's actual roles from your User entity (or user repository) and
mapping them to a List<GrantedAuthority> (e.g., prefixing with "ROLE_" if
needed), then pass that list into the DefaultOAuth2User constructor; ensure you
handle null/empty roles by providing a safe default and convert any role
DTOs/strings into SimpleGrantedAuthority instances before returning the
DefaultOAuth2User.

@Kosw6 Kosw6 merged commit ee2781d into main Dec 12, 2025
1 check 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