Skip to content

Feat/google o auth #9#11

Merged
kim-tea-nam merged 5 commits into
devfrom
feat/googleOAuth-#9
Jan 19, 2026
Merged

Feat/google o auth #9#11
kim-tea-nam merged 5 commits into
devfrom
feat/googleOAuth-#9

Conversation

@UjinVV

@UjinVV UjinVV commented Dec 29, 2025

Copy link
Copy Markdown

Ⅰ. 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(인가코드)를 확인

로그인 후 브라우저 응답 상태코드는 404 : 로그인 성공이후 띄워줄 프론트 주소가 아직 없음

로그인(구글 code → JWT 발급)

엔드포인트 : POST /api/v1/auth/google
authorizationCode(구글 인가 코드)로 구글에 토큰 교환 후 유저 정보 조회/생성
Access Token(응답 바디)와 Refresh Token(응답 헤더: X-Refresh-Token) 발급.

refresh 토큰을 X-Refresh-Token으로 분리한 이유 : Access 토큰(Authorization)과 역할을 명확히 구분하고, 재발급 요청을 의도적인 행위로 제한하기 위해

리프레시(Refresh Token → 새 Access Token 발급)

엔드포인트 : POST /api/v1/auth/refresh
accessToken이 만료되었거나 재발급이 필요할 때, X-Refresh-Token을 서버에 보내면 서버는 DB에 저장된 refresh token 상태(만료/폐기/해시 일치 등)를 검사하고 정상이라면 새 accessToken을 반환

로그아웃(Refresh Token 폐기)

엔드포인트 : POST /api/v1/auth/logout
refresh token을 DB에서 revoked=true 같은 방식으로 폐기하고, 이후 같은 refreshToken으로 refresh를 못 하게 막음

Ⅳ. 리뷰 시 참고 사항 (Special notes for reviews)

jihun4452 and others added 3 commits December 15, 2025 00:30
* feat: sonarcloud 추가설정 및 yml추가 #2

* chore: sonarcloud.yml 워크플로우 브랜치 이름 변경 #2

* chore: sonarcloud 버전이 gradle 8버전과 맞지않아 변경 #2

* chore: Gradle 8.10으로 다운그레이드 #2
@UjinVV UjinVV self-assigned this Dec 29, 2025
@UjinVV UjinVV added feat 기능 추가 docs 문서 수정 labels Dec 29, 2025
@UjinVV UjinVV linked an issue Dec 29, 2025 that may be closed by this pull request
@jihun4452 jihun4452 requested a review from Copilot December 30, 2025 10:37
Comment thread docs/Auth_API.md Outdated

### Response

#### 성공 (302 Found)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

302인데 Json 바디를 적어두신 이유가 있나요?

Comment thread docs/Auth_API.md Outdated
### 개요
| 항목 | 내용 |
| ------ | ----------------------------------------------------------------------------------------------- |
| **설명** | HttpOnly 쿠키로 전달된 Refresh Token을 검증하고, 새로운 Access Token을 재발급합니다. (Refresh Token 재발급 시 기존 토큰은 폐기) |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

리프레쉬 토큰을 HttpOnly 쿠키 사용을 하신다고 하셨는데 로그아웃에 리프레쉬토큰을 바디로 받는 이유가 있을까요?

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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";

Copilot AI Dec 30, 2025

Copy link

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
@Column(nullable = false, length = 100)
private String providerSubject;

@OneToOne(fetch = FetchType.LAZY)

Copilot AI Dec 30, 2025

Copy link

Choose a reason for hiding this comment

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

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.

Suggested change
@OneToOne(fetch = FetchType.LAZY)
@ManyToOne(fetch = FetchType.LAZY)

Copilot uses AI. Check for mistakes.
Comment on lines +98 to +100
} catch (Exception e) {
throw new SocialLoginException("GOOGLE_USERINFO_REQUEST_FAILED", "구글 사용자 정보 조회 중 오류가 발생했습니다.");
}

Copilot AI Dec 30, 2025

Copy link

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

원래 예외를 로그로 남기고, cause로 전달

Comment on lines +20 to +23
@PostConstruct
public void init() {
this.key = Keys.hmacShaKeyFor(props.getSecret().getBytes(StandardCharsets.UTF_8));
}

Copilot AI Dec 30, 2025

Copy link

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@PostConstruct에서 secret을 byte 배열로 변환 후, 길이가 32바이트 보다 짧은 경우 예외 발생. 조건 통과 시에만 key 생성으로 수정

Comment on lines +37 to +43
@GetMapping("/google/callback")
public ResponseEntity<AccessTokenResponse> googleCallback(
@RequestParam("code") String code,
@RequestParam(value = "state", required = false) String state
) {
return okWithRefreshCookie(authService.loginWithGoogleCode(code));
}

Copilot AI Dec 30, 2025

Copy link

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

현재 스펙(302 redirect + Set-Cookie)에 맞춰 문서 수정

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Builder

Copilot AI Dec 30, 2025

Copy link

Choose a reason for hiding this comment

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

Default toString(): UserEntity inherits toString() from Object, and so is not suitable for printing.

Suggested change
@ToString(of = {"id", "tokenHash", "expiresAt"})

Copilot uses AI. Check for mistakes.
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Builder

Copilot AI Dec 30, 2025

Copy link

Choose a reason for hiding this comment

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

Default toString(): UserEntity inherits toString() from Object, and so is not suitable for printing.

Copilot uses AI. Check for mistakes.
return new AuthTokenPair(newAccessToken, newRefreshToken);
}

@Transactional

Copilot AI Dec 30, 2025

Copy link

Choose a reason for hiding this comment

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

This method overrides AuthService.logout; it is advisable to add an Override annotation.

Suggested change
@Transactional
@Transactional
@Override

Copilot uses AI. Check for mistakes.

return new AuthTokenPair(accessToken, refreshToken);
}

Copilot AI Dec 30, 2025

Copy link

Choose a reason for hiding this comment

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

This method overrides AuthService.refresh; it is advisable to add an Override annotation.

Suggested change
@Override

Copilot uses AI. Check for mistakes.
private final JwtTokenProvider jwtTokenProvider;
private final RefreshTokenHasher refreshTokenHasher;

// 인가코드 기반 로그인(구글콜백처리)

Copilot AI Dec 30, 2025

Copy link

Choose a reason for hiding this comment

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

This method overrides AuthService.loginWithGoogleCode; it is advisable to add an Override annotation.

Suggested change
// 인가코드 기반 로그인(구글콜백처리)
// 인가코드 기반 로그인(구글콜백처리)
@Override

Copilot uses AI. Check for mistakes.
@jihun4452

Copy link
Copy Markdown
Contributor

코파일럿이 리뷰를 다 해주었는데, 제가 깜빡하고 한글로 해달라고 설정을 안해놨습니다.
다음에 해놓을게요! 그리고 코드가 생각보다 길어서 읽기가 쉽지가 않습니다.
PR설명이 조금 더 상세했으면 좋겠어요 어떻게 구현했고 어떻게 흘러가는지에 대해서 좀더 상세하게 변경 해주시면 좋을 거 같습니다!
docs랑 소셜로그인 PR이 같이 한번에 올라왔는데 다음에는 구분을 해서 따로 올려주심 더 좋지않을까싶어요!
고생하셨어요😀

@kim-tea-nam kim-tea-nam merged commit dcfc119 into dev Jan 19, 2026
1 check failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs 문서 수정 feat 기능 추가

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: 구글 소셜 로그인 기능 구현

4 participants