Skip to content

Commit 210c66d

Browse files
authored
Merge pull request #310 from DevKor-github/dev
Dev
2 parents 3be6fb1 + c02abf8 commit 210c66d

8 files changed

Lines changed: 103 additions & 10 deletions

File tree

.github/workflows/deploy-dev.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,8 @@ jobs:
126126
JWT_ACCESS_HEADER=${{ secrets.DEV_JWT_ACCESS_HEADER || 'Authorization' }}
127127
JWT_REFRESH_HEADER=${{ secrets.DEV_JWT_REFRESH_HEADER || 'Authorization-refresh' }}
128128
129-
GOOGLE_WEB_CLIENT_ID=${{ secrets.DEV_GOOGLE_WEB_CLIENT_ID || 'dev-google-web-client-id' }}
130-
GOOGLE_APP_CLIENT_ID=${{ secrets.DEV_GOOGLE_APP_CLIENT_ID || 'dev-google-app-client-id' }}
129+
GOOGLE_WEB_CLIENT_ID=${{ secrets.GOOGLE_WEB_CLIENT_ID || 'dev-google-web-client-id' }}
130+
GOOGLE_APP_CLIENT_ID=${{ secrets.GOOGLE_APP_CLIENT_ID || 'dev-google-app-client-id' }}
131131
132132
APPLE_CLIENT_ID=${{ secrets.DEV_APPLE_CLIENT_ID || 'dev-apple-client-id' }}
133133
APPLE_TEAM_ID=${{ secrets.DEV_APPLE_TEAM_ID || 'dev-apple-team-id' }}

docs/deployment.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -143,8 +143,8 @@ Optional development secrets:
143143
- `DEV_JWT_REFRESH_EXPIRATION`
144144
- `DEV_JWT_ACCESS_HEADER`
145145
- `DEV_JWT_REFRESH_HEADER`
146-
- `DEV_GOOGLE_WEB_CLIENT_ID`
147-
- `DEV_GOOGLE_APP_CLIENT_ID`
146+
- `GOOGLE_WEB_CLIENT_ID`
147+
- `GOOGLE_APP_CLIENT_ID`
148148
- `DEV_APPLE_CLIENT_ID`
149149
- `DEV_APPLE_TEAM_ID`
150150
- `DEV_APPLE_LOGIN_KEY`
@@ -212,8 +212,8 @@ Optional development secrets:
212212
- `DEV_JWT_REFRESH_EXPIRATION`
213213
- `DEV_JWT_ACCESS_HEADER`
214214
- `DEV_JWT_REFRESH_HEADER`
215-
- `DEV_GOOGLE_WEB_CLIENT_ID`
216-
- `DEV_GOOGLE_APP_CLIENT_ID`
215+
- `GOOGLE_WEB_CLIENT_ID`
216+
- `GOOGLE_APP_CLIENT_ID`
217217
- `DEV_APPLE_CLIENT_ID`
218218
- `DEV_APPLE_TEAM_ID`
219219
- `DEV_APPLE_LOGIN_KEY`

ontime-back/docs/deployment/ec2.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ The workflow builds a Docker image, pushes it to GHCR, uploads `docker-compose.y
3030
- `JWT_ACCESS_HEADER`
3131
- `JWT_REFRESH_HEADER`
3232
- `GOOGLE_WEB_CLIENT_ID`
33-
- `GOOGLE_APP_CLIENT_ID`
33+
- `GOOGLE_APP_CLIENT_ID` (comma-separated iOS and Android client IDs when both platforms are enabled)
3434
- `APPLE_CLIENT_ID`
3535
- `APPLE_LOGIN_KEY`
3636
- `APPLE_TEAM_ID`

ontime-back/src/main/java/devkor/ontime_back/global/oauth/google/GoogleLoginFilter.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import jakarta.validation.Validator;
1717
import lombok.extern.slf4j.Slf4j;
1818
import org.springframework.dao.DataIntegrityViolationException;
19+
import org.springframework.security.authentication.BadCredentialsException;
1920
import org.springframework.security.core.Authentication;
2021
import org.springframework.security.core.AuthenticationException;
2122
import org.springframework.security.core.context.SecurityContextHolder;
@@ -57,7 +58,14 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ
5758

5859
try {
5960
GoogleIdToken.Payload googlePayload = googleLoginService.verifyIdentityToken(oAuthGoogleRequestDto.getIdToken());
61+
if (googlePayload == null) {
62+
throw new BadCredentialsException("Invalid Google identity token");
63+
}
64+
6065
String googleUserId = googlePayload.getSubject();
66+
if (googleUserId == null || googleUserId.isBlank()) {
67+
throw new BadCredentialsException("Google identity token has no subject");
68+
}
6169

6270
Object loginLock = LOGIN_LOCKS.computeIfAbsent(googleUserId, key -> new Object());
6371
synchronized (loginLock) {
@@ -84,6 +92,9 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ
8492
}
8593
}
8694

95+
} catch (AuthenticationException e) {
96+
log.warn("Google login failed: {}", e.getMessage());
97+
throw e;
8798
} catch (Exception e) {
8899
log.error("Google login failed: {}", e.getClass().getSimpleName());
89100
throw new AuthenticationException("Google 로그인 실패") {};

ontime-back/src/main/java/devkor/ontime_back/global/oauth/google/GoogleLoginService.java

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import java.util.List;
3636
import java.util.Optional;
3737
import java.util.UUID;
38+
import java.util.stream.Stream;
3839

3940
@Slf4j
4041
@Service
@@ -58,7 +59,16 @@ public GoogleLoginService(
5859
this.jwtTokenProvider = jwtTokenProvider;
5960
this.userRepository = userRepository;
6061
this.userAlarmSettingRepository = userAlarmSettingRepository;
61-
this.validClientIds = List.of(webClientId, appClientId);
62+
this.validClientIds = Stream.concat(
63+
Stream.of(webClientId),
64+
Stream.of(appClientId.split(","))
65+
)
66+
.map(String::trim)
67+
.filter(clientId -> !clientId.isBlank())
68+
.toList();
69+
log.info("Configured Google OAuth audiences: {}", validClientIds.stream()
70+
.map(this::maskClientId)
71+
.toList());
6272
}
6373

6474

@@ -166,10 +176,52 @@ public GoogleIdToken.Payload verifyIdentityToken(String identityToken) throws Ex
166176
return payload;
167177
} else {
168178
log.info("Google identity credential is invalid");
179+
logGoogleIdentityTokenClaims(identityToken);
169180
return null;
170181
}
171182
}
172183

184+
private void logGoogleIdentityTokenClaims(String identityToken) {
185+
try {
186+
GoogleIdToken parsedToken = GoogleIdToken.parse(
187+
GsonFactory.getDefaultInstance(),
188+
identityToken
189+
);
190+
GoogleIdToken.Payload payload = parsedToken.getPayload();
191+
String audience = String.valueOf(payload.getAudience());
192+
Long expirationTimeSeconds = payload.getExpirationTimeSeconds();
193+
long nowSeconds = System.currentTimeMillis() / 1000;
194+
log.info(
195+
"Google identity token claims aud={}, azp={}, iss={}, exp={}, now={}, secondsUntilExp={}, audienceAllowed={}",
196+
maskClientId(audience),
197+
payload.get("azp"),
198+
payload.getIssuer(),
199+
expirationTimeSeconds,
200+
nowSeconds,
201+
expirationTimeSeconds == null ? null : expirationTimeSeconds - nowSeconds,
202+
validClientIds.contains(audience)
203+
);
204+
} catch (Exception e) {
205+
log.info("Google identity token claim parsing failed: {}", e.getClass().getSimpleName());
206+
}
207+
}
208+
209+
private String maskClientId(String clientId) {
210+
int separatorIndex = clientId.indexOf('-');
211+
if (separatorIndex < 0) {
212+
return "<invalid-client-id>";
213+
}
214+
String projectNumber = clientId.substring(0, separatorIndex);
215+
String suffix = ".apps.googleusercontent.com";
216+
boolean hasGoogleSuffix = clientId.endsWith(suffix);
217+
String middle = clientId.substring(
218+
separatorIndex + 1,
219+
hasGoogleSuffix ? clientId.length() - suffix.length() : clientId.length()
220+
);
221+
String visibleTail = middle.length() <= 4 ? middle : middle.substring(middle.length() - 4);
222+
return projectNumber + "-..." + visibleTail + (hasGoogleSuffix ? suffix : "");
223+
}
224+
173225
public boolean revokeToken(Long userId) {
174226
User user = userRepository.findById(userId)
175227
.orElseThrow(() -> new IllegalArgumentException("User not found with id: " + userId));

ontime-back/src/main/resources/application-dev.properties

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ jwt.access.header=${JWT_ACCESS_HEADER:Authorization}
2323
jwt.refresh.header=${JWT_REFRESH_HEADER:Authorization-refresh}
2424

2525
# Google OAuth
26-
google.web.client-id=${GOOGLE_WEB_CLIENT_ID:dev-google-web-client-id}
27-
google.app.client-id=${GOOGLE_APP_CLIENT_ID:dev-google-app-client-id}
26+
google.web.client-id=${GOOGLE_WEB_CLIENT_ID:456571312261-5kuf2r6i5i7lqjr7qealv06sdgkn3hcp.apps.googleusercontent.com}
27+
google.app.client-id=${GOOGLE_APP_CLIENT_ID:456571312261-r35ah9qi0qaq7al007e2db0e0jmjcmb4.apps.googleusercontent.com,456571312261-5e99nruk62f21uoh7stfp8i82acmh6iq.apps.googleusercontent.com,456571312261-6470v6goejjkcqn3608b4nbbtpt6dknu.apps.googleusercontent.com}
2828

2929
# Apple OAuth
3030
apple.client.id=${APPLE_CLIENT_ID:dev-apple-client-id}

ontime-back/src/main/resources/application-prod.properties

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ spring.flyway.baseline-on-migrate=false
1919
logging.level.root=INFO
2020
logging.level.devkor.ontime_back=INFO
2121

22+
# Google OAuth
23+
google.web.client-id=${GOOGLE_WEB_CLIENT_ID}
24+
google.app.client-id=${GOOGLE_APP_CLIENT_ID}
25+
2226
# Actuator
2327
management.endpoint.health.probes.enabled=true
2428
management.endpoints.web.exposure.include=health

ontime-back/src/test/java/devkor/ontime_back/global/oauth/OAuthLoginFilterValidationTest.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,14 @@
1818
import org.mockito.junit.jupiter.MockitoExtension;
1919
import org.springframework.mock.web.MockHttpServletRequest;
2020
import org.springframework.mock.web.MockHttpServletResponse;
21+
import org.springframework.security.authentication.BadCredentialsException;
2122
import org.springframework.security.core.AuthenticationException;
2223

2324
import static org.assertj.core.api.Assertions.assertThat;
2425
import static org.assertj.core.api.Assertions.assertThatThrownBy;
26+
import static org.mockito.Mockito.verify;
2527
import static org.mockito.Mockito.verifyNoInteractions;
28+
import static org.mockito.Mockito.when;
2629

2730
@ExtendWith(MockitoExtension.class)
2831
class OAuthLoginFilterValidationTest {
@@ -65,6 +68,29 @@ void googleLoginFilterRejectsInvalidRequest() throws Exception {
6568
verifyNoInteractions(googleLoginService, userRepository);
6669
}
6770

71+
@Test
72+
@DisplayName("구글 로그인 필터가 검증 실패한 idToken을 인증 실패로 처리한다")
73+
void googleLoginFilterRejectsUnverifiedIdToken() throws Exception {
74+
GoogleLoginFilter filter = new GoogleLoginFilter(
75+
"/oauth2/google/login",
76+
objectMapper,
77+
validator,
78+
googleLoginService,
79+
userRepository);
80+
MockHttpServletResponse response = new MockHttpServletResponse();
81+
82+
when(googleLoginService.verifyIdentityToken("invalid-token")).thenReturn(null);
83+
84+
assertThatThrownBy(() -> filter.attemptAuthentication(
85+
request("/oauth2/google/login", "{\"idToken\":\"invalid-token\"}"),
86+
response))
87+
.isInstanceOf(BadCredentialsException.class)
88+
.hasMessage("Invalid Google identity token");
89+
90+
verify(googleLoginService).verifyIdentityToken("invalid-token");
91+
verifyNoInteractions(userRepository);
92+
}
93+
6894
@Test
6995
@DisplayName("카카오 로그인 필터가 잘못된 요청을 400 validation 응답으로 처리한다")
7096
void kakaoLoginFilterRejectsInvalidRequest() throws Exception {

0 commit comments

Comments
 (0)