From 1eedcfe10b805c22b2a44ad267dbe989134a926c Mon Sep 17 00:00:00 2001 From: inhyeok Date: Mon, 29 Jul 2024 13:49:02 +0900 Subject: [PATCH 1/3] =?UTF-8?q?Feat(Oauth):=20google=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EA=B0=80=EC=9E=85=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../oidc/AbstractIdTokenValidator.java | 2 +- .../oidc/GoogleIdTokenValidator.java | 105 ++++++++++++++++++ .../application/oidc/IdTokenJwtHandler.java | 2 +- .../oidc/KakaoIdTokenValidator.java | 6 +- .../oidc/property/GoogleOidcProperty.java | 21 ++++ jabiseo-api/src/main/resources/api.yml | 3 + .../oidc/KakaoIdTokenValidatorTest.java | 6 +- .../jabiseo/cache/RedisCacheRepository.java | 33 +++++- .../jabiseo/client/NetworkApiErrorCode.java | 6 +- .../com/jabiseo/client/RestClientConfig.java | 21 +++- .../client/oidc/GoogleAccountsClient.java | 13 +++ .../jabiseo/client/oidc/GoogleOidcClient.java | 9 ++ .../client/oidc/GoogleOidcClientImpl.java | 20 ++++ .../oidc/GoogleOpenIdConfiguration.java | 29 +++++ .../client/{ => oidc}/KakaoKauthClient.java | 2 +- .../client/{ => oidc}/OidcPublicKey.java | 6 +- .../{ => oidc}/OidcPublicKeyResponse.java | 2 +- 17 files changed, 268 insertions(+), 18 deletions(-) create mode 100644 jabiseo-api/src/main/java/com/jabiseo/auth/application/oidc/GoogleIdTokenValidator.java create mode 100644 jabiseo-api/src/main/java/com/jabiseo/auth/application/oidc/property/GoogleOidcProperty.java create mode 100644 jabiseo-infrastructure/src/main/java/com/jabiseo/client/oidc/GoogleAccountsClient.java create mode 100644 jabiseo-infrastructure/src/main/java/com/jabiseo/client/oidc/GoogleOidcClient.java create mode 100644 jabiseo-infrastructure/src/main/java/com/jabiseo/client/oidc/GoogleOidcClientImpl.java create mode 100644 jabiseo-infrastructure/src/main/java/com/jabiseo/client/oidc/GoogleOpenIdConfiguration.java rename jabiseo-infrastructure/src/main/java/com/jabiseo/client/{ => oidc}/KakaoKauthClient.java (93%) rename jabiseo-infrastructure/src/main/java/com/jabiseo/client/{ => oidc}/OidcPublicKey.java (86%) rename jabiseo-infrastructure/src/main/java/com/jabiseo/client/{ => oidc}/OidcPublicKeyResponse.java (90%) diff --git a/jabiseo-api/src/main/java/com/jabiseo/auth/application/oidc/AbstractIdTokenValidator.java b/jabiseo-api/src/main/java/com/jabiseo/auth/application/oidc/AbstractIdTokenValidator.java index 1688da23..61e4f979 100644 --- a/jabiseo-api/src/main/java/com/jabiseo/auth/application/oidc/AbstractIdTokenValidator.java +++ b/jabiseo-api/src/main/java/com/jabiseo/auth/application/oidc/AbstractIdTokenValidator.java @@ -1,7 +1,7 @@ package com.jabiseo.auth.application.oidc; -import com.jabiseo.client.OidcPublicKey; +import com.jabiseo.client.oidc.OidcPublicKey; import com.jabiseo.auth.application.oidc.property.OidcIdTokenProperty; import com.jabiseo.member.domain.OauthServer; diff --git a/jabiseo-api/src/main/java/com/jabiseo/auth/application/oidc/GoogleIdTokenValidator.java b/jabiseo-api/src/main/java/com/jabiseo/auth/application/oidc/GoogleIdTokenValidator.java new file mode 100644 index 00000000..44439057 --- /dev/null +++ b/jabiseo-api/src/main/java/com/jabiseo/auth/application/oidc/GoogleIdTokenValidator.java @@ -0,0 +1,105 @@ +package com.jabiseo.auth.application.oidc; + + +import com.jabiseo.auth.application.oidc.property.GoogleOidcProperty; +import com.jabiseo.auth.exception.AuthenticationBusinessException; +import com.jabiseo.auth.exception.AuthenticationErrorCode; +import com.jabiseo.cache.RedisCacheRepository; +import com.jabiseo.client.oidc.GoogleAccountsClient; +import com.jabiseo.client.oidc.GoogleOidcClient; +import com.jabiseo.client.oidc.GoogleOpenIdConfiguration; +import com.jabiseo.client.oidc.OidcPublicKey; +import com.jabiseo.common.exception.CommonErrorCode; +import com.jabiseo.member.domain.OauthServer; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; + +@Slf4j +@Component +public class GoogleIdTokenValidator extends AbstractIdTokenValidator { + + private final RedisCacheRepository redisCacheRepository; + private final GoogleAccountsClient googleAccountsClient; + private final GoogleOidcClient googleOidcClient; + private static final String OPENID_CONFIGURATION_CACHE_KEY = "GOOGLE_OPENID_CONFIGURATION"; + private static final String PUBLIC_KEY_CACHE_KEY = "GOOGLE_OIDC_PUBLIC_KEY"; + private static final String GOOGLE_ID_KEY = "sub"; + private static final String GOOGLE_EMAIL_KEY = "email"; + + /* Google Public Key를 받는 로직 + * 1. https://accounts.google.com/.well-known/openid-configuration -> jwks_uri 값을 파싱해온다. + * 2. jwks_uri 으로 API를 호출해서 Public Key에 대한 응답 값을 받는다 + * 1번과 2번의 모든 결과는 캐싱을 해야 한다. + * Ref: https://developers.google.com/identity/openid-connect/openid-connect?hl=ko#validatinganidtoken + */ + + public GoogleIdTokenValidator(GoogleOidcProperty googleOidcProperty, + IdTokenJwtHandler idTokenJwtHandler, + RedisCacheRepository redisCacheRepository, + GoogleAccountsClient googleAccountsClient, + GoogleOidcClient googleOidcClient) { + super(googleOidcProperty.toIdTokenProperty(), idTokenJwtHandler); + this.redisCacheRepository = redisCacheRepository; + this.googleAccountsClient = googleAccountsClient; + this.googleOidcClient = googleOidcClient; + } + + + @Override + protected OidcPublicKey getOidcPublicKey(String kid) { + List keys = redisCacheRepository.getPublicKeys(PUBLIC_KEY_CACHE_KEY); + + if (keys == null) { + keys = getOidcPublicKeysFromGoogle(); + redisCacheRepository.savePublicKey(PUBLIC_KEY_CACHE_KEY, keys); + } + + return keys.stream().filter((key) -> key.getKid().equals(kid)) + .findAny() + .orElseThrow(() -> new AuthenticationBusinessException(AuthenticationErrorCode.INVALID_ID_TOKEN)); + } + + private List getOidcPublicKeysFromGoogle() { + GoogleOpenIdConfiguration config = redisCacheRepository.getGoogleConfiguration(OPENID_CONFIGURATION_CACHE_KEY); + + try { + if (config == null) { + ResponseEntity oidcConfiguration = googleAccountsClient.getOidcConfiguration(); + config = oidcConfiguration.getBody(); + redisCacheRepository.saveOpenConfiguation(OPENID_CONFIGURATION_CACHE_KEY, config); + } + return googleOidcClient.getPublicKeys(config.getJwks_uri()); + } catch (NullPointerException e) { + log.error(e.getMessage()); + throw new AuthenticationBusinessException(AuthenticationErrorCode.GET_JWK_FAIL); + } + } + + @Override + protected OauthMemberInfo extractMemberInfoFromPayload(Map payload) { + String oauthId = (String) payload.get(GOOGLE_ID_KEY); + String email = (String) payload.get(GOOGLE_EMAIL_KEY); + if (requireValueIsNull(oauthId, email)) { + throw new AuthenticationBusinessException(CommonErrorCode.INTERNAL_SERVER_ERROR); + } + + return OauthMemberInfo.builder() + .oauthId(oauthId) + .email(email) + .oauthServer(OauthServer.GOOGLE) + .build(); + } + + @Override + OauthServer getOauthServer() { + return OauthServer.GOOGLE; + } + + private boolean requireValueIsNull(String oauthId, String email) { + return oauthId == null || email == null; + } +} diff --git a/jabiseo-api/src/main/java/com/jabiseo/auth/application/oidc/IdTokenJwtHandler.java b/jabiseo-api/src/main/java/com/jabiseo/auth/application/oidc/IdTokenJwtHandler.java index b042bf16..b93cd62b 100644 --- a/jabiseo-api/src/main/java/com/jabiseo/auth/application/oidc/IdTokenJwtHandler.java +++ b/jabiseo-api/src/main/java/com/jabiseo/auth/application/oidc/IdTokenJwtHandler.java @@ -1,6 +1,6 @@ package com.jabiseo.auth.application.oidc; -import com.jabiseo.client.OidcPublicKey; +import com.jabiseo.client.oidc.OidcPublicKey; import com.jabiseo.auth.exception.AuthenticationBusinessException; import com.jabiseo.auth.exception.AuthenticationErrorCode; import io.jsonwebtoken.*; diff --git a/jabiseo-api/src/main/java/com/jabiseo/auth/application/oidc/KakaoIdTokenValidator.java b/jabiseo-api/src/main/java/com/jabiseo/auth/application/oidc/KakaoIdTokenValidator.java index 34cd5a80..5b7ac869 100644 --- a/jabiseo-api/src/main/java/com/jabiseo/auth/application/oidc/KakaoIdTokenValidator.java +++ b/jabiseo-api/src/main/java/com/jabiseo/auth/application/oidc/KakaoIdTokenValidator.java @@ -1,10 +1,10 @@ package com.jabiseo.auth.application.oidc; import com.jabiseo.cache.RedisCacheRepository; -import com.jabiseo.client.KakaoKauthClient; +import com.jabiseo.client.oidc.KakaoKauthClient; import com.jabiseo.client.NetworkApiException; -import com.jabiseo.client.OidcPublicKey; -import com.jabiseo.client.OidcPublicKeyResponse; +import com.jabiseo.client.oidc.OidcPublicKey; +import com.jabiseo.client.oidc.OidcPublicKeyResponse; import com.jabiseo.auth.exception.AuthenticationBusinessException; import com.jabiseo.auth.exception.AuthenticationErrorCode; import com.jabiseo.auth.application.oidc.property.KakaoOidcProperty; diff --git a/jabiseo-api/src/main/java/com/jabiseo/auth/application/oidc/property/GoogleOidcProperty.java b/jabiseo-api/src/main/java/com/jabiseo/auth/application/oidc/property/GoogleOidcProperty.java new file mode 100644 index 00000000..54ac1245 --- /dev/null +++ b/jabiseo-api/src/main/java/com/jabiseo/auth/application/oidc/property/GoogleOidcProperty.java @@ -0,0 +1,21 @@ +package com.jabiseo.auth.application.oidc.property; + +import lombok.Getter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Getter +@ConfigurationProperties(prefix = "oidc.google") +public class GoogleOidcProperty { + + private final String clientId; + private final String issuer; + + public GoogleOidcProperty(String clientId, String issuer) { + this.clientId = clientId; + this.issuer = issuer; + } + + public OidcIdTokenProperty toIdTokenProperty() { + return new OidcIdTokenProperty(issuer, clientId); + } +} diff --git a/jabiseo-api/src/main/resources/api.yml b/jabiseo-api/src/main/resources/api.yml index c5a1cafa..be688a69 100644 --- a/jabiseo-api/src/main/resources/api.yml +++ b/jabiseo-api/src/main/resources/api.yml @@ -5,6 +5,9 @@ oidc: issuer: ${KAKAO_ISSUER} admin-key: ${KAKAO_ADMIN_KEY} client-id: ${KAKAO_CLIENT_ID} + google: + client-id: ${GOOGLE_CLIENT_ID} + issuer: ${GOOGLE_ISSUER} jwt: access-expired-min: 60 diff --git a/jabiseo-api/src/test/java/com/jabiseo/auth/application/oidc/KakaoIdTokenValidatorTest.java b/jabiseo-api/src/test/java/com/jabiseo/auth/application/oidc/KakaoIdTokenValidatorTest.java index af6b3748..58281847 100644 --- a/jabiseo-api/src/test/java/com/jabiseo/auth/application/oidc/KakaoIdTokenValidatorTest.java +++ b/jabiseo-api/src/test/java/com/jabiseo/auth/application/oidc/KakaoIdTokenValidatorTest.java @@ -4,10 +4,10 @@ import com.jabiseo.auth.exception.AuthenticationBusinessException; import com.jabiseo.auth.exception.AuthenticationErrorCode; import com.jabiseo.cache.RedisCacheRepository; -import com.jabiseo.client.KakaoKauthClient; +import com.jabiseo.client.oidc.KakaoKauthClient; import com.jabiseo.client.NetworkApiException; -import com.jabiseo.client.OidcPublicKey; -import com.jabiseo.client.OidcPublicKeyResponse; +import com.jabiseo.client.oidc.OidcPublicKey; +import com.jabiseo.client.oidc.OidcPublicKeyResponse; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; diff --git a/jabiseo-infrastructure/src/main/java/com/jabiseo/cache/RedisCacheRepository.java b/jabiseo-infrastructure/src/main/java/com/jabiseo/cache/RedisCacheRepository.java index 5b565d65..f899df6c 100644 --- a/jabiseo-infrastructure/src/main/java/com/jabiseo/cache/RedisCacheRepository.java +++ b/jabiseo-infrastructure/src/main/java/com/jabiseo/cache/RedisCacheRepository.java @@ -2,9 +2,11 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import com.jabiseo.client.OidcPublicKey; +import com.jabiseo.client.oidc.GoogleOpenIdConfiguration; +import com.jabiseo.client.oidc.OidcPublicKey; import com.jabiseo.common.exception.BusinessException; import com.jabiseo.common.exception.CommonErrorCode; +import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ValueOperations; import org.springframework.stereotype.Component; @@ -15,6 +17,7 @@ import java.util.concurrent.TimeUnit; @Component +@Slf4j public class RedisCacheRepository { private final RedisTemplate redisStringTemplate; @@ -37,11 +40,11 @@ public Optional findToken(String key) { return Optional.ofNullable(token); } - public void deleteToken(String key){ + public void deleteToken(String key) { operation.getAndDelete(toMemberTokenKey(key)); } - private String toMemberTokenKey(String id){ + private String toMemberTokenKey(String id) { return MEMBER_TOKEN_PREFIX + id; } @@ -51,6 +54,7 @@ public void savePublicKey(String key, List publicKeys) { // TODO: timeout 값 논의 필요 operation.set(key, publicKeyString, 1, TimeUnit.DAYS); } catch (JsonProcessingException e) { + log.error(e.getMessage()); throw new BusinessException(CommonErrorCode.INTERNAL_SERVER_ERROR); } } @@ -63,9 +67,32 @@ public List getPublicKeys(String key) { try { return Arrays.asList(mapper.readValue(values, OidcPublicKey[].class)); } catch (JsonProcessingException e) { + log.error(e.getMessage()); throw new BusinessException(CommonErrorCode.INTERNAL_SERVER_ERROR); } } + public void saveOpenConfiguation(String key, GoogleOpenIdConfiguration configuration) { + try { + String configDocs = mapper.writeValueAsString(configuration); + operation.set(key, configDocs, 1, TimeUnit.DAYS); + } catch (JsonProcessingException e) { + log.error(e.getMessage()); + throw new BusinessException(CommonErrorCode.INTERNAL_SERVER_ERROR); + } + } + public GoogleOpenIdConfiguration getGoogleConfiguration(String key) { + String value = operation.get(key); + if (value == null) { + return null; + } + + try { + return mapper.readValue(value, GoogleOpenIdConfiguration.class); + } catch (JsonProcessingException e) { + log.error(e.getMessage()); + throw new BusinessException(CommonErrorCode.INTERNAL_SERVER_ERROR); + } + } } diff --git a/jabiseo-infrastructure/src/main/java/com/jabiseo/client/NetworkApiErrorCode.java b/jabiseo-infrastructure/src/main/java/com/jabiseo/client/NetworkApiErrorCode.java index 2cc97f5d..c3a56e0e 100644 --- a/jabiseo-infrastructure/src/main/java/com/jabiseo/client/NetworkApiErrorCode.java +++ b/jabiseo-infrastructure/src/main/java/com/jabiseo/client/NetworkApiErrorCode.java @@ -5,8 +5,10 @@ @Getter public enum NetworkApiErrorCode implements ErrorCode { - KAKAO_JWK_API_FAIL("카카오 kauth jwk 연결 실패", "NETWORK_001", ErrorCode.INTERNAL_SERVER_ERROR); - + COMMON_API_FAIL("API fail", "NETWORK_001", ErrorCode.INTERNAL_SERVER_ERROR), + KAKAO_JWK_API_FAIL("카카오 kauth jwk 연결 실패", "NETWORK_002", ErrorCode.INTERNAL_SERVER_ERROR), + GOOGLE_OPENAI_CONFIG_API_FAIL("구글 openai 연결 실패", "NETWORK_003", ErrorCode.INTERNAL_SERVER_ERROR), + GOOGLE_JWK_API_FAIL("구글 jwk 연결 실패", "NETWORK_004", ErrorCode.INTERNAL_SERVER_ERROR); private final String message; private final String errorCode; private final int statusCode; diff --git a/jabiseo-infrastructure/src/main/java/com/jabiseo/client/RestClientConfig.java b/jabiseo-infrastructure/src/main/java/com/jabiseo/client/RestClientConfig.java index e650cd2e..a4307af6 100644 --- a/jabiseo-infrastructure/src/main/java/com/jabiseo/client/RestClientConfig.java +++ b/jabiseo-infrastructure/src/main/java/com/jabiseo/client/RestClientConfig.java @@ -1,6 +1,8 @@ package com.jabiseo.client; +import com.jabiseo.client.oidc.GoogleAccountsClient; +import com.jabiseo.client.oidc.KakaoKauthClient; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpStatusCode; @@ -11,7 +13,6 @@ @Configuration public class RestClientConfig { - @Bean public KakaoKauthClient kakaoKauthClient() { RestClient client = RestClient.builder() @@ -27,4 +28,22 @@ public KakaoKauthClient kakaoKauthClient() { .build() .createClient(KakaoKauthClient.class); } + + @Bean + public GoogleAccountsClient googleOidcClient() { + RestClient client = RestClient.builder() + .baseUrl("https://accounts.google.com") + .defaultStatusHandler(HttpStatusCode::isError, ((request, response) -> { + throw new NetworkApiException(NetworkApiErrorCode.GOOGLE_OPENAI_CONFIG_API_FAIL); + })) + .build(); + + return HttpServiceProxyFactory + .builder() + .exchangeAdapter(RestClientAdapter.create(client)) + .build() + .createClient(GoogleAccountsClient.class); + } + + } diff --git a/jabiseo-infrastructure/src/main/java/com/jabiseo/client/oidc/GoogleAccountsClient.java b/jabiseo-infrastructure/src/main/java/com/jabiseo/client/oidc/GoogleAccountsClient.java new file mode 100644 index 00000000..b2772685 --- /dev/null +++ b/jabiseo-infrastructure/src/main/java/com/jabiseo/client/oidc/GoogleAccountsClient.java @@ -0,0 +1,13 @@ +package com.jabiseo.client.oidc; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.service.annotation.GetExchange; +import org.springframework.web.service.annotation.HttpExchange; + +@HttpExchange +public interface GoogleAccountsClient { + + @GetExchange(url = "/.well-known/openid-configuration") + ResponseEntity getOidcConfiguration(); + +} diff --git a/jabiseo-infrastructure/src/main/java/com/jabiseo/client/oidc/GoogleOidcClient.java b/jabiseo-infrastructure/src/main/java/com/jabiseo/client/oidc/GoogleOidcClient.java new file mode 100644 index 00000000..65bccaa5 --- /dev/null +++ b/jabiseo-infrastructure/src/main/java/com/jabiseo/client/oidc/GoogleOidcClient.java @@ -0,0 +1,9 @@ +package com.jabiseo.client.oidc; + +import java.util.List; + +public interface GoogleOidcClient { + + List getPublicKeys(String url); + +} diff --git a/jabiseo-infrastructure/src/main/java/com/jabiseo/client/oidc/GoogleOidcClientImpl.java b/jabiseo-infrastructure/src/main/java/com/jabiseo/client/oidc/GoogleOidcClientImpl.java new file mode 100644 index 00000000..92d1e0fe --- /dev/null +++ b/jabiseo-infrastructure/src/main/java/com/jabiseo/client/oidc/GoogleOidcClientImpl.java @@ -0,0 +1,20 @@ +package com.jabiseo.client.oidc; + +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClient; + +import java.util.List; + +@Component +public class GoogleOidcClientImpl implements GoogleOidcClient { + + @Override + public List getPublicKeys(String uri) { + RestClient restClient = RestClient.create(); + OidcPublicKeyResponse response = restClient.get() + .uri(uri) + .retrieve() + .body(OidcPublicKeyResponse.class); + return response.getKeys(); + } +} diff --git a/jabiseo-infrastructure/src/main/java/com/jabiseo/client/oidc/GoogleOpenIdConfiguration.java b/jabiseo-infrastructure/src/main/java/com/jabiseo/client/oidc/GoogleOpenIdConfiguration.java new file mode 100644 index 00000000..6340b3ce --- /dev/null +++ b/jabiseo-infrastructure/src/main/java/com/jabiseo/client/oidc/GoogleOpenIdConfiguration.java @@ -0,0 +1,29 @@ +package com.jabiseo.client.oidc; + + +import lombok.*; + +import java.util.List; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class GoogleOpenIdConfiguration { + private String issuer; + private String authorization_endpoint; + private String device_authorization_endpoint; + private String token_endpoint; + private String userinfo_endpoint; + private String revocation_endpoint; + private String jwks_uri; + private List response_types_supported; + private List subject_types_supported; + private List id_token_signing_alg_values_supported; + private List scopes_supported; + private List token_endpoint_auth_methods_supported; + private List claims_supported; + private List code_challenge_methods_supported; + private List grant_types_supported; +} diff --git a/jabiseo-infrastructure/src/main/java/com/jabiseo/client/KakaoKauthClient.java b/jabiseo-infrastructure/src/main/java/com/jabiseo/client/oidc/KakaoKauthClient.java similarity index 93% rename from jabiseo-infrastructure/src/main/java/com/jabiseo/client/KakaoKauthClient.java rename to jabiseo-infrastructure/src/main/java/com/jabiseo/client/oidc/KakaoKauthClient.java index 5617bff7..9e26c226 100644 --- a/jabiseo-infrastructure/src/main/java/com/jabiseo/client/KakaoKauthClient.java +++ b/jabiseo-infrastructure/src/main/java/com/jabiseo/client/oidc/KakaoKauthClient.java @@ -1,4 +1,4 @@ -package com.jabiseo.client; +package com.jabiseo.client.oidc; ; import org.springframework.http.ResponseEntity; diff --git a/jabiseo-infrastructure/src/main/java/com/jabiseo/client/OidcPublicKey.java b/jabiseo-infrastructure/src/main/java/com/jabiseo/client/oidc/OidcPublicKey.java similarity index 86% rename from jabiseo-infrastructure/src/main/java/com/jabiseo/client/OidcPublicKey.java rename to jabiseo-infrastructure/src/main/java/com/jabiseo/client/oidc/OidcPublicKey.java index 3c4037be..8561ccf5 100644 --- a/jabiseo-infrastructure/src/main/java/com/jabiseo/client/OidcPublicKey.java +++ b/jabiseo-infrastructure/src/main/java/com/jabiseo/client/oidc/OidcPublicKey.java @@ -1,4 +1,4 @@ -package com.jabiseo.client; +package com.jabiseo.client.oidc; import lombok.Getter; import lombok.NoArgsConstructor; @@ -16,13 +16,15 @@ public class OidcPublicKey { private String use; private String n; private String e; + private String kty; - public OidcPublicKey(String kid, String alg, String use, String n, String e) { + public OidcPublicKey(String kid, String alg, String use, String n, String e, String kty) { this.kid = kid; this.alg = alg; this.use = use; this.n = n; this.e = e; + this.kty = kty; } @Override diff --git a/jabiseo-infrastructure/src/main/java/com/jabiseo/client/OidcPublicKeyResponse.java b/jabiseo-infrastructure/src/main/java/com/jabiseo/client/oidc/OidcPublicKeyResponse.java similarity index 90% rename from jabiseo-infrastructure/src/main/java/com/jabiseo/client/OidcPublicKeyResponse.java rename to jabiseo-infrastructure/src/main/java/com/jabiseo/client/oidc/OidcPublicKeyResponse.java index c1a35797..0cafe70b 100644 --- a/jabiseo-infrastructure/src/main/java/com/jabiseo/client/OidcPublicKeyResponse.java +++ b/jabiseo-infrastructure/src/main/java/com/jabiseo/client/oidc/OidcPublicKeyResponse.java @@ -1,4 +1,4 @@ -package com.jabiseo.client; +package com.jabiseo.client.oidc; import lombok.Getter; import lombok.NoArgsConstructor; From 22ac256ba2eeb4a1898d66f74f5f0e2ad5bdcb19 Mon Sep 17 00:00:00 2001 From: inhyeok Date: Mon, 29 Jul 2024 14:51:53 +0900 Subject: [PATCH 2/3] =?UTF-8?q?Test(Auth):=20GoogleTokenValidator=20Test?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../oidc/GoogleIdTokenValidator.java | 3 +- .../oidc/GoogleIdTokenValidatorTest.java | 164 ++++++++++++++++++ .../oidc/KakaoIdTokenValidatorTest.java | 4 +- .../oidc/GoogleOpenIdConfiguration.java | 1 + 4 files changed, 169 insertions(+), 3 deletions(-) create mode 100644 jabiseo-api/src/test/java/com/jabiseo/auth/application/oidc/GoogleIdTokenValidatorTest.java diff --git a/jabiseo-api/src/main/java/com/jabiseo/auth/application/oidc/GoogleIdTokenValidator.java b/jabiseo-api/src/main/java/com/jabiseo/auth/application/oidc/GoogleIdTokenValidator.java index 44439057..b81af308 100644 --- a/jabiseo-api/src/main/java/com/jabiseo/auth/application/oidc/GoogleIdTokenValidator.java +++ b/jabiseo-api/src/main/java/com/jabiseo/auth/application/oidc/GoogleIdTokenValidator.java @@ -5,6 +5,7 @@ import com.jabiseo.auth.exception.AuthenticationBusinessException; import com.jabiseo.auth.exception.AuthenticationErrorCode; import com.jabiseo.cache.RedisCacheRepository; +import com.jabiseo.client.NetworkApiException; import com.jabiseo.client.oidc.GoogleAccountsClient; import com.jabiseo.client.oidc.GoogleOidcClient; import com.jabiseo.client.oidc.GoogleOpenIdConfiguration; @@ -73,7 +74,7 @@ private List getOidcPublicKeysFromGoogle() { redisCacheRepository.saveOpenConfiguation(OPENID_CONFIGURATION_CACHE_KEY, config); } return googleOidcClient.getPublicKeys(config.getJwks_uri()); - } catch (NullPointerException e) { + } catch (NetworkApiException e) { log.error(e.getMessage()); throw new AuthenticationBusinessException(AuthenticationErrorCode.GET_JWK_FAIL); } diff --git a/jabiseo-api/src/test/java/com/jabiseo/auth/application/oidc/GoogleIdTokenValidatorTest.java b/jabiseo-api/src/test/java/com/jabiseo/auth/application/oidc/GoogleIdTokenValidatorTest.java new file mode 100644 index 00000000..d96fa9aa --- /dev/null +++ b/jabiseo-api/src/test/java/com/jabiseo/auth/application/oidc/GoogleIdTokenValidatorTest.java @@ -0,0 +1,164 @@ +package com.jabiseo.auth.application.oidc; + +import com.jabiseo.auth.application.oidc.property.GoogleOidcProperty; +import com.jabiseo.auth.exception.AuthenticationBusinessException; +import com.jabiseo.auth.exception.AuthenticationErrorCode; +import com.jabiseo.cache.RedisCacheRepository; +import com.jabiseo.client.NetworkApiException; +import com.jabiseo.client.oidc.*; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.ResponseEntity; + +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.*; + +@DisplayName("구글 IdToken 검증 테스트") +@ExtendWith(MockitoExtension.class) +class GoogleIdTokenValidatorTest { + + @InjectMocks + GoogleIdTokenValidator validator; + + @Mock + IdTokenJwtHandler idTokenJwtHandler; + + @Mock + GoogleOidcProperty googleOidcProperty; + + @Mock + RedisCacheRepository redisCacheRepository; + + @Mock + GoogleAccountsClient googleAccountsClient; + + @Mock + GoogleOidcClient googleOidcClient; + + GoogleOpenIdConfiguration configuration = mockGoogleOpenIdConfiguration(); + + @BeforeEach + void setUp() { + googleOidcProperty = new GoogleOidcProperty("client_id", "issuer"); + } + + @Test + @DisplayName("구글 jwk 조회시 캐시에 데이터가 있다면 Google API를 호출하지 않는다.") + void notCallApiAlreadySavedCache() { + //given + given(redisCacheRepository.getPublicKeys(any(String.class))).willReturn(List.of(mockPublicKey("kid1"), mockPublicKey("kid2"))); + + //when + validator.getOidcPublicKey("kid1"); + + //then + verify(googleOidcClient, never()).getPublicKeys("urls"); + verify(googleAccountsClient, never()).getOidcConfiguration(); + } + + @Test + @DisplayName("구글 jwk 조회시 캐시에 데이터가 없다면 캐시 데이터를 호출하고 구글API호출 후 캐시에 저장한다.") + void callApiNotSavedCallGoogleAPIAndSavedCache(){ + //given + List publicKeys = List.of(mockPublicKey("kid1"), mockPublicKey("kid2")); + given(redisCacheRepository.getPublicKeys(any())).willReturn(null); // cache hit fail + given(redisCacheRepository.getGoogleConfiguration(any())).willReturn(configuration); // cahce hit success + given(googleOidcClient.getPublicKeys(configuration.getJwks_uri())).willReturn(publicKeys); + + //when + validator.getOidcPublicKey("kid1"); + + //then + verify(googleOidcClient, times(1)).getPublicKeys(configuration.getJwks_uri()); + verify(redisCacheRepository, times(1)).savePublicKey("GOOGLE_OIDC_PUBLIC_KEY",publicKeys); + } + + @Test + @DisplayName("구글 jwk 조회시 jwk도 없고 openid정보도 없다면 둘 다 호출한다") + void isNullJwKAndOpenIdCallingJwkAndOpenId(){ + //given + ResponseEntity res = ResponseEntity.of(Optional.of(configuration)); + + List publicKeys = List.of(mockPublicKey("kid1"), mockPublicKey("kid2")); + given(redisCacheRepository.getPublicKeys(any())).willReturn(null); // cache hit fail + given(redisCacheRepository.getGoogleConfiguration(any())).willReturn(null); // cahce hit success + given(googleOidcClient.getPublicKeys(any())).willReturn(publicKeys); + given(googleAccountsClient.getOidcConfiguration()).willReturn(res); + + //when + validator.getOidcPublicKey("kid1"); + + //then + verify(googleAccountsClient, times(1)).getOidcConfiguration(); + verify(googleOidcClient, times(1)).getPublicKeys(any()); + } + + @Test + @DisplayName("구글 jwk 조회시 kid 에 맞는 key가 없다면 예외를 반환한다.") + void notMatchKidThrownException(){ + //given + List publicKeys = List.of(mockPublicKey("kid1"), mockPublicKey("kid2")); + given(redisCacheRepository.getPublicKeys(any())).willReturn(publicKeys); + String otherKid = "kid"; + //when //then + + Assertions.assertThatThrownBy(()->validator.getOidcPublicKey(otherKid)) + .isInstanceOf(AuthenticationBusinessException.class) + .hasMessage(AuthenticationErrorCode.INVALID_ID_TOKEN.getMessage()); + } + + @Test + @DisplayName("구글 jwk 획득 중 api 호출 실패시 예외를 반환한다") + void getJwkKakaoApiCallingFailThrownException(){ + //given + given(redisCacheRepository.getPublicKeys(any())).willReturn(null); + given(redisCacheRepository.getGoogleConfiguration(any())).willReturn(configuration); + given(googleOidcClient.getPublicKeys(configuration.getJwks_uri())).willThrow(NetworkApiException.class); + + //when //then + assertThatThrownBy(() -> validator.getOidcPublicKey("key")) + .isInstanceOf(AuthenticationBusinessException.class) + .hasMessage(AuthenticationErrorCode.GET_JWK_FAIL.getMessage()); + } + + @Test + @DisplayName("구글 jwk 획등 중 kid에 맞는 jwk가 있다면 반환한다") + void successMatchKidReturnOidcPublicKey(){ + //given + String matchKid = "kid"; + OidcPublicKey matchPublicKey = mockPublicKey(matchKid); + List publicKeys = List.of(matchPublicKey, mockPublicKey("kid2")); + + given(redisCacheRepository.getPublicKeys(any())).willReturn(publicKeys); + + //when + OidcPublicKey key = validator.getOidcPublicKey("kid"); + + //then + Assertions.assertThat(key).isEqualTo(matchPublicKey); + } + + + private GoogleOpenIdConfiguration mockGoogleOpenIdConfiguration() { + return GoogleOpenIdConfiguration + .builder() + .jwks_uri("https..") + .build(); + } + + + private OidcPublicKey mockPublicKey(String kid) { + return new OidcPublicKey(kid, "a", "u", "n", "e", "."); + } +} \ No newline at end of file diff --git a/jabiseo-api/src/test/java/com/jabiseo/auth/application/oidc/KakaoIdTokenValidatorTest.java b/jabiseo-api/src/test/java/com/jabiseo/auth/application/oidc/KakaoIdTokenValidatorTest.java index 58281847..23967b73 100644 --- a/jabiseo-api/src/test/java/com/jabiseo/auth/application/oidc/KakaoIdTokenValidatorTest.java +++ b/jabiseo-api/src/test/java/com/jabiseo/auth/application/oidc/KakaoIdTokenValidatorTest.java @@ -113,7 +113,7 @@ void SuccessMatchKidReturnOidcPublicKey() { @Test @DisplayName("카카오 jwk 획득 api 호출 실패시 에러를 반환한다") - void getJwkKakaoApiCallingFailThrownExcetpion(){ + void getJwkKakaoApiCallingFailThrownException(){ //given given(redisCacheRepository.getPublicKeys(any())).willReturn(null); given(kakaoKauthClient.getPublicKeys()).willThrow(NetworkApiException.class); @@ -126,6 +126,6 @@ void getJwkKakaoApiCallingFailThrownExcetpion(){ } private OidcPublicKey mockPublicKey(String kid) { - return new OidcPublicKey(kid, "a", "u", "n", "e"); + return new OidcPublicKey(kid, "a", "u", "n", "e","."); } } diff --git a/jabiseo-infrastructure/src/main/java/com/jabiseo/client/oidc/GoogleOpenIdConfiguration.java b/jabiseo-infrastructure/src/main/java/com/jabiseo/client/oidc/GoogleOpenIdConfiguration.java index 6340b3ce..0f05217c 100644 --- a/jabiseo-infrastructure/src/main/java/com/jabiseo/client/oidc/GoogleOpenIdConfiguration.java +++ b/jabiseo-infrastructure/src/main/java/com/jabiseo/client/oidc/GoogleOpenIdConfiguration.java @@ -8,6 +8,7 @@ @Getter @Setter @NoArgsConstructor +@Builder @AllArgsConstructor @ToString public class GoogleOpenIdConfiguration { From 7d0fc1f15639fe5abaeee04d365bb003f5ad1a54 Mon Sep 17 00:00:00 2001 From: inhyeok Date: Mon, 29 Jul 2024 21:25:37 +0900 Subject: [PATCH 3/3] =?UTF-8?q?Chore(*):=20=EC=98=A4=ED=83=80=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20=EC=95=88=EC=93=B0=EB=8A=94=20=EB=9D=BC?= =?UTF-8?q?=EC=9D=B8=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jabiseo/auth/application/oidc/GoogleIdTokenValidator.java | 2 +- .../auth/application/oidc/GoogleIdTokenValidatorTest.java | 3 --- .../auth/application/oidc/KakaoIdTokenValidatorTest.java | 2 -- .../src/main/java/com/jabiseo/cache/RedisCacheRepository.java | 2 +- 4 files changed, 2 insertions(+), 7 deletions(-) diff --git a/jabiseo-api/src/main/java/com/jabiseo/auth/application/oidc/GoogleIdTokenValidator.java b/jabiseo-api/src/main/java/com/jabiseo/auth/application/oidc/GoogleIdTokenValidator.java index b81af308..d684217f 100644 --- a/jabiseo-api/src/main/java/com/jabiseo/auth/application/oidc/GoogleIdTokenValidator.java +++ b/jabiseo-api/src/main/java/com/jabiseo/auth/application/oidc/GoogleIdTokenValidator.java @@ -71,7 +71,7 @@ private List getOidcPublicKeysFromGoogle() { if (config == null) { ResponseEntity oidcConfiguration = googleAccountsClient.getOidcConfiguration(); config = oidcConfiguration.getBody(); - redisCacheRepository.saveOpenConfiguation(OPENID_CONFIGURATION_CACHE_KEY, config); + redisCacheRepository.saveOpenConfiguration(OPENID_CONFIGURATION_CACHE_KEY, config); } return googleOidcClient.getPublicKeys(config.getJwks_uri()); } catch (NetworkApiException e) { diff --git a/jabiseo-api/src/test/java/com/jabiseo/auth/application/oidc/GoogleIdTokenValidatorTest.java b/jabiseo-api/src/test/java/com/jabiseo/auth/application/oidc/GoogleIdTokenValidatorTest.java index d96fa9aa..309cf673 100644 --- a/jabiseo-api/src/test/java/com/jabiseo/auth/application/oidc/GoogleIdTokenValidatorTest.java +++ b/jabiseo-api/src/test/java/com/jabiseo/auth/application/oidc/GoogleIdTokenValidatorTest.java @@ -31,9 +31,6 @@ class GoogleIdTokenValidatorTest { @InjectMocks GoogleIdTokenValidator validator; - @Mock - IdTokenJwtHandler idTokenJwtHandler; - @Mock GoogleOidcProperty googleOidcProperty; diff --git a/jabiseo-api/src/test/java/com/jabiseo/auth/application/oidc/KakaoIdTokenValidatorTest.java b/jabiseo-api/src/test/java/com/jabiseo/auth/application/oidc/KakaoIdTokenValidatorTest.java index 23967b73..8a5a2077 100644 --- a/jabiseo-api/src/test/java/com/jabiseo/auth/application/oidc/KakaoIdTokenValidatorTest.java +++ b/jabiseo-api/src/test/java/com/jabiseo/auth/application/oidc/KakaoIdTokenValidatorTest.java @@ -38,8 +38,6 @@ class KakaoIdTokenValidatorTest { @Mock KakaoOidcProperty kakaoOidcProperty; - @Mock - IdTokenJwtHandler idTokenJwtHandler; @Mock KakaoKauthClient kakaoKauthClient; diff --git a/jabiseo-infrastructure/src/main/java/com/jabiseo/cache/RedisCacheRepository.java b/jabiseo-infrastructure/src/main/java/com/jabiseo/cache/RedisCacheRepository.java index f899df6c..cf64d9b3 100644 --- a/jabiseo-infrastructure/src/main/java/com/jabiseo/cache/RedisCacheRepository.java +++ b/jabiseo-infrastructure/src/main/java/com/jabiseo/cache/RedisCacheRepository.java @@ -72,7 +72,7 @@ public List getPublicKeys(String key) { } } - public void saveOpenConfiguation(String key, GoogleOpenIdConfiguration configuration) { + public void saveOpenConfiguration(String key, GoogleOpenIdConfiguration configuration) { try { String configDocs = mapper.writeValueAsString(configuration); operation.set(key, configDocs, 1, TimeUnit.DAYS);