From 8baa330544b9b5e863b3ddb35e2064d912927683 Mon Sep 17 00:00:00 2001 From: jjoonleo Date: Mon, 11 May 2026 01:07:38 +0900 Subject: [PATCH 1/3] Handle invalid Google identity tokens --- .../oauth/google/GoogleLoginFilter.java | 11 ++++++++ .../oauth/OAuthLoginFilterValidationTest.java | 26 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/ontime-back/src/main/java/devkor/ontime_back/global/oauth/google/GoogleLoginFilter.java b/ontime-back/src/main/java/devkor/ontime_back/global/oauth/google/GoogleLoginFilter.java index 1a23274..e133085 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/global/oauth/google/GoogleLoginFilter.java +++ b/ontime-back/src/main/java/devkor/ontime_back/global/oauth/google/GoogleLoginFilter.java @@ -16,6 +16,7 @@ import jakarta.validation.Validator; import lombok.extern.slf4j.Slf4j; import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContextHolder; @@ -57,7 +58,14 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ try { GoogleIdToken.Payload googlePayload = googleLoginService.verifyIdentityToken(oAuthGoogleRequestDto.getIdToken()); + if (googlePayload == null) { + throw new BadCredentialsException("Invalid Google identity token"); + } + String googleUserId = googlePayload.getSubject(); + if (googleUserId == null || googleUserId.isBlank()) { + throw new BadCredentialsException("Google identity token has no subject"); + } Object loginLock = LOGIN_LOCKS.computeIfAbsent(googleUserId, key -> new Object()); synchronized (loginLock) { @@ -84,6 +92,9 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ } } + } catch (AuthenticationException e) { + log.warn("Google login failed: {}", e.getMessage()); + throw e; } catch (Exception e) { log.error("Google login failed: {}", e.getClass().getSimpleName()); throw new AuthenticationException("Google 로그인 실패") {}; diff --git a/ontime-back/src/test/java/devkor/ontime_back/global/oauth/OAuthLoginFilterValidationTest.java b/ontime-back/src/test/java/devkor/ontime_back/global/oauth/OAuthLoginFilterValidationTest.java index b9fbf81..d188185 100644 --- a/ontime-back/src/test/java/devkor/ontime_back/global/oauth/OAuthLoginFilterValidationTest.java +++ b/ontime-back/src/test/java/devkor/ontime_back/global/oauth/OAuthLoginFilterValidationTest.java @@ -18,11 +18,14 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.core.AuthenticationException; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class OAuthLoginFilterValidationTest { @@ -65,6 +68,29 @@ void googleLoginFilterRejectsInvalidRequest() throws Exception { verifyNoInteractions(googleLoginService, userRepository); } + @Test + @DisplayName("구글 로그인 필터가 검증 실패한 idToken을 인증 실패로 처리한다") + void googleLoginFilterRejectsUnverifiedIdToken() throws Exception { + GoogleLoginFilter filter = new GoogleLoginFilter( + "/oauth2/google/login", + objectMapper, + validator, + googleLoginService, + userRepository); + MockHttpServletResponse response = new MockHttpServletResponse(); + + when(googleLoginService.verifyIdentityToken("invalid-token")).thenReturn(null); + + assertThatThrownBy(() -> filter.attemptAuthentication( + request("/oauth2/google/login", "{\"idToken\":\"invalid-token\"}"), + response)) + .isInstanceOf(BadCredentialsException.class) + .hasMessage("Invalid Google identity token"); + + verify(googleLoginService).verifyIdentityToken("invalid-token"); + verifyNoInteractions(userRepository); + } + @Test @DisplayName("카카오 로그인 필터가 잘못된 요청을 400 validation 응답으로 처리한다") void kakaoLoginFilterRejectsInvalidRequest() throws Exception { From 757bdd08f29a02a073d5946527e153e4373ae654 Mon Sep 17 00:00:00 2001 From: jjoonleo Date: Mon, 11 May 2026 01:26:12 +0900 Subject: [PATCH 2/3] Support comma-separated Google client IDs --- ontime-back/docs/deployment/ec2.md | 2 +- .../global/oauth/google/GoogleLoginService.java | 9 ++++++++- .../src/main/resources/application-prod.properties | 4 ++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/ontime-back/docs/deployment/ec2.md b/ontime-back/docs/deployment/ec2.md index 2c713bc..b9d3598 100644 --- a/ontime-back/docs/deployment/ec2.md +++ b/ontime-back/docs/deployment/ec2.md @@ -30,7 +30,7 @@ The workflow builds a Docker image, pushes it to GHCR, uploads `docker-compose.y - `JWT_ACCESS_HEADER` - `JWT_REFRESH_HEADER` - `GOOGLE_WEB_CLIENT_ID` -- `GOOGLE_APP_CLIENT_ID` +- `GOOGLE_APP_CLIENT_ID` (comma-separated iOS and Android client IDs when both platforms are enabled) - `APPLE_CLIENT_ID` - `APPLE_LOGIN_KEY` - `APPLE_TEAM_ID` diff --git a/ontime-back/src/main/java/devkor/ontime_back/global/oauth/google/GoogleLoginService.java b/ontime-back/src/main/java/devkor/ontime_back/global/oauth/google/GoogleLoginService.java index 3b64d38..ad5b723 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/global/oauth/google/GoogleLoginService.java +++ b/ontime-back/src/main/java/devkor/ontime_back/global/oauth/google/GoogleLoginService.java @@ -35,6 +35,7 @@ import java.util.List; import java.util.Optional; import java.util.UUID; +import java.util.stream.Stream; @Slf4j @Service @@ -58,7 +59,13 @@ public GoogleLoginService( this.jwtTokenProvider = jwtTokenProvider; this.userRepository = userRepository; this.userAlarmSettingRepository = userAlarmSettingRepository; - this.validClientIds = List.of(webClientId, appClientId); + this.validClientIds = Stream.concat( + Stream.of(webClientId), + Stream.of(appClientId.split(",")) + ) + .map(String::trim) + .filter(clientId -> !clientId.isBlank()) + .toList(); } diff --git a/ontime-back/src/main/resources/application-prod.properties b/ontime-back/src/main/resources/application-prod.properties index cd67d8b..47b5032 100644 --- a/ontime-back/src/main/resources/application-prod.properties +++ b/ontime-back/src/main/resources/application-prod.properties @@ -19,6 +19,10 @@ spring.flyway.baseline-on-migrate=false logging.level.root=INFO logging.level.devkor.ontime_back=INFO +# Google OAuth +google.web.client-id=${GOOGLE_WEB_CLIENT_ID} +google.app.client-id=${GOOGLE_APP_CLIENT_ID} + # Actuator management.endpoint.health.probes.enabled=true management.endpoints.web.exposure.include=health From c02abf8159e7160c79655f0bd21cc0e97525c493 Mon Sep 17 00:00:00 2001 From: jjoonleo Date: Mon, 11 May 2026 01:58:32 +0900 Subject: [PATCH 3/3] Use shared Google client secret names in dev deploy --- .github/workflows/deploy-dev.yml | 4 +- docs/deployment.md | 8 ++-- .../oauth/google/GoogleLoginService.java | 45 +++++++++++++++++++ .../main/resources/application-dev.properties | 4 +- 4 files changed, 53 insertions(+), 8 deletions(-) diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml index 836aaab..068b407 100644 --- a/.github/workflows/deploy-dev.yml +++ b/.github/workflows/deploy-dev.yml @@ -126,8 +126,8 @@ jobs: JWT_ACCESS_HEADER=${{ secrets.DEV_JWT_ACCESS_HEADER || 'Authorization' }} JWT_REFRESH_HEADER=${{ secrets.DEV_JWT_REFRESH_HEADER || 'Authorization-refresh' }} - GOOGLE_WEB_CLIENT_ID=${{ secrets.DEV_GOOGLE_WEB_CLIENT_ID || 'dev-google-web-client-id' }} - GOOGLE_APP_CLIENT_ID=${{ secrets.DEV_GOOGLE_APP_CLIENT_ID || 'dev-google-app-client-id' }} + GOOGLE_WEB_CLIENT_ID=${{ secrets.GOOGLE_WEB_CLIENT_ID || 'dev-google-web-client-id' }} + GOOGLE_APP_CLIENT_ID=${{ secrets.GOOGLE_APP_CLIENT_ID || 'dev-google-app-client-id' }} APPLE_CLIENT_ID=${{ secrets.DEV_APPLE_CLIENT_ID || 'dev-apple-client-id' }} APPLE_TEAM_ID=${{ secrets.DEV_APPLE_TEAM_ID || 'dev-apple-team-id' }} diff --git a/docs/deployment.md b/docs/deployment.md index d31528a..78b3b9b 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -143,8 +143,8 @@ Optional development secrets: - `DEV_JWT_REFRESH_EXPIRATION` - `DEV_JWT_ACCESS_HEADER` - `DEV_JWT_REFRESH_HEADER` -- `DEV_GOOGLE_WEB_CLIENT_ID` -- `DEV_GOOGLE_APP_CLIENT_ID` +- `GOOGLE_WEB_CLIENT_ID` +- `GOOGLE_APP_CLIENT_ID` - `DEV_APPLE_CLIENT_ID` - `DEV_APPLE_TEAM_ID` - `DEV_APPLE_LOGIN_KEY` @@ -212,8 +212,8 @@ Optional development secrets: - `DEV_JWT_REFRESH_EXPIRATION` - `DEV_JWT_ACCESS_HEADER` - `DEV_JWT_REFRESH_HEADER` -- `DEV_GOOGLE_WEB_CLIENT_ID` -- `DEV_GOOGLE_APP_CLIENT_ID` +- `GOOGLE_WEB_CLIENT_ID` +- `GOOGLE_APP_CLIENT_ID` - `DEV_APPLE_CLIENT_ID` - `DEV_APPLE_TEAM_ID` - `DEV_APPLE_LOGIN_KEY` diff --git a/ontime-back/src/main/java/devkor/ontime_back/global/oauth/google/GoogleLoginService.java b/ontime-back/src/main/java/devkor/ontime_back/global/oauth/google/GoogleLoginService.java index ad5b723..512960c 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/global/oauth/google/GoogleLoginService.java +++ b/ontime-back/src/main/java/devkor/ontime_back/global/oauth/google/GoogleLoginService.java @@ -66,6 +66,9 @@ public GoogleLoginService( .map(String::trim) .filter(clientId -> !clientId.isBlank()) .toList(); + log.info("Configured Google OAuth audiences: {}", validClientIds.stream() + .map(this::maskClientId) + .toList()); } @@ -173,10 +176,52 @@ public GoogleIdToken.Payload verifyIdentityToken(String identityToken) throws Ex return payload; } else { log.info("Google identity credential is invalid"); + logGoogleIdentityTokenClaims(identityToken); return null; } } + private void logGoogleIdentityTokenClaims(String identityToken) { + try { + GoogleIdToken parsedToken = GoogleIdToken.parse( + GsonFactory.getDefaultInstance(), + identityToken + ); + GoogleIdToken.Payload payload = parsedToken.getPayload(); + String audience = String.valueOf(payload.getAudience()); + Long expirationTimeSeconds = payload.getExpirationTimeSeconds(); + long nowSeconds = System.currentTimeMillis() / 1000; + log.info( + "Google identity token claims aud={}, azp={}, iss={}, exp={}, now={}, secondsUntilExp={}, audienceAllowed={}", + maskClientId(audience), + payload.get("azp"), + payload.getIssuer(), + expirationTimeSeconds, + nowSeconds, + expirationTimeSeconds == null ? null : expirationTimeSeconds - nowSeconds, + validClientIds.contains(audience) + ); + } catch (Exception e) { + log.info("Google identity token claim parsing failed: {}", e.getClass().getSimpleName()); + } + } + + private String maskClientId(String clientId) { + int separatorIndex = clientId.indexOf('-'); + if (separatorIndex < 0) { + return ""; + } + String projectNumber = clientId.substring(0, separatorIndex); + String suffix = ".apps.googleusercontent.com"; + boolean hasGoogleSuffix = clientId.endsWith(suffix); + String middle = clientId.substring( + separatorIndex + 1, + hasGoogleSuffix ? clientId.length() - suffix.length() : clientId.length() + ); + String visibleTail = middle.length() <= 4 ? middle : middle.substring(middle.length() - 4); + return projectNumber + "-..." + visibleTail + (hasGoogleSuffix ? suffix : ""); + } + public boolean revokeToken(Long userId) { User user = userRepository.findById(userId) .orElseThrow(() -> new IllegalArgumentException("User not found with id: " + userId)); diff --git a/ontime-back/src/main/resources/application-dev.properties b/ontime-back/src/main/resources/application-dev.properties index 953a8bd..7a552c6 100644 --- a/ontime-back/src/main/resources/application-dev.properties +++ b/ontime-back/src/main/resources/application-dev.properties @@ -23,8 +23,8 @@ jwt.access.header=${JWT_ACCESS_HEADER:Authorization} jwt.refresh.header=${JWT_REFRESH_HEADER:Authorization-refresh} # Google OAuth -google.web.client-id=${GOOGLE_WEB_CLIENT_ID:dev-google-web-client-id} -google.app.client-id=${GOOGLE_APP_CLIENT_ID:dev-google-app-client-id} +google.web.client-id=${GOOGLE_WEB_CLIENT_ID:456571312261-5kuf2r6i5i7lqjr7qealv06sdgkn3hcp.apps.googleusercontent.com} +google.app.client-id=${GOOGLE_APP_CLIENT_ID:456571312261-r35ah9qi0qaq7al007e2db0e0jmjcmb4.apps.googleusercontent.com,456571312261-5e99nruk62f21uoh7stfp8i82acmh6iq.apps.googleusercontent.com,456571312261-6470v6goejjkcqn3608b4nbbtpt6dknu.apps.googleusercontent.com} # Apple OAuth apple.client.id=${APPLE_CLIENT_ID:dev-apple-client-id}