feat: 구글 로그인 기능 구현 - #18
Conversation
- Spring Security + jjwt 의존성 추가, JWT 필터/엔트리포인트 구성 - RefreshToken 엔티티와 리포지토리 추가 (만료 시각 함께 저장) - Users에 onboardingCompleted 추가 및 소셜 가입 정적 팩토리 추가 - 구글 OAuth/JWT/쿠키 설정을 환경변수 기반으로 분리
GET /api/v1/auth/login/google - CSRF 방어용 state를 생성해 HttpOnly 쿠키로 저장 - redirectTo는 화이트리스트에 있는 경로만 허용해 오픈 리다이렉트 방지 - 구글 인증 페이지로 302 리다이렉트
GET /api/v1/auth/callback/google - state 쿠키와 대조해 CSRF 검증, 불일치 시 INVALID_STATE로 에러 페이지 이동 - 인가 코드를 토큰으로 교환하고 구글 프로필 조회 - 미가입 시 유저 생성, 같은 이메일의 기존 계정이 있으면 구글 계정 연동 - 리프레시 토큰을 DB에 저장하고 HttpOnly 쿠키로 발급 후 프론트 콜백 페이지로 리다이렉트
POST /api/v1/auth/refresh - 쿠키의 리프레시 토큰을 DB 저장분과 대조하고 서명/만료 검증 - 무효하거나 만료된 경우 도메인 코드 AUTH401 응답 - 회전(rotation) 도입 지점에 TODO 표기
POST /api/v1/auth/logout - DB에 저장된 리프레시 토큰 삭제 - Max-Age=0 으로 리프레시 토큰 쿠키 만료 처리 - 액세스 토큰 인증이 필요한 엔드포인트로 구성
- 새로 추가된 리포지토리를 MockitoBean으로 대체 - app.* 설정 바인딩을 위한 테스트 프로퍼티 추가
기존에는 DataSource/JPA 오토컨피그를 제외한 채 @EnableJpaAuditing이 동작해 'JPA metamodel must not be empty'로 contextLoads가 실패하고 있었다. - 테스트 런타임에 H2 추가, 제외 설정을 걷어내고 실제 JPA로 기동 - ddl-auto=create-drop으로 엔티티 매핑까지 스모크 테스트가 검증하도록 변경 - 리포지토리 MockitoBean 제거
로컬 http 환경에서 브라우저가 Secure 쿠키를 저장하지 않아 콜백 이후 흐름을 확인할 수 없어, COOKIE_SECURE로 낮출 수 있게 한다. 기본값은 true.
구글에서 콜백으로 돌아오는 요청은 크로스 사이트 이동이라 SameSite=Strict인 state 쿠키가 브라우저에서 전송되지 않아 정상 로그인도 INVALID_STATE로 실패하고 있었다. - state 쿠키만 SameSite=Lax로 분리 (최상위 GET 이동에는 전송되어 CSRF 방어는 유지) - 리프레시 토큰 쿠키는 명세대로 Strict 유지
인증 스킴이 없어 Authorize 버튼이 노출되지 않아 토큰이 필요한 API를 Swagger UI에서 테스트할 수 없었다.
- callback/google은 구글만 호출하므로 @hidden으로 문서에서 제외 - login/google은 브라우저 이동용임을 설명에 명시 - 인증이 필요 없는 login/google, refresh는 @SecurityRequirements로 표시
- UserRepository: develop의 조회 메서드와 소셜 로그인용 메서드를 통합 - application.yml: youtube 설정과 app(인증) 설정을 함께 유지 - 테스트 설정에 youtube 프로퍼티 추가, .env.example에 YOUTUBE_API_KEY 명시
📝 WalkthroughWalkthroughGoogle OAuth 로그인과 콜백 처리를 추가하고, JWT 기반 요청 인증 및 리프레시 토큰 재발급·로그아웃 API를 구현했습니다. 관련 설정 프로퍼티, 쿠키 처리, 사용자 소셜 계정 연동, 토큰 저장용 데이터베이스 구조와 테스트 환경도 추가되었습니다. ChangesGoogle 인증 기능
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant AuthController
participant AuthService
participant GoogleOAuthClient
participant RefreshTokenRepository
Client->>AuthController: Google 로그인 요청
AuthController->>AuthService: 로그인 진입 정보 생성
AuthService->>GoogleOAuthClient: Google 인가 URI 생성
AuthController-->>Client: state 쿠키 설정 및 Google 리다이렉트
Client->>AuthController: OAuth callback 전달
AuthController->>AuthService: callback 처리
AuthService->>GoogleOAuthClient: 인증 코드 교환 및 사용자 조회
AuthService->>RefreshTokenRepository: 리프레시 토큰 저장
AuthController-->>Client: 프론트엔드 리다이렉트 및 토큰 쿠키 설정
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (1)
.env.example (1)
20-21: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win로컬 HTTP 예시와
COOKIE_SECURE값이 충돌합니다.기본 redirect/backend/frontend 주소가 모두
http://localhost인데COOKIE_SECURE=true입니다. 예시를 그대로 사용하면 브라우저가 OAuth state 및 refresh 쿠키를 콜백·refresh 요청에 포함하지 않을 수 있습니다. 로컬 예시는false로 두고 운영 환경에서만true로 덮어쓰는 구성이 안전합니다.제안
-COOKIE_SECURE=true +# 로컬 HTTP 테스트용. 운영에서는 HTTPS와 true를 사용 +COOKIE_SECURE=false🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.env.example around lines 20 - 21, .env.example의 로컬 HTTP 설정과 COOKIE_SECURE 기본값이 일치하도록 COOKIE_SECURE를 false로 변경하고, 운영 환경에서는 별도 환경 설정으로 true를 지정하도록 유지하세요.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/main/java/com/slatto/domain/auth/client/GoogleOAuthClient.java`:
- Around line 19-21: Update the GoogleOAuthClient constructor to configure
connection and response/read timeouts on the RestClient.Builder before build(),
matching the timeout behavior used by YoutubeApiClient or the project’s shared
builder defaults. Apply these settings to the restClient used for Google token
and user-information requests.
In `@src/main/java/com/slatto/domain/auth/entity/RefreshToken.java`:
- Around line 30-31: Update the RefreshToken token persistence flow around the
token field to store only an HMAC digest derived with a server-side secret,
never the raw bearer token. Apply the same deterministic digesting to incoming
cookie tokens before refresh-token lookup and deletion, while keeping token
issuance and client-facing cookie values unchanged.
In `@src/main/java/com/slatto/domain/auth/service/AuthService.java`:
- Around line 127-131: Update issueRefreshToken to atomically replace the user’s
refresh token: enforce a unique user_id constraint for refreshTokenRepository’s
entity, and use a transaction-safe lock or atomic upsert covering deletion and
saving. Ensure concurrent login requests cannot leave multiple tokens valid
through findByToken, preserving only the latest token.
- Around line 70-75: Update the Google authentication validation around userInfo
and findOrCreateUser to handle a missing userInfo.name() before persistence;
either generate a valid default nickname or return AUTH_FAILED explicitly,
ensuring findOrCreateUser never receives a null name for the non-null nickname
field.
- Around line 77-79: Update handleGoogleCallback() to validate the
cookie-derived storedState.redirectPath() through the existing
resolveRedirectPath() whitelist logic before passing it to
frontendProperties.toAbsoluteUrl(). Preserve the validated redirect path in the
GoogleCallbackResult.
In `@src/main/java/com/slatto/domain/user/repository/UserRepository.java`:
- Around line 15-17: Update UserRepository methods findBySocialTypeAndSocialId
and findByEmail to include a DeletedAtIsNull condition, then update the
corresponding AuthService calls, including findOrCreateUser, to use the revised
repository method signatures so soft-deleted users are excluded from
authentication lookups.
In `@src/main/java/com/slatto/global/config/SecurityConfig.java`:
- Around line 30-37: SecurityConfig의 SecurityFilterChain에 CORS 설정을 추가하고
CorsConfigurationSource를 연결하세요. FRONTEND_BASE_URL을 허용 origin으로 사용하고 필요한 HTTP
메서드와 요청 헤더를 명시하며 credentials 허용을 활성화해 preflight 및 인증 포함 refresh 요청이 통과하도록 구성하세요.
In `@src/main/resources/db/migration/001-auth-google-login.sql`:
- Around line 1-19: Connect the migration containing onboarding_completed and
refresh_token to the deployment path so it is applied automatically before the
application starts. Configure and enable the project’s migration mechanism, or
add an atomic deployment step that executes this SQL for new and existing
environments; do not leave it as an unreferenced resource while ddl-auto remains
validate.
---
Nitpick comments:
In @.env.example:
- Around line 20-21: .env.example의 로컬 HTTP 설정과 COOKIE_SECURE 기본값이 일치하도록
COOKIE_SECURE를 false로 변경하고, 운영 환경에서는 별도 환경 설정으로 true를 지정하도록 유지하세요.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: c8b7318f-bf1d-4fca-a900-95f9a8e86ce4
📒 Files selected for processing (31)
.env.example.gitignorebuild.gradlesrc/main/java/com/slatto/SlattoApplication.javasrc/main/java/com/slatto/domain/auth/client/GoogleOAuthClient.javasrc/main/java/com/slatto/domain/auth/client/dto/GoogleTokenResponse.javasrc/main/java/com/slatto/domain/auth/client/dto/GoogleUserInfo.javasrc/main/java/com/slatto/domain/auth/controller/AuthController.javasrc/main/java/com/slatto/domain/auth/dto/AccessTokenResponse.javasrc/main/java/com/slatto/domain/auth/entity/RefreshToken.javasrc/main/java/com/slatto/domain/auth/exception/AuthErrorCode.javasrc/main/java/com/slatto/domain/auth/repository/RefreshTokenRepository.javasrc/main/java/com/slatto/domain/auth/service/AuthService.javasrc/main/java/com/slatto/domain/auth/support/AuthCookieFactory.javasrc/main/java/com/slatto/domain/auth/support/GoogleAuthFailureReason.javasrc/main/java/com/slatto/domain/auth/support/OAuthState.javasrc/main/java/com/slatto/domain/user/entity/Users.javasrc/main/java/com/slatto/domain/user/repository/UserRepository.javasrc/main/java/com/slatto/global/config/SecurityConfig.javasrc/main/java/com/slatto/global/config/SwaggerConfig.javasrc/main/java/com/slatto/global/config/properties/CookieProperties.javasrc/main/java/com/slatto/global/config/properties/FrontendProperties.javasrc/main/java/com/slatto/global/config/properties/GoogleOAuthProperties.javasrc/main/java/com/slatto/global/config/properties/JwtProperties.javasrc/main/java/com/slatto/global/security/JwtAuthenticationEntryPoint.javasrc/main/java/com/slatto/global/security/JwtAuthenticationFilter.javasrc/main/java/com/slatto/global/security/JwtTokenProvider.javasrc/main/resources/application.ymlsrc/main/resources/db/migration/001-auth-google-login.sqlsrc/test/java/com/slatto/SlattoApplicationTests.javasrc/test/resources/application.yml
| public GoogleOAuthClient(GoogleOAuthProperties googleOAuthProperties, RestClient.Builder restClientBuilder) { | ||
| this.googleOAuthProperties = googleOAuthProperties; | ||
| this.restClient = restClientBuilder.build(); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
rg -n -C3 -g '*.java' -g '*.kt' \
'RestClient\.builder|RestClient\.Builder|requestFactory|connectTimeout|readTimeout|setConnectTimeout|setReadTimeout' \
src/main/java
rg -n -C3 -g '*.yml' -g '*.yaml' -g '*.properties' \
'timeout|restclient|http' \
src/main/resourcesRepository: SLAT-TO/SLATE-TO-BE
Length of output: 4652
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
echo "== RestClient.Builder / RestClient customization =="
rg -n -C3 -g '*.java' -g '*.kt' \
'RestClient\.Builder|RestClientCustomizer|RestClient.Builder|requestFactory|setConnectTimeout|setReadTimeout|connect-timeout|read-timeout|timeout' \
src/main/java src/test/java
echo
echo "== Bean definitions likely affecting HTTP clients =="
rg -n -C3 -g '*.java' -g '*.kt' \
'`@Bean`|`@Configuration`|ClientHttpRequestFactory|SimpleClientHttpRequestFactory|HttpComponentsClientHttpRequestFactory|OkHttp3ClientHttpRequestFactory' \
src/main/java
echo
echo "== All auth-related config =="
sed -n '1,120p' src/main/resources/application.ymlRepository: SLAT-TO/SLATE-TO-BE
Length of output: 12208
Google OAuth RestClient에 타임아웃을 추가하세요.
GoogleOAuthClient는 RestClient.Builder를 바로 build()만 하고 있어, Google 토큰/사용자정보 호출에 연결·응답 제한 시간이 없습니다. YoutubeApiClient처럼 이 클라이언트에도 connect/read timeout을 넣거나 공통 builder에 기본 타임아웃을 설정하세요.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/main/java/com/slatto/domain/auth/client/GoogleOAuthClient.java` around
lines 19 - 21, Update the GoogleOAuthClient constructor to configure connection
and response/read timeouts on the RestClient.Builder before build(), matching
the timeout behavior used by YoutubeApiClient or the project’s shared builder
defaults. Apply these settings to the restClient used for Google token and
user-information requests.
| @Column(name = "token", nullable = false, length = 512, unique = true) | ||
| private String token; |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
리프레시 토큰을 평문으로 저장하지 마세요.
Line 30은 재사용 가능한 bearer 토큰을 그대로 저장합니다. DB 읽기 권한 유출만으로 세션 탈취가 가능하므로, 서버 비밀키 기반 HMAC 다이제스트를 저장하고 조회·삭제 시에도 쿠키 토큰을 동일하게 다이제스트하세요.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/main/java/com/slatto/domain/auth/entity/RefreshToken.java` around lines
30 - 31, Update the RefreshToken token persistence flow around the token field
to store only an HMAC digest derived with a server-side secret, never the raw
bearer token. Apply the same deterministic digesting to incoming cookie tokens
before refresh-token lookup and deletion, while keeping token issuance and
client-facing cookie values unchanged.
| if (userInfo == null || userInfo.email() == null || !userInfo.isEmailVerified()) { | ||
| return failure(GoogleAuthFailureReason.AUTH_FAILED); | ||
| } | ||
|
|
||
| Users user = findOrCreateUser(userInfo); | ||
| String refreshToken = issueRefreshToken(user); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
ast-grep outline src/main/java/com/slatto/domain/auth/client/GoogleOAuthClient.java --items all
rg -n -C4 '(scope|profile|name)' \
src/main/java/com/slatto/domain/auth/client/GoogleOAuthClient.java \
src/main/java/com/slatto/global/config/properties/GoogleOAuthProperties.javaRepository: SLAT-TO/SLATE-TO-BE
Length of output: 2567
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
ast-grep outline src/main/java/com/slatto/domain/auth/service/AuthService.java --items all
ast-grep outline src/main/java/com/slatto/domain/auth/client/dto/GoogleUserInfo.java --items all
ast-grep outline src/main/java/com/slatto/domain/user/domain/Users.java --items all
sed -n '1,220p' src/main/java/com/slatto/domain/auth/service/AuthService.java
printf '\n--- GOOGLE USER INFO ---\n'
sed -n '1,220p' src/main/java/com/slatto/domain/auth/client/dto/GoogleUserInfo.java
printf '\n--- USERS ---\n'
sed -n '1,220p' src/main/java/com/slatto/domain/user/domain/Users.java
printf '\n--- SCOPE REFERENCES ---\n'
rg -n -C3 'googleOAuthProperties\.scope\(|scope:' src/main/resources src/main/javaRepository: SLAT-TO/SLATE-TO-BE
Length of output: 7462
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
fd -a 'Users.java' src/main/java
fd -a 'application*.y*ml' src/main/resources
printf '\n--- USERS OUTLINE ---\n'
ast-grep outline src/main/java/com/slatto/domain/user/entity/Users.java --items all
printf '\n--- USERS FILE ---\n'
sed -n '1,260p' src/main/java/com/slatto/domain/user/entity/Users.java
printf '\n--- GOOGLE OAUTH PROPERTIES / SCOPE ---\n'
rg -n -C4 'scope|google' src/main/resources src/main/java/com/slatto/global/config/propertiesRepository: SLAT-TO/SLATE-TO-BE
Length of output: 5735
🌐 Web query:
Google OAuth userinfo endpoint name claim profile scope optional returned when name is missing documentation
💡 Result:
When using the Google OAuth 2.0 / OpenID Connect userinfo endpoint, the name, given_name, and family_name claims are not guaranteed to be present in the response [1][2][3]. Even when the profile scope is requested, these fields may be omitted for several reasons [1][4][5]: 1. User/Organization Settings: Users or their organizations may choose to withhold specific profile fields [1][4]. 2. Data Availability: The information simply may not be set or available in the user's Google account profile [4][5]. 3. Scope Limitations: While the profile scope is required to signal a request for this information, it does not act as a guarantee that the data will be provided [1][2]. Official Google documentation explicitly notes that the name claim "is never guaranteed to be present" [1]. Because these fields are optional, applications should implement defensive programming when handling the userinfo response [6][7]. If your application strictly requires user profile information that is not available via these claims, Google recommends using the Google People API (specifically the people.get method) as a more reliable alternative for retrieving comprehensive profile data [4][5]. Top results: [1][4][6][3]
Citations:
- 1: https://developers.google.com/identity/openid-connect/openid-connect
- 2: https://developers.google.cn/identity/openid-connect/reference
- 3: https://developers.google.com/identity/openid-connect/reference
- 4: https://stackoverflow.com/questions/79836393/google-oauth-2-0-when-are-given-name-family-name-omitted-from-the-id-token-sco
- 5: https://stackoverflow.com/questions/68774479/google-oauth-with-profile-in-scope-missing-name
- 6: Schema$Userinfo interface returns optional values googleapis/google-api-nodejs-client#3151
- 7: https://stackoverflow.com/questions/15073386/does-googles-userinfo-api-offer-any-guarantees
Google name 누락을 처리하세요. 이미 profile scope를 요청해도 name은 보장되지 않습니다. 현재 findOrCreateUser()가 userInfo.name()을 nickname(nullable=false)에 그대로 넣어, 일부 계정에서는 콜백이 DB 예외로 실패합니다. 기본 닉네임을 생성하거나, name이 없을 때 명시적으로 인증 실패로 처리하세요.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/main/java/com/slatto/domain/auth/service/AuthService.java` around lines
70 - 75, Update the Google authentication validation around userInfo and
findOrCreateUser to handle a missing userInfo.name() before persistence; either
generate a valid default nickname or return AUTH_FAILED explicitly, ensuring
findOrCreateUser never receives a null name for the non-null nickname field.
| return new GoogleCallbackResult( | ||
| frontendProperties.toAbsoluteUrl(storedState.redirectPath()), | ||
| refreshToken, |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
ast-grep outline src/main/java/com/slatto/global/config/properties/FrontendProperties.java --items all
rg -n -A20 -B5 'resolveRedirectPath|toAbsoluteUrl' \
src/main/java/com/slatto/global/config/properties/FrontendProperties.javaRepository: SLAT-TO/SLATE-TO-BE
Length of output: 727
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
ast-grep outline src/main/java/com/slatto/domain/auth/service/AuthService.java --items all
ast-grep outline src/main/java/com/slatto/domain/auth/model --items all
rg -n -A30 -B10 'stateCookieValue|redirectPath|resolveRedirectPath|toAbsoluteUrl|GoogleCallbackResult|StoredState|State' \
src/main/java/com/slatto/domain/authRepository: SLAT-TO/SLATE-TO-BE
Length of output: 29043
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
wc -l src/main/java/com/slatto/domain/auth/service/AuthService.java
cat -n src/main/java/com/slatto/domain/auth/service/AuthService.java | sed -n '1,180p'Repository: SLAT-TO/SLATE-TO-BE
Length of output: 6548
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
rg -n -A20 -B20 'stateCookieValue|setState|createState|parseState|State' src/main/java/com/slatto/domain/auth/service/AuthService.javaRepository: SLAT-TO/SLATE-TO-BE
Length of output: 5087
콜백에서도 redirectPath를 다시 화이트리스트로 거르세요.
handleGoogleCallback()는 쿠키에서 꺼낸 storedState.redirectPath()를 그대로 toAbsoluteUrl()에 넣습니다. 초기 로그인 때만 resolveRedirectPath()를 적용하면, 쿠키를 바꾼 경우 허용되지 않은 프론트엔드 경로로 우회할 수 있습니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/main/java/com/slatto/domain/auth/service/AuthService.java` around lines
77 - 79, Update handleGoogleCallback() to validate the cookie-derived
storedState.redirectPath() through the existing resolveRedirectPath() whitelist
logic before passing it to frontendProperties.toAbsoluteUrl(). Preserve the
validated redirect path in the GoogleCallbackResult.
| private String issueRefreshToken(Users user) { | ||
| refreshTokenRepository.deleteByUser(user); | ||
|
|
||
| String token = jwtTokenProvider.createRefreshToken(user.getId()); | ||
| refreshTokenRepository.save(RefreshToken.issue(user, token, jwtTokenProvider.refreshTokenExpiresAt())); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
리프레시 토큰 교체를 원자화하세요.
Line 128의 삭제와 Line 131의 저장 사이에 동시 로그인 요청이 끼면 둘 이상의 토큰이 남을 수 있고, 이후 모두 findByToken으로 유효하게 처리됩니다. user_id 유니크 제약과 잠금/원자적 upsert를 도입해 “최신 토큰만 유효” 계약을 보장하세요.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/main/java/com/slatto/domain/auth/service/AuthService.java` around lines
127 - 131, Update issueRefreshToken to atomically replace the user’s refresh
token: enforce a unique user_id constraint for refreshTokenRepository’s entity,
and use a transaction-safe lock or atomic upsert covering deletion and saving.
Ensure concurrent login requests cannot leave multiple tokens valid through
findByToken, preserving only the latest token.
| Optional<Users> findBySocialTypeAndSocialId(SocialType socialType, String socialId); | ||
|
|
||
| Optional<Users> findByEmail(String email); |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
소프트 삭제된 사용자를 인증 조회에서 제외하세요.
findBySocialTypeAndSocialId와 findByEmail에는 DeletedAtIsNull 조건이 없습니다. 콜백의 findOrCreateUser가 이 결과를 우선 사용하므로, 삭제된 계정에도 소셜 계정이 다시 연결되고 리프레시 토큰이 발급될 수 있습니다. 두 조회를 삭제 제외 조건으로 변경하고 AuthService 호출도 함께 갱신하세요.
수정 예시
- Optional<Users> findBySocialTypeAndSocialId(SocialType socialType, String socialId);
- Optional<Users> findByEmail(String email);
+ Optional<Users> findBySocialTypeAndSocialIdAndDeletedAtIsNull(SocialType socialType, String socialId);
+ Optional<Users> findByEmailAndDeletedAtIsNull(String email);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/main/java/com/slatto/domain/user/repository/UserRepository.java` around
lines 15 - 17, Update UserRepository methods findBySocialTypeAndSocialId and
findByEmail to include a DeletedAtIsNull condition, then update the
corresponding AuthService calls, including findOrCreateUser, to use the revised
repository method signatures so soft-deleted users are excluded from
authentication lookups.
| .authorizeHttpRequests(auth -> auth | ||
| .requestMatchers( | ||
| "/api/v1/auth/login/**", | ||
| "/api/v1/auth/callback/**", | ||
| "/api/v1/auth/refresh" | ||
| ).permitAll() | ||
| .requestMatchers(HttpMethod.GET, "/api/v1/health", "/swagger-ui/**", "/v3/api-docs/**").permitAll() | ||
| .anyRequest().authenticated() |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -eu
# 전역 또는 MVC CORS 설정과 Spring Security CORS 활성화 여부를 확인합니다.
rg -nP -C 3 '(\.cors\s*\(|CorsConfigurationSource|CorsFilter|`@CrossOrigin`)' \
src/main/java
# 프런트엔드 origin 설정과 프록시 관련 구성을 확인합니다.
rg -n -C 3 'FRONTEND_BASE_URL|base-url|proxy' \
src/main/resources src/test/resourcesRepository: SLAT-TO/SLATE-TO-BE
Length of output: 157
🏁 Script executed:
#!/bin/bash
set -eu
printf '\n== SecurityConfig ==\n'
sed -n '1,220p' src/main/java/com/slatto/global/config/SecurityConfig.java
printf '\n== CORS-related symbols ==\n'
rg -n -C 2 'CorsConfigurationSource|CorsFilter|`@CrossOrigin`|\.cors\s*\(' src/main/java || true
printf '\n== Frontend/proxy config symbols ==\n'
rg -n -C 2 'FRONTEND_BASE_URL|base-url|proxy|cors|allowed-origins|allowedOriginPatterns' src/main/resources src/test/resources src/main/java || true
printf '\n== File list around config ==\n'
git ls-files 'src/main/java/com/slatto/global/config/*'Repository: SLAT-TO/SLATE-TO-BE
Length of output: 3118
🏁 Script executed:
#!/bin/bash
set -eu
printf '\n== Repository tree (top levels) ==\n'
find . -maxdepth 2 -type f | sed 's#^\./##' | sort | head -n 200
printf '\n== SecurityConfig around the relevant section ==\n'
nl -ba src/main/java/com/slatto/global/config/SecurityConfig.java | sed -n '1,220p'
printf '\n== Any CORS configuration anywhere in repo ==\n'
rg -n -C 2 'CorsConfigurationSource|CorsFilter|`@CrossOrigin`|\.cors\s*\(' . || true
printf '\n== Any frontend origin / proxy config anywhere in repo ==\n'
rg -n -C 2 'FRONTEND_BASE_URL|base-url|proxy|allowed-origins|allowedOriginPatterns|credentials' . || trueRepository: SLAT-TO/SLATE-TO-BE
Length of output: 1452
🏁 Script executed:
#!/bin/bash
set -eu
printf '\n== Frontend-related files ==\n'
git ls-files | rg '(^|/)(package.json|vite\.config|next\.config|nuxt\.config|webpack\.config|angular\.json|nginx|docker-compose|compose|Dockerfile|\.env|application.*\.ya?ml|application.*\.properties|README|docs)' || true
printf '\n== Files mentioning frontend or API base URLs ==\n'
rg -n -C 2 '(frontend|front-end|base url|base-url|api url|api-url|origin|proxy)' . || trueRepository: SLAT-TO/SLATE-TO-BE
Length of output: 4261
SecurityConfig에 CORS 설정을 추가하세요.
FRONTEND_BASE_URL가 분리된 구성인데 이 SecurityFilterChain에는 http.cors()/CorsConfigurationSource가 없습니다. 브라우저에서 preflight나 credentials 포함 /api/v1/auth/refresh 요청이 막힐 수 있으니 허용 origin, 메서드, 헤더, credentials를 명시하세요.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/main/java/com/slatto/global/config/SecurityConfig.java` around lines 30 -
37, SecurityConfig의 SecurityFilterChain에 CORS 설정을 추가하고 CorsConfigurationSource를
연결하세요. FRONTEND_BASE_URL을 허용 origin으로 사용하고 필요한 HTTP 메서드와 요청 헤더를 명시하며 credentials
허용을 활성화해 preflight 및 인증 포함 refresh 요청이 통과하도록 구성하세요.
| -- ddl-auto=validate 이므로 애플리케이션 기동 전에 직접 적용해야 한다. | ||
|
|
||
| ALTER TABLE users | ||
| ADD COLUMN onboarding_completed BIT(1) NOT NULL DEFAULT b'0'; | ||
|
|
||
| CREATE TABLE refresh_token | ||
| ( | ||
| id BIGINT NOT NULL AUTO_INCREMENT, | ||
| user_id BIGINT NOT NULL, | ||
| token VARCHAR(512) NOT NULL, | ||
| expires_at DATETIME(6) NOT NULL, | ||
| created_at DATETIME(6) NOT NULL, | ||
| updated_at DATETIME(6) NULL, | ||
| PRIMARY KEY (id), | ||
| UNIQUE KEY uk_refresh_token_token (token), | ||
| KEY idx_refresh_token_user_id (user_id), | ||
| CONSTRAINT fk_refresh_token_user FOREIGN KEY (user_id) REFERENCES users (id) | ||
| ) ENGINE = InnoDB | ||
| DEFAULT CHARSET = utf8mb4; |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
fd -HI -t f 'build\.gradle(\.kts)?|application\.(yml|yaml|properties)' . \
-x rg -n -C3 'flyway|liquibase|ddl-auto|sql\.init' {}
fd -HI -t f . src/main/resources/db/migrationRepository: SLAT-TO/SLATE-TO-BE
Length of output: 526
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
printf '\n## migration files\n'
fd -HI -t f . src/main/resources/db
printf '\n## application configs with migration / ddl settings\n'
for f in $(git ls-files 'src/main/resources/*application*' 'src/main/resources/**/*.yml' 'src/main/resources/**/*.yaml' 'src/main/resources/**/*.properties' 2>/dev/null); do
echo "### $f"
rg -n -C2 'flyway|liquibase|ddl-auto|sql\.init|schema|data' "$f" || true
done
printf '\n## build files with migration deps\n'
for f in $(git ls-files 'build.gradle' 'build.gradle.kts' 'settings.gradle' 'settings.gradle.kts' 2>/dev/null); do
echo "### $f"
rg -n -C2 'flyway|liquibase|hibernate|jpa|mysql|h2' "$f" || true
doneRepository: SLAT-TO/SLATE-TO-BE
Length of output: 1513
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
echo '## files under src/main/resources/db'
git ls-files 'src/main/resources/db/**' | sort
echo
echo '## exact application files'
git ls-files 'src/main/resources/**application*' | sort
echo
echo '## relevant snippets from application.yml'
f='src/main/resources/application.yml'
if [ -f "$f" ]; then
sed -n '1,120p' "$f"
fiRepository: SLAT-TO/SLATE-TO-BE
Length of output: 2020
배포 경로에 마이그레이션을 연결하세요.
ddl-auto=validate만 사용하고 Flyway/Liquibase 설정도 없어서, 이 src/main/resources/db/migration/001-auth-google-login.sql은 자동 실행되지 않습니다. 신규 환경에서는 users.onboarding_completed/refresh_token 누락으로 인증이 실패할 수 있으니, 마이그레이션 도구를 붙이거나 배포 단계에서 원자적으로 적용되게 해야 합니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/main/resources/db/migration/001-auth-google-login.sql` around lines 1 -
19, Connect the migration containing onboarding_completed and refresh_token to
the deployment path so it is applied automatically before the application
starts. Configure and enable the project’s migration mechanism, or add an atomic
deployment step that executes this SQL for new and existing environments; do not
leave it as an unreferenced resource while ddl-auto remains validate.
🔗 관련 이슈 (Related Issue)
Closes #14
📝 작업 내용
구글 OAuth 로그인 기능 구현
✅ PR 체크리스트
Summary by CodeRabbit
새 기능
보안