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": 번들아이디,