From c74b4abfaa61a26b4c1d1a475f309694c9dd52be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=82=98=EB=AF=B8?= Date: Wed, 29 Jul 2026 02:49:39 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20Apple=20ID=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EC=84=9C=EB=AA=85=20=EA=B2=80=EC=A6=9D=20=EB=88=84=EB=9D=BD=20?= =?UTF-8?q?=EC=B7=A8=EC=95=BD=EC=A0=90=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AppleSignInService.getSocialInfo()가 클라이언트가 보낸 Apple ID 토큰을 SignedJWT.parse()로 파싱만 하고 암호학적 서명 검증을 하지 않고 있었음. 발급처(iss)/대상(aud)/만료시간/이메일 인증 여부 같은 클레임 값만 확인했는데, 이 값들은 서명 검증 없이는 클라이언트가 얼마든지 임의로 채울 수 있어 실제로는 애플 로그인 없이도 provider=APPLE로 정식 로그인/토큰 발급이 가능한 상태였음. Apple의 공개키(JWKS, https://appleid.apple.com/auth/keys)로 RS256 서명을 검증하도록 수정. nimbus-jose-jwt(기존 의존성)의 JWSVerificationKeySelector + DefaultJWTProcessor를 사용해 서명이 유효한 토큰만 클레임을 신뢰하도록 함. --- .../auth/service/AppleSignInService.java | 35 +++++++++++++++---- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/runnect/server/auth/service/AppleSignInService.java b/src/main/java/org/runnect/server/auth/service/AppleSignInService.java index 6ff1ab2..706149f 100644 --- a/src/main/java/org/runnect/server/auth/service/AppleSignInService.java +++ b/src/main/java/org/runnect/server/auth/service/AppleSignInService.java @@ -1,7 +1,14 @@ package org.runnect.server.auth.service; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.jwk.source.RemoteJWKSet; +import com.nimbusds.jose.proc.JWSVerificationKeySelector; +import com.nimbusds.jose.proc.SecurityContext; import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.proc.ConfigurableJWTProcessor; +import com.nimbusds.jwt.proc.DefaultJWTProcessor; import lombok.RequiredArgsConstructor; import okhttp3.OkHttpClient; import okhttp3.FormBody; @@ -13,9 +20,8 @@ import org.runnect.server.common.exception.UnauthorizedException; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; -import com.nimbusds.jwt.SignedJWT; -import java.text.ParseException; +import java.net.URL; import java.security.KeyFactory; import java.security.PrivateKey; import java.security.spec.PKCS8EncodedKeySpec; @@ -45,6 +51,8 @@ public class AppleSignInService { @Value("${apple.revoke-url}") private String APPLE_REVOKE_URL; + private static final String APPLE_JWKS_URL = "https://appleid.apple.com/auth/keys"; + private PrivateKey PRIVATE_KEY; @Value("${apple.p8key}") private void getPrivateKey(String P8KEY){ @@ -61,13 +69,12 @@ private void getPrivateKey(String P8KEY){ public SocialInfoResponseDto getSocialInfo(String idToken) { - // 클라에서 준 인증토큰이 정말 애플에서 발급받은게 맞는지 확인 + // (애플 공개키(JWKS)로 서명을 검증해야만 위조된 토큰을 걸러낼 수 있다 — + // 서명 검증 없이 파싱만 하면 누구나 클레임을 임의로 채운 토큰으로 로그인할 수 있음) try{ - //1. idToken을 parse - SignedJWT jwt = SignedJWT.parse(idToken); - JWTClaimsSet claimsSet = jwt.getJWTClaimsSet(); + JWTClaimsSet claimsSet = verifySignatureAndGetClaims(idToken); // 발급처, aud, 시간제한, 이메일 검증 @@ -90,13 +97,27 @@ public SocialInfoResponseDto getSocialInfo(String idToken) { return SocialInfoResponseDto.of(claimsSet.getStringClaim("email"), claimsSet.getSubject()); - }catch (ParseException e){ + }catch (UnauthorizedException e){ + throw e; + }catch (Exception e){ + // 서명 검증 실패(BadJOSEException), 파싱 실패(ParseException), JWKS 조회 실패(JOSEException) 등 + // 위조/변조된 토큰 또는 애플 검증 자체가 불가능한 경우 전부 동일하게 처리 throw new UnauthorizedException(ErrorStatus.INVALID_APPLE_ID_TOKEN_EXCEPTION, ErrorStatus.INVALID_APPLE_ID_TOKEN_EXCEPTION.getMessage()); } } + private JWTClaimsSet verifySignatureAndGetClaims(String idToken) throws Exception { + JWKSource keySource = new RemoteJWKSet<>(new URL(APPLE_JWKS_URL)); + ConfigurableJWTProcessor jwtProcessor = new DefaultJWTProcessor<>(); + JWSVerificationKeySelector keySelector = + new JWSVerificationKeySelector<>(JWSAlgorithm.RS256, keySource); + jwtProcessor.setJWSKeySelector(keySelector); + // 서명이 유효하지 않으면 여기서 BadJOSEException/JOSEException이 던져진다 + return jwtProcessor.process(idToken, null); + } + // id_token 형태 : // { // "aud": 번들아이디,