diff --git a/backend/src/main/java/com/festago/auth/domain/OpenIdUserInfoProvider.java b/backend/src/main/java/com/festago/auth/domain/OpenIdUserInfoProvider.java deleted file mode 100644 index f721d40d4..000000000 --- a/backend/src/main/java/com/festago/auth/domain/OpenIdUserInfoProvider.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.festago.auth.domain; - -public interface OpenIdUserInfoProvider { - - UserInfo provide(String idToken); -} diff --git a/backend/src/main/java/com/festago/auth/infrastructure/openid/KakaoOpenIdClient.java b/backend/src/main/java/com/festago/auth/infrastructure/openid/KakaoOpenIdClient.java index 5c2fbd0be..09e1c4cd1 100644 --- a/backend/src/main/java/com/festago/auth/infrastructure/openid/KakaoOpenIdClient.java +++ b/backend/src/main/java/com/festago/auth/infrastructure/openid/KakaoOpenIdClient.java @@ -1,20 +1,66 @@ package com.festago.auth.infrastructure.openid; import com.festago.auth.domain.OpenIdClient; +import com.festago.auth.domain.OpenIdNonceValidator; import com.festago.auth.domain.SocialType; import com.festago.auth.domain.UserInfo; -import lombok.RequiredArgsConstructor; +import com.festago.common.exception.ErrorCode; +import com.festago.common.exception.UnauthorizedException; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import java.time.Clock; +import java.util.Date; +import java.util.Set; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; +@Slf4j @Component -@RequiredArgsConstructor public class KakaoOpenIdClient implements OpenIdClient { - private final KakaoOpenIdUserInfoProvider kakaoIdTokenUserInfoProvider; + private static final String ISSUER = "https://kauth.kakao.com"; + private final OpenIdNonceValidator openIdNonceValidator; + private final OpenIdIdTokenParser idTokenParser; + private final Set appKeys; + + public KakaoOpenIdClient( + @Value("${festago.oauth2.kakao.rest-api-key}") String restApiKey, + @Value("${festago.oauth2.kakao.native-app-key}") String nativeAppKey, + KakaoOpenIdPublicKeyLocator kakaoOpenIdPublicKeyLocator, + OpenIdNonceValidator openIdNonceValidator, + Clock clock + ) { + this.appKeys = Set.of(restApiKey, nativeAppKey); + this.openIdNonceValidator = openIdNonceValidator; + this.idTokenParser = new OpenIdIdTokenParser(Jwts.parser() + .keyLocator(kakaoOpenIdPublicKeyLocator) + .requireIssuer(ISSUER) + .clock(() -> Date.from(clock.instant())) + .build()); + } @Override public UserInfo getUserInfo(String idToken) { - return kakaoIdTokenUserInfoProvider.provide(idToken); + Claims payload = idTokenParser.parse(idToken); + openIdNonceValidator.validate(payload.get("nonce", String.class), payload.getExpiration()); + validateAudience(payload.getAudience()); + return UserInfo.builder() + .socialType(SocialType.KAKAO) + .socialId(payload.getSubject()) + .nickname(payload.get("nickname", String.class)) + .profileImage(payload.get("picture", String.class)) + .build(); + } + + private void validateAudience(Set audiences) { + for (String audience : audiences) { + if (appKeys.contains(audience)) { + return; + } + } + log.info("허용되지 않는 id 토큰의 audience 값이 요청되었습니다. audiences={}", audiences); + throw new UnauthorizedException(ErrorCode.OPEN_ID_INVALID_TOKEN); } @Override diff --git a/backend/src/test/java/com/festago/auth/infrastructure/KakaoOpenIdUserInfoProviderTest.java b/backend/src/test/java/com/festago/auth/infrastructure/KakaoOpenIdClientTest.java similarity index 86% rename from backend/src/test/java/com/festago/auth/infrastructure/KakaoOpenIdUserInfoProviderTest.java rename to backend/src/test/java/com/festago/auth/infrastructure/KakaoOpenIdClientTest.java index 0f03442c9..314d5bcf0 100644 --- a/backend/src/test/java/com/festago/auth/infrastructure/KakaoOpenIdUserInfoProviderTest.java +++ b/backend/src/test/java/com/festago/auth/infrastructure/KakaoOpenIdClientTest.java @@ -1,11 +1,10 @@ package com.festago.auth.infrastructure; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.mock; -import static org.mockito.BDDMockito.spy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; import com.festago.auth.infrastructure.openid.KakaoOpenIdPublicKeyLocator; import com.festago.auth.infrastructure.openid.KakaoOpenIdUserInfoProvider; @@ -28,9 +27,9 @@ @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") -class KakaoOpenIdUserInfoProviderTest { +class KakaoOpenIdClientTest { - KakaoOpenIdUserInfoProvider kakaoOpenIdUserInfoProvider; + KakaoOpenIdClient kakaoOpenIdClient; KakaoOpenIdPublicKeyLocator keyLocator; @@ -42,7 +41,7 @@ class KakaoOpenIdUserInfoProviderTest { void setUp() { keyLocator = mock(); clock = spy(Clock.systemDefaultZone()); - kakaoOpenIdUserInfoProvider = new KakaoOpenIdUserInfoProvider( + kakaoOpenIdClient = new KakaoOpenIdClient( "restApiKey", "nativeAppKey", keyLocator, @@ -65,7 +64,7 @@ void setUp() { .compact(); // when & then - assertThatThrownBy(() -> kakaoOpenIdUserInfoProvider.provide(idToken)) + assertThatThrownBy(() -> kakaoOpenIdClient.getUserInfo(idToken)) .isInstanceOf(UnauthorizedException.class) .hasMessage(ErrorCode.OPEN_ID_INVALID_TOKEN.getMessage()); } @@ -84,7 +83,7 @@ void setUp() { .compact(); // when & then - assertThatThrownBy(() -> kakaoOpenIdUserInfoProvider.provide(idToken)) + assertThatThrownBy(() -> kakaoOpenIdClient.getUserInfo(idToken)) .isInstanceOf(UnauthorizedException.class) .hasMessage(ErrorCode.OPEN_ID_INVALID_TOKEN.getMessage()); } @@ -104,7 +103,7 @@ void setUp() { .compact(); // when & then - assertThatThrownBy(() -> kakaoOpenIdUserInfoProvider.provide(idToken)) + assertThatThrownBy(() -> kakaoOpenIdClient.getUserInfo(idToken)) .isInstanceOf(UnauthorizedException.class) .hasMessage(ErrorCode.OPEN_ID_INVALID_TOKEN.getMessage()); } @@ -124,7 +123,7 @@ void setUp() { .compact(); // when & then - assertThatThrownBy(() -> kakaoOpenIdUserInfoProvider.provide(idToken)) + assertThatThrownBy(() -> kakaoOpenIdClient.getUserInfo(idToken)) .isInstanceOf(UnauthorizedException.class) .hasMessage(ErrorCode.OPEN_ID_INVALID_TOKEN.getMessage()); } @@ -143,7 +142,7 @@ void setUp() { .compact(); // when & then - assertThatThrownBy(() -> kakaoOpenIdUserInfoProvider.provide(idToken)) + assertThatThrownBy(() -> kakaoOpenIdClient.getUserInfo(idToken)) .isInstanceOf(UnauthorizedException.class) .hasMessage(ErrorCode.OPEN_ID_INVALID_TOKEN.getMessage()); } @@ -164,7 +163,7 @@ void setUp() { .compact(); // when - var expect = kakaoOpenIdUserInfoProvider.provide(idToken); + var expect = kakaoOpenIdClient.getUserInfo(idToken); // then assertThat(expect.socialId()).isEqualTo(socialId); @@ -187,7 +186,7 @@ void setUp() { .compact(); // when - var expect = kakaoOpenIdUserInfoProvider.provide(idToken); + var expect = kakaoOpenIdClient.getUserInfo(idToken); // then assertThat(expect.socialId()).isEqualTo(socialId);