
구글 소셜 로그인을 구현하던 중 아래와 같은 에러가 발생했다.
ERROR 1167065 --- [emotion-storage] [io-8080-exec-10] c.g.a.c.a.openidconnect.IdTokenVerifier : id token signature verification failed.
Please see docs for IdTokenVerifier for default settings and configuration options
로그 메시지 상으로는 구글 ID 토큰의 서명 검증이 실패한 것처럼 보여서 처음에는 아래와 같은 문제들을 의심했다.
- Google OAuth Client ID 설정 오류
- ID 토큰 만료
- JWT 서명 검증 실패
- GoogleIdTokenVerifier 설정 문제
특히 구글 로그인에서 ID 토큰 검증이 중요한 과정이기 때문에 OAuth 설정이나 JWT 검증 로직을 먼저 의심했었다. 하지만 디버깅을 계속할수록 예상과는 다른 방향으로 흘러갔다.
문제는 OAuth도 아니었고, JWT도 아니었다.
바로 콤마 하나였다.
여러 플랫폼을 지원하기 위한 Client ID 설정
MOOI 프로젝트에서는 안드로이드와 iOS에서 모두 구글 로그인이 포함되었다. 따라서 서버에서는 여러 Client ID를 허용하는 구조를 사용하고 있었다.
Spring Boot 설정에서는 다음과 같이 Google Client ID를 관리하고 있었다.
spring:
security:
oauth2:
google:
client-ids: ${GOOGLE_CLIENT_IDS}
실제 환경변수는 다음과 같이 Client ID를 콤마로 구분해서 저장하고 있었다.
GOOGLE_CLIENT_IDS=id1.apps.googleusercontent.com,id2.apps.googleusercontent.com
문제 없이 구글 로그인이 성공할 것이라고 생각했다.
정상 토큰인데 왜 검증이 실패했을까?
위에서 얘기했듯이 에러 로그가 id token signature verification failed 였기 때문에 혹시 토큰 자체에 문제가 있는 건 아닐까 생각했다.
그래서 jwt.io에서 ID 토큰을 직접 디코딩해서 payload를 확인했다.
그 결과 aud, iss, exp 모두 정상임을 확인할 수 있었다. 즉, 토큰 자체는 정상적으로 발급되고 있었기 때문에 토큰 문제는 아니었다.
범인은 OAuth도 JWT도 아니었다
서버에서는 구글 ID 토큰을 검증하기 위해 GoogleTokenVerifier을 사용하고 있었다.
여러 Client ID를 허용하기 위해 다음과 같이 설정 값을 List<String> 형태로 주입받는 방식으로 구현하고 있었다.
@Component
@RequiredArgsConstructor
@Slf4j
public class GoogleTokenVerifier {
@Value("${spring.security.oauth2.google.client-ids}")
private List<String> googleClientIds;
public Payload verifyToken(String idToken) {
try {
GoogleIdTokenVerifier verifier = new GoogleIdTokenVerifier.Builder(
new NetHttpTransport(),
GsonFactory.getDefaultInstance())
.setAudience(googleClientIds)
.build();
GoogleIdToken googleIdToken = verifier.verify(idToken);
if (googleIdToken == null) {
throw new BaseException(ErrorCode.INVALID_ID_TOKEN);
}
return googleIdToken.getPayload();
} catch (IOException | GeneralSecurityException e) {
throw new BaseException(ErrorCode.INVALID_ID_TOKEN);
}
}
}
처음에는 이 방식으로 환경변수에 설정한 여러 Client ID가 자동으로 리스트로 바인딩될 것이라고 생각했다. 그런데 @Value로 주입받는 과정에서 기대한 형태와 실제 전달 형태가 달라서 검증 실패로 이어졌다.
다른 블로그 글들을 참고하면서 콤마 구분 환경변수가 성공하는 것들을 본 것 같아서 콤마 구분 환경변수로 작성했으나 현재 프로젝트의 설정/주입 방식에서 GoogleIdTokenVerifier가 기대하는 audience 컬렉션 형태와 불일치해서 오류가 발생했다. 즉, 문제의 핵심은 OAuth 토큰 자체가 아니라 설정 값을 verifier에 전달하는 바인딩 경로였다.
결국 문제는 콤마였다: @Value 바인딩과 audience 불일치
환경변수에서 전달되는 값은 다음과 같은 문자열이다.
id1.apps.googleusercontent.com,id2.apps.googleusercontent.com
즉, 하나의 문자열 안에 콤마로 구분된 값이 들어있는 구조였다. 하지만 GoogleIdTokenVerifier은 다음과 같은 Client ID 목록을 기대한다.
["id1.apps.googleusercontent.com", "id2.apps.googleusercontent.com"]
문제는 이 문자열을 어디에서 어떻게 파싱하는지가 코드에서 명확하지 않았다는 점이었다.
결과적으로 GoogleIdTokenVerifier에 전달되는 audience 값이 기대한 형태로 구성되지 않았고 그 결과 다음과 같은 에러가 발생한 것이었다.
ERROR 1167065 --- [emotion-storage] [io-8080-exec-10] c.g.a.c.a.openidconnect.IdTokenVerifier : id token signature verification failed.
Please see docs for IdTokenVerifier for default settings and configuration options
로그만 보면 JWT 문제처럼 보였지만 실제로는 설정 값을 처리하는 방식의 문제였다.
해결: Client ID 명시 파싱으로 검증 안정화
문제를 해결하기 위해 환경변수에서 받은 Client ID 문자열을 명확하게 파싱하는 구조로 변경했다.
@ConfigurationProperties(prefix = "spring.security.oauth2.google")
public class GoogleOAuthProperties {
private String clientIds;
public void setClientIds(String clientIds) {
this.clientIds = clientIds;
}
public List<String> getClientIdList() {
if (clientIds == null || clientIds.isBlank()) {
return List.of();
}
return Arrays.stream(clientIds.split(","))
.map(String::trim)
.filter(s -> !s.isBlank())
.toList();
}
}
그리고 해당 설정을 활성화하기 위한 Configuration을 추가했다.
@Configuration
@EnableConfigurationProperties(GoogleOAuthProperties.class)
public class GoogleOAuthPropertiesConfig {
}
위 코드를 바탕으로 환경변수 값을 아래와 같은 형태로 변환에 성공했다.
["id1.apps.googleusercontent.com", "id2.apps.googleusercontent.com"]
이후 GoogleIdTokenVerifier에 전달되는 audience 값도 정상적으로 구성되었고 Google ID Token 검증 역시 정상적으로 동작했다.
이번 이슈를 통해 느낀 건 인증 에러 로그가 JWT나 OAuth 문제처럼 보여도 실제 원인은 설정 바인딩 경로처럼 단순한 곳에 있을 수 있다는 점이었다. 특히 멀티 플랫폼처럼 Client ID가 여러 개인 구조에서는 값을 어떻게 저장했는지보다 검증 라이브러리가 기대하는 형태로 어떻게 전달되는지를 먼저 확인하는 게 중요했다. 앞으로는 인증 오류가 발생하면 토큰 검증 로직과 함께 설정 주입 결과까지 반드시 같이 점검할 예정이다.
'Backend' 카테고리의 다른 글
| 배포 시간 14분에서 53초로: 빌드 구조 개선과 ARM64 정합성 해결 (0) | 2026.05.31 |
|---|---|
| 포트는 하나로, 배포는 더 명확하게: Nginx 리버스 프록시와 ECR 전환기 (0) | 2026.05.08 |
| 서버 접속 배포에서 GitHub Actions 자동 배포까지, CI/CD 구성하기 (0) | 2026.05.02 |
| AWS 인프라, 콘솔 대신 Terraform으로 관리해보자 (0) | 2026.04.16 |