Feat/google o auth #9#11
Conversation
|
|
||
| ### Response | ||
|
|
||
| #### 성공 (302 Found) |
There was a problem hiding this comment.
302인데 Json 바디를 적어두신 이유가 있나요?
| ### 개요 | ||
| | 항목 | 내용 | | ||
| | ------ | ----------------------------------------------------------------------------------------------- | | ||
| | **설명** | HttpOnly 쿠키로 전달된 Refresh Token을 검증하고, 새로운 Access Token을 재발급합니다. (Refresh Token 재발급 시 기존 토큰은 폐기) | |
There was a problem hiding this comment.
리프레쉬 토큰을 HttpOnly 쿠키 사용을 하신다고 하셨는데 로그아웃에 리프레쉬토큰을 바디로 받는 이유가 있을까요?
There was a problem hiding this comment.
Pull request overview
This PR implements Google OAuth 2.0 social login functionality for the Hanseiro server. Users authenticate via Google's authorization server, and upon successful authentication, the backend issues custom JWT access and refresh tokens. The refresh token is delivered as an HttpOnly cookie for security.
Key Changes:
- Google OAuth 2.0 integration with authorization code flow
- JWT token generation and validation (access and refresh tokens)
- Spring Security configuration with custom JWT authentication filter
- User and OAuth account entities with refresh token storage
- API endpoints for authorization, callback, token refresh, and logout
Reviewed changes
Copilot reviewed 38 out of 39 changed files in this pull request and generated 28 comments.
Show a summary per file
| File | Description |
|---|---|
build.gradle |
Added JWT dependencies (jjwt 0.11.5) and SonarQube plugin configuration |
gradle/wrapper/gradle-wrapper.properties |
Downgraded Gradle from 9.2.1 to 8.10 |
.github/workflows/sonarcloud.yml |
Added SonarCloud CI workflow for code quality analysis |
application-local.yml |
Added OAuth, JWT, and refresh token configuration properties |
ServerApplication.java |
Enabled @ConfigurationPropertiesScan for configuration binding |
GoogleOAuthService[Impl] |
Implements OAuth authorization URL generation and user info retrieval |
GoogleOAuthProperties |
Configuration properties for Google OAuth credentials |
GoogleTokenResponse / GoogleUserInfo |
DTOs for Google API responses |
GoogleHttpClientConfig |
RestTemplate bean configuration for HTTP calls |
JwtTokenProvider[Impl] |
JWT token creation, validation, and user extraction logic |
JwtProperties |
Configuration properties for JWT secrets and expiration times |
RefreshTokenHasher |
HMAC-SHA256 hashing for refresh tokens before database storage |
RefreshTokenHashProperties |
Configuration for refresh token hashing secret |
AuthService[Impl] |
Core authentication logic: login, refresh, and logout operations |
AuthTokenPair |
DTO containing access and refresh token pair |
UserAuthController |
REST endpoints for OAuth flow and token management |
AccessTokenResponse |
DTO for access token API responses |
CookieUtil |
Utility for creating HttpOnly refresh token cookies |
SecurityConfig |
Spring Security configuration with JWT filter and endpoint permissions |
JwtAuthenticationFilter |
Filter to validate JWT tokens and set security context |
JwtAuthExceptionHandler |
Custom authentication and authorization error handlers |
UserEntity |
User entity with email, department, and name fields |
OAuthAccountEntity |
OAuth account entity linking providers to users |
RefreshTokenEntity |
Refresh token entity with hash storage and expiration |
SocialProvider |
Enum for OAuth providers (currently Google only) |
UserRepository |
JPA repository for user queries |
OAuthAccountRepository |
JPA repository for OAuth account lookups |
RefreshTokenRepository |
JPA repository for refresh token management |
GoogleUserValidator |
Validates Google user info and school email domain |
UserNameParser |
Parses user names in "department/name" format |
SocialLoginException / InvalidSchoolEmailException |
Custom exceptions for OAuth errors |
GlobalExceptionHandler |
Centralized exception handler for auth-related errors |
docs/Documentation_Guide.md |
API documentation convention guide |
docs/Auth_API.md |
Detailed authentication API documentation |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| private final AuthService authService; | ||
|
|
||
| private static final boolean COOKIE_SECURE = false; //http면 false, https면 true | ||
| private static final String COOKIE_SAMESITE = "Lax"; |
There was a problem hiding this comment.
The API documentation specifies SameSite=None for cookies, but the implementation uses SameSite=Lax (line 27 in UserAuthController). This is a discrepancy. SameSite=None is needed for cross-site requests and requires Secure=true (HTTPS), while SameSite=Lax provides some CSRF protection but may not work for cross-origin scenarios. The documentation should match the implementation or vice versa, and the choice should be intentional based on the deployment architecture.
| @Column(nullable = false, length = 100) | ||
| private String providerSubject; | ||
|
|
||
| @OneToOne(fetch = FetchType.LAZY) |
There was a problem hiding this comment.
The relationship between OAuthAccountEntity and UserEntity should be @manytoone instead of @OnetoOne. A single user might want to link multiple OAuth providers in the future (e.g., Google and another provider). The current @OnetoOne mapping creates an artificial constraint that would require schema changes to support multiple OAuth accounts per user.
| @OneToOne(fetch = FetchType.LAZY) | |
| @ManyToOne(fetch = FetchType.LAZY) |
| } catch (Exception e) { | ||
| throw new SocialLoginException("GOOGLE_USERINFO_REQUEST_FAILED", "구글 사용자 정보 조회 중 오류가 발생했습니다."); | ||
| } |
There was a problem hiding this comment.
The exception handling in the catch block swallows the original exception details. When catching a generic Exception, the original exception should be logged or passed as a cause to maintain the stack trace and help with debugging. This makes troubleshooting OAuth failures more difficult in production.
| @PostConstruct | ||
| public void init() { | ||
| this.key = Keys.hmacShaKeyFor(props.getSecret().getBytes(StandardCharsets.UTF_8)); | ||
| } |
There was a problem hiding this comment.
The JWT secret key validation is missing. The Keys.hmacShaKeyFor() method requires a key of at least 256 bits (32 bytes) for HS256. If the configured secret is shorter, the application will fail at runtime with a WeakKeyException. Consider adding validation in the @PostConstruct method to check the secret length and throw a clear exception at startup if it's too short.
There was a problem hiding this comment.
@PostConstruct에서 secret을 byte 배열로 변환 후, 길이가 32바이트 보다 짧은 경우 예외 발생. 조건 통과 시에만 key 생성으로 수정
| @GetMapping("/google/callback") | ||
| public ResponseEntity<AccessTokenResponse> googleCallback( | ||
| @RequestParam("code") String code, | ||
| @RequestParam(value = "state", required = false) String state | ||
| ) { | ||
| return okWithRefreshCookie(authService.loginWithGoogleCode(code)); | ||
| } |
There was a problem hiding this comment.
The API documentation describes a response format that includes user details (userId, name, email), tokenType, expiresIn, isProfileCompleted, and createdAt fields, but the actual controller implementation (UserAuthController.googleCallback) only returns an AccessTokenResponse with the accessToken field. This is a significant discrepancy between the documented API and the actual implementation.
There was a problem hiding this comment.
현재 스펙(302 redirect + Set-Cookie)에 맞춰 문서 수정
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| @AllArgsConstructor(access = AccessLevel.PRIVATE) | ||
| @Builder | ||
|
|
There was a problem hiding this comment.
Default toString(): UserEntity inherits toString() from Object, and so is not suitable for printing.
| @ToString(of = {"id", "tokenHash", "expiresAt"}) |
| @Getter | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| @AllArgsConstructor(access = AccessLevel.PRIVATE) | ||
| @Builder |
There was a problem hiding this comment.
Default toString(): UserEntity inherits toString() from Object, and so is not suitable for printing.
| return new AuthTokenPair(newAccessToken, newRefreshToken); | ||
| } | ||
|
|
||
| @Transactional |
There was a problem hiding this comment.
This method overrides AuthService.logout; it is advisable to add an Override annotation.
| @Transactional | |
| @Transactional | |
| @Override |
|
|
||
| return new AuthTokenPair(accessToken, refreshToken); | ||
| } | ||
|
|
There was a problem hiding this comment.
This method overrides AuthService.refresh; it is advisable to add an Override annotation.
| @Override |
| private final JwtTokenProvider jwtTokenProvider; | ||
| private final RefreshTokenHasher refreshTokenHasher; | ||
|
|
||
| // 인가코드 기반 로그인(구글콜백처리) |
There was a problem hiding this comment.
This method overrides AuthService.loginWithGoogleCode; it is advisable to add an Override annotation.
| // 인가코드 기반 로그인(구글콜백처리) | |
| // 인가코드 기반 로그인(구글콜백처리) | |
| @Override |
|
코파일럿이 리뷰를 다 해주었는데, 제가 깜빡하고 한글로 해달라고 설정을 안해놨습니다. |
Ⅰ. PR 내용 설명 (Describe what this PR did)
Google 인가코드 기반 로그인 API 구현.(POST /api/v1/auth/google)
프론트에서 전달한 인가코드(code) 를 이용해 구글 토큰 교환을 수행하고, 구글 사용자 정보를 조회한 뒤 서버 JWT를 발급하도록 구현했습니다. 로그인 성공 시 응답으로 Access Token / Refresh Token, 내부 사용자 정보(id, email, name, isNew)를 제공합니다.
Refresh 토큰 재발급 API 구현(POST /api/v1/auth/refresh)
만료된 Access Token을 대체하기 위해 Refresh Token 기반 Access Token 재발급을 구현했습니다. Refresh Token은 요청 Authorization 헤더(Bearer) 로 전달받는 방식으로 통일했습니다. Refresh Token 누락/만료/위조 시 401(AUTH_INVALID_REFRESH_TOKEN) 로 응답하도록 예외 흐름을 정리했습니다.
로그아웃 처리 API 구현(POST /api/v1/auth/logout)
앱 특성상 “자동 로그인 유지”를 위해 Refresh Token을 사용하되, 로그아웃 시에는 서버 측에서 해당 Refresh Token을 폐기(무효화) 하여 이후 재발급이 불가능하도록 처리했습니다. 정상 처리 시 204 No Content 로 응답하여 “상태 변경만 수행”하는 로그아웃 API 의미를 명확히 했습니다.
Ⅱ. 관련 이슈 (Does this pull request fix one issue?)
fixes #9Ⅲ. 검증 방법 (Describe how to verify it)
준비 사항
서버 실행 후, Google 로그인 성공 시, 주소창: http://127.0.0.1:8080/?code=...&scope=...&... 에서 code(인가코드)를 확인
로그인(구글 code → JWT 발급)
엔드포인트 :
POST/api/v1/auth/googleauthorizationCode(구글 인가 코드)로 구글에 토큰 교환 후 유저 정보 조회/생성
Access Token(응답 바디)와 Refresh Token(응답 헤더: X-Refresh-Token) 발급.
리프레시(Refresh Token → 새 Access Token 발급)
엔드포인트 :
POST/api/v1/auth/refreshaccessToken이 만료되었거나 재발급이 필요할 때, X-Refresh-Token을 서버에 보내면 서버는 DB에 저장된 refresh token 상태(만료/폐기/해시 일치 등)를 검사하고 정상이라면 새 accessToken을 반환
로그아웃(Refresh Token 폐기)
엔드포인트 :
POST/api/v1/auth/logoutrefresh token을 DB에서 revoked=true 같은 방식으로 폐기하고, 이후 같은 refreshToken으로 refresh를 못 하게 막음
Ⅳ. 리뷰 시 참고 사항 (Special notes for reviews)