From 1b3a68ca393d96f3b643fb7dcbff1bbfbc160fe2 Mon Sep 17 00:00:00 2001 From: KWY Date: Sat, 1 Jun 2024 11:52:00 +0900 Subject: [PATCH 01/13] =?UTF-8?q?#261=5FT-10856=20[feat]=20jwt=20util=20?= =?UTF-8?q?=EB=82=B4=20=ED=94=8C=EB=9E=AB=ED=8F=BC=20=EC=9D=B8=EA=B0=80?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit auth service 내에 있는 플랫폼 인가코드 발급 및 검증기능을 jwt token provider 내로 옮겼습니다. 테스트를 위해서 @value 어노테이션을 생성자로 받도록 수정했습니다 --- .../operation/jwt/JwtTokenProvider.java | 60 +++++++++++++++---- .../makers/operation/jwt/JwtTokenType.java | 2 +- 2 files changed, 50 insertions(+), 12 deletions(-) diff --git a/operation-auth/src/main/java/org/sopt/makers/operation/jwt/JwtTokenProvider.java b/operation-auth/src/main/java/org/sopt/makers/operation/jwt/JwtTokenProvider.java index ae249d01..6945a461 100644 --- a/operation-auth/src/main/java/org/sopt/makers/operation/jwt/JwtTokenProvider.java +++ b/operation-auth/src/main/java/org/sopt/makers/operation/jwt/JwtTokenProvider.java @@ -1,7 +1,5 @@ package org.sopt.makers.operation.jwt; -import static org.sopt.makers.operation.code.failure.TokenFailureCode.*; - import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Jwts; @@ -19,7 +17,6 @@ import javax.crypto.spec.SecretKeySpec; import javax.xml.bind.DatatypeConverter; - import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; import java.time.ZoneId; @@ -28,18 +25,42 @@ import java.util.HashMap; import java.util.Map; -@RequiredArgsConstructor +import static org.sopt.makers.operation.code.failure.TokenFailureCode.INVALID_TOKEN; + @Service public class JwtTokenProvider { - @Value("${spring.jwt.secretKey.access}") - private String accessSecretKey; + private final String accessSecretKey; + private final String refreshSecretKey; + private final String appAccessSecretKey; + private final String platformCodeSecretKey; + + public JwtTokenProvider( + @Value("${spring.jwt.secretKey.access}") String accessSecretKey, + @Value("${spring.jwt.secretKey.refresh}") String refreshSecretKey, + @Value("${spring.jwt.secretKey.app}") String appAccessSecretKey, + @Value("${spring.jwt.secretKey.platform_code}") String platformCodeSecretKey + ) { + this.accessSecretKey = accessSecretKey; + this.refreshSecretKey = refreshSecretKey; + this.appAccessSecretKey = appAccessSecretKey; + this.platformCodeSecretKey = platformCodeSecretKey; + } - @Value("${spring.jwt.secretKey.refresh}") - private String refreshSecretKey; + public String generatePlatformCode(final String clientId, final String redirectUri, final Long userId) { + val encodeKey = encodeKey(platformCodeSecretKey); + val secretKeyBytes = DatatypeConverter.parseBase64Binary(encodeKey); + val signingKey = new SecretKeySpec(secretKeyBytes, SignatureAlgorithm.HS256.getJcaName()); - @Value("${spring.jwt.secretKey.app}") - private String appAccessSecretKey; + return Jwts.builder() + .setHeader(createHeader()) + .setIssuer(clientId) + .setAudience(redirectUri) + .setSubject(Long.toString(userId)) + .setExpiration(createExpireDate(JwtTokenType.PLATFORM_CODE)) + .signWith(signingKey, SignatureAlgorithm.HS256) + .compact(); + } public String generateAccessToken(final Authentication authentication) { val encodedKey = encodeKey(accessSecretKey); @@ -76,9 +97,19 @@ public boolean validateTokenExpiration(String token, JwtTokenType jwtTokenType) } } + public boolean validatePlatformCode(String platformCode, String clientId, String redirectUri) { + try { + val claims = getClaimsFromToken(platformCode, JwtTokenType.PLATFORM_CODE); + return isClaimsMatchingRequest(claims, clientId, redirectUri); + } catch (ExpiredJwtException | SignatureException e) { + return false; + } + } + public AdminAuthentication getAuthentication(String token, JwtTokenType jwtTokenType) { return switch (jwtTokenType) { - case ACCESS_TOKEN, REFRESH_TOKEN -> new AdminAuthentication(getId(token, jwtTokenType), null, null); + case ACCESS_TOKEN, REFRESH_TOKEN, PLATFORM_CODE -> + new AdminAuthentication(getId(token, jwtTokenType), null, null); case APP_ACCESS_TOKEN -> new AdminAuthentication(getPlayGroundId(token, jwtTokenType), null, null); }; } @@ -103,6 +134,11 @@ public Long getId(String token, JwtTokenType jwtTokenType) { } } + private boolean isClaimsMatchingRequest(Claims claims, String clientId, String redirectUri) { + return claims.getAudience().equals(redirectUri) + && claims.getIssuer().equals(clientId); + } + private Claims getClaimsFromToken(String token, JwtTokenType jwtTokenType) { val encodedKey = encodeKey(setSecretKey(jwtTokenType)); @@ -131,6 +167,7 @@ private String setSecretKey(JwtTokenType jwtTokenType) { case ACCESS_TOKEN -> accessSecretKey; case REFRESH_TOKEN -> refreshSecretKey; case APP_ACCESS_TOKEN -> appAccessSecretKey; + case PLATFORM_CODE -> platformCodeSecretKey; }; } @@ -139,6 +176,7 @@ private LocalDateTime setExpireTime(LocalDateTime now, JwtTokenType jwtTokenType case ACCESS_TOKEN -> now.plusHours(5); case REFRESH_TOKEN -> now.plusWeeks(2); case APP_ACCESS_TOKEN -> throw new TokenException(INVALID_TOKEN); + case PLATFORM_CODE -> now.plusMinutes(5); }; } diff --git a/operation-auth/src/main/java/org/sopt/makers/operation/jwt/JwtTokenType.java b/operation-auth/src/main/java/org/sopt/makers/operation/jwt/JwtTokenType.java index 385ec839..2bb24ef4 100644 --- a/operation-auth/src/main/java/org/sopt/makers/operation/jwt/JwtTokenType.java +++ b/operation-auth/src/main/java/org/sopt/makers/operation/jwt/JwtTokenType.java @@ -1,5 +1,5 @@ package org.sopt.makers.operation.jwt; public enum JwtTokenType { - ACCESS_TOKEN, REFRESH_TOKEN, APP_ACCESS_TOKEN + ACCESS_TOKEN, REFRESH_TOKEN, APP_ACCESS_TOKEN, PLATFORM_CODE } From 4fffa66ce6a2de7f70f76b89c9b0e45a6d5fd167 Mon Sep 17 00:00:00 2001 From: KWY Date: Sat, 1 Jun 2024 11:52:42 +0900 Subject: [PATCH 02/13] =?UTF-8?q?#261=5FT-10856=20[test]=20jwt=20util=20?= =?UTF-8?q?=EB=82=B4=20=ED=94=8C=EB=9E=AB=ED=8F=BC=20=EC=9D=B8=EA=B0=80?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../operation/jwt/JwtTokenProviderTest.java | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 operation-api/src/test/java/org/sopt/makers/operation/jwt/JwtTokenProviderTest.java diff --git a/operation-api/src/test/java/org/sopt/makers/operation/jwt/JwtTokenProviderTest.java b/operation-api/src/test/java/org/sopt/makers/operation/jwt/JwtTokenProviderTest.java new file mode 100644 index 00000000..035b9cfd --- /dev/null +++ b/operation-api/src/test/java/org/sopt/makers/operation/jwt/JwtTokenProviderTest.java @@ -0,0 +1,99 @@ +package org.sopt.makers.operation.jwt; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import lombok.val; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +class JwtTokenProviderTest { + final String platformSecretKey = "123456789123456789123456789123456789123456789123456789"; + JwtTokenProvider jwtTokenProvider; + + @BeforeEach + void setUp() { + jwtTokenProvider = new JwtTokenProvider("", "", "", platformSecretKey); + } + + @Nested + @DisplayName("generatePlatformCode 메서드 테스트") + class GeneratePlatformCodeMethodTest { + + @DisplayName("iss:clientId, aud:redirectUri , sub:userId 인 jwt 토큰을 발급한다.") + @Test + void test() { + // given + val clientId = "clientId"; + val redirectUri = "redirectUri"; + val userId = 1L; + + // when + String platformCode = jwtTokenProvider.generatePlatformCode(clientId, redirectUri, userId); + val claims = getClaimsFromToken(platformCode); + + // then + assertThat(claims.getIssuer()).isEqualTo("clientId"); + assertThat(claims.getAudience()).isEqualTo("redirectUri"); + assertThat(claims.getSubject()).isEqualTo("1"); + } + + private Claims getClaimsFromToken(String token) { + val encodedKey = encodeKey(platformSecretKey); + + return Jwts.parserBuilder() + .setSigningKey(encodedKey) + .build() + .parseClaimsJws(token) + .getBody(); + } + + private String encodeKey(String secretKey) { + return Base64.getEncoder().encodeToString(secretKey.getBytes(StandardCharsets.UTF_8)); + } + } + + @Nested + @DisplayName("validatePlatformCode 메서드 테스트") + class ValidatePlatformCodeMethodTest { + + @DisplayName("iss:clientId, aud:redirectUri , sub:userId 인 jwt 토큰과 clientId, redirectUri 를 아규먼트로 넣으면 true 를 반환한다.") + @Test + void test() { + // given + val clientId = "clientId"; + val redirectUri = "redirectUri"; + val userId = 1L; + String platformCode = jwtTokenProvider.generatePlatformCode(clientId, redirectUri, userId); + + // when + val result = jwtTokenProvider.validatePlatformCode(platformCode, "clientId", "redirectUri"); + + // then + assertThat(result).isTrue(); + } + + @DisplayName("iss:clientId, aud:redirectUri , sub:userId 인 jwt 토큰과 notClientId, notRedirectUri 를 아규먼트로 넣으면 false 를 반환한다.") + @Test + void test2() { + // given + val clientId = "clientId"; + val redirectUri = "redirectUri"; + val userId = 1L; + String platformCode = jwtTokenProvider.generatePlatformCode(clientId, redirectUri, userId); + + // when + val result = jwtTokenProvider.validatePlatformCode(platformCode, "notClientId", "notRedirectUri"); + + // then + assertThat(result).isFalse(); + } + } + +} \ No newline at end of file From a46c49ab8ebb0c5f3b6ff98d21d7533e25bdc2b3 Mon Sep 17 00:00:00 2001 From: KWY Date: Sat, 1 Jun 2024 11:54:29 +0900 Subject: [PATCH 03/13] =?UTF-8?q?#261=5FT-10856=20[feat]=20jwt=20token=20p?= =?UTF-8?q?rovider=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../operation/auth/api/AuthApiController.java | 4 +- .../operation/auth/service/AuthService.java | 2 - .../auth/service/AuthServiceImpl.java | 29 ------------- .../auth/api/AuthApiControllerTest.java | 5 ++- .../auth/service/AuthServiceTest.java | 42 +------------------ 5 files changed, 8 insertions(+), 74 deletions(-) diff --git a/operation-api/src/main/java/org/sopt/makers/operation/auth/api/AuthApiController.java b/operation-api/src/main/java/org/sopt/makers/operation/auth/api/AuthApiController.java index 740b130b..a9ca3e14 100644 --- a/operation-api/src/main/java/org/sopt/makers/operation/auth/api/AuthApiController.java +++ b/operation-api/src/main/java/org/sopt/makers/operation/auth/api/AuthApiController.java @@ -6,6 +6,7 @@ import org.sopt.makers.operation.auth.service.AuthService; import org.sopt.makers.operation.dto.BaseResponse; import org.sopt.makers.operation.exception.AuthException; +import org.sopt.makers.operation.jwt.JwtTokenProvider; import org.sopt.makers.operation.user.domain.SocialType; import org.sopt.makers.operation.util.ApiResponseUtil; import org.springframework.http.ResponseEntity; @@ -25,6 +26,7 @@ public class AuthApiController implements AuthApi { private final ConcurrentHashMap tempPlatformCode = new ConcurrentHashMap<>(); private final AuthService authService; + private final JwtTokenProvider jwtTokenProvider; @Override @GetMapping("/api/v1/authorize") @@ -53,7 +55,7 @@ private Long findUserIdBySocialTypeAndCode(String type, String code) { } private String generatePlatformCode(String clientId, String redirectUri, Long userId) { - val platformCode = authService.generatePlatformCode(clientId, redirectUri, userId); + val platformCode = jwtTokenProvider.generatePlatformCode(clientId, redirectUri, userId); tempPlatformCode.putIfAbsent(platformCode, platformCode); return platformCode; } diff --git a/operation-api/src/main/java/org/sopt/makers/operation/auth/service/AuthService.java b/operation-api/src/main/java/org/sopt/makers/operation/auth/service/AuthService.java index aca12053..0500d42a 100644 --- a/operation-api/src/main/java/org/sopt/makers/operation/auth/service/AuthService.java +++ b/operation-api/src/main/java/org/sopt/makers/operation/auth/service/AuthService.java @@ -8,6 +8,4 @@ public interface AuthService { String getSocialUserInfo(SocialType type, String code); Long getUserId(SocialType socialType, String userSocialId); - - String generatePlatformCode(String clientId, String redirectUri, Long userId); } diff --git a/operation-api/src/main/java/org/sopt/makers/operation/auth/service/AuthServiceImpl.java b/operation-api/src/main/java/org/sopt/makers/operation/auth/service/AuthServiceImpl.java index 486e38aa..c5211228 100644 --- a/operation-api/src/main/java/org/sopt/makers/operation/auth/service/AuthServiceImpl.java +++ b/operation-api/src/main/java/org/sopt/makers/operation/auth/service/AuthServiceImpl.java @@ -1,22 +1,14 @@ package org.sopt.makers.operation.auth.service; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; -import jakarta.xml.bind.DatatypeConverter; import lombok.RequiredArgsConstructor; import lombok.val; import org.sopt.makers.operation.auth.repository.TeamOAuthInfoRepository; import org.sopt.makers.operation.client.social.SocialLoginManager; -import org.sopt.makers.operation.config.ValueConfig; import org.sopt.makers.operation.exception.AuthException; import org.sopt.makers.operation.user.domain.SocialType; import org.sopt.makers.operation.user.repository.identityinfo.UserIdentityInfoRepository; import org.springframework.stereotype.Service; -import javax.crypto.spec.SecretKeySpec; -import java.time.ZoneId; -import java.util.Date; - import static org.sopt.makers.operation.code.failure.auth.AuthFailureCode.INVALID_SOCIAL_CODE; import static org.sopt.makers.operation.code.failure.auth.AuthFailureCode.NOT_FOUND_USER_SOCIAL_IDENTITY_INFO; @@ -24,12 +16,9 @@ @RequiredArgsConstructor public class AuthServiceImpl implements AuthService { - private static final ZoneId KST = ZoneId.of("Asia/Seoul"); - private final SocialLoginManager socialLoginManager; private final TeamOAuthInfoRepository teamOAuthInfoRepository; private final UserIdentityInfoRepository userIdentityInfoRepository; - private final ValueConfig valueConfig; @Override public boolean checkRegisteredTeamOAuthInfo(String clientId, String redirectUri) { @@ -51,22 +40,4 @@ public Long getUserId(SocialType socialType, String userSocialId) { .orElseThrow(() -> new AuthException(NOT_FOUND_USER_SOCIAL_IDENTITY_INFO)); return userIdentityInfo.getUserId(); } - - @Override - public String generatePlatformCode(String clientId, String redirectUri, Long userId) { - val platformCodeSecretKey = valueConfig.getPlatformCodeSecretKey(); - - val signatureAlgorithm = SignatureAlgorithm.HS256; - val secretKeyBytes = DatatypeConverter.parseBase64Binary(platformCodeSecretKey); - val signingKey = new SecretKeySpec(secretKeyBytes, signatureAlgorithm.getJcaName()); - val exp = new Date().toInstant().atZone(KST) - .toLocalDateTime().plusMinutes(5).atZone(KST).toInstant(); - return Jwts.builder() - .setIssuer(clientId) - .setAudience(redirectUri) - .setSubject(Long.toString(userId)) - .setExpiration(Date.from(exp)) - .signWith(signingKey, signatureAlgorithm) - .compact(); - } } diff --git a/operation-api/src/test/java/org/sopt/makers/operation/auth/api/AuthApiControllerTest.java b/operation-api/src/test/java/org/sopt/makers/operation/auth/api/AuthApiControllerTest.java index 1706a681..8cbfb160 100644 --- a/operation-api/src/test/java/org/sopt/makers/operation/auth/api/AuthApiControllerTest.java +++ b/operation-api/src/test/java/org/sopt/makers/operation/auth/api/AuthApiControllerTest.java @@ -8,6 +8,7 @@ import org.junit.jupiter.params.provider.CsvSource; import org.sopt.makers.operation.auth.service.AuthService; import org.sopt.makers.operation.common.handler.ErrorHandler; +import org.sopt.makers.operation.jwt.JwtTokenProvider; import org.sopt.makers.operation.user.domain.SocialType; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; @@ -29,6 +30,8 @@ class AuthApiControllerTest { @MockBean AuthService authService; + @MockBean + JwtTokenProvider jwtTokenProvider; @Autowired MockMvc mockMvc; @Autowired @@ -49,7 +52,7 @@ void successTest(String type, String code, String clientId, String redirectUri) val socialType = SocialType.valueOf(type); given(authService.getSocialUserInfo(socialType, code)).willReturn("123"); given(authService.getUserId(socialType, "123")).willReturn(1L); - given(authService.generatePlatformCode(clientId, redirectUri, 1L)).willReturn("Platform Code"); + given(jwtTokenProvider.generatePlatformCode(clientId, redirectUri, 1L)).willReturn("Platform Code"); // when, then mockMvc.perform(get(uri) diff --git a/operation-api/src/test/java/org/sopt/makers/operation/auth/service/AuthServiceTest.java b/operation-api/src/test/java/org/sopt/makers/operation/auth/service/AuthServiceTest.java index 66e7292f..8e26991a 100644 --- a/operation-api/src/test/java/org/sopt/makers/operation/auth/service/AuthServiceTest.java +++ b/operation-api/src/test/java/org/sopt/makers/operation/auth/service/AuthServiceTest.java @@ -1,9 +1,5 @@ package org.sopt.makers.operation.auth.service; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.security.SignatureException; -import jakarta.xml.bind.DatatypeConverter; import lombok.val; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -14,14 +10,12 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.sopt.makers.operation.auth.repository.TeamOAuthInfoRepository; import org.sopt.makers.operation.client.social.SocialLoginManager; -import org.sopt.makers.operation.config.ValueConfig; import org.sopt.makers.operation.exception.AuthException; import org.sopt.makers.operation.user.domain.SocialType; import org.sopt.makers.operation.user.repository.identityinfo.UserIdentityInfoRepository; import java.util.Optional; -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; import static org.mockito.BDDMockito.given; @@ -33,14 +27,12 @@ class AuthServiceTest { TeamOAuthInfoRepository teamOAuthInfoRepository; @Mock UserIdentityInfoRepository userIdentityInfoRepository; - @Mock - ValueConfig valueConfig; AuthService authService; @BeforeEach void setUp() { - authService = new AuthServiceImpl(socialLoginManager, teamOAuthInfoRepository, userIdentityInfoRepository, valueConfig); + authService = new AuthServiceImpl(socialLoginManager, teamOAuthInfoRepository, userIdentityInfoRepository); } @Nested @@ -79,36 +71,4 @@ void test() { } } - @Nested - @DisplayName("generatePlatformCode 메서드 테스트") - class GeneratePlatformCodeMethodTest { - final String platformSecretKey = "123456789123456789123456789123456789123456789123456789"; - - @DisplayName("iss:clientId, aud:redirectUri , sub:userId 인 jwt 토큰을 발급한다.") - @Test - void test() { - // given - val clientId = "clientId"; - val redirectUri = "redirectUri"; - val userId = 1L; - given(valueConfig.getPlatformCodeSecretKey()).willReturn(platformSecretKey); - - // when - String platformCode = authService.generatePlatformCode(clientId, redirectUri, userId); - Claims claims = getClaimsFromToken(platformCode); - - // then - assertThat(claims.getIssuer()).isEqualTo("clientId"); - assertThat(claims.getAudience()).isEqualTo("redirectUri"); - assertThat(claims.getSubject()).isEqualTo("1"); - } - - private Claims getClaimsFromToken(String token) throws SignatureException { - return Jwts.parserBuilder() - .setSigningKey(DatatypeConverter.parseBase64Binary(platformSecretKey)) - .build() - .parseClaimsJws(token) - .getBody(); - } - } } \ No newline at end of file From a1b1fbb1cc2b356e9b8c6c826c54effe56d24dc9 Mon Sep 17 00:00:00 2001 From: KWY Date: Sun, 2 Jun 2024 00:29:01 +0900 Subject: [PATCH 04/13] =?UTF-8?q?#261=5FT-10856=20[fix]=20cocurrent=20hash?= =?UTF-8?q?=20map=20bean=20=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 테스트 코드 작성을 위해 concurrent hash map을 configuration 설정했다 --- .../operation/auth/api/AuthApiController.java | 2 +- .../common/config/ConcurrentHashMapConfig.java | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 operation-api/src/main/java/org/sopt/makers/operation/common/config/ConcurrentHashMapConfig.java diff --git a/operation-api/src/main/java/org/sopt/makers/operation/auth/api/AuthApiController.java b/operation-api/src/main/java/org/sopt/makers/operation/auth/api/AuthApiController.java index a9ca3e14..59064252 100644 --- a/operation-api/src/main/java/org/sopt/makers/operation/auth/api/AuthApiController.java +++ b/operation-api/src/main/java/org/sopt/makers/operation/auth/api/AuthApiController.java @@ -24,7 +24,7 @@ @RequiredArgsConstructor public class AuthApiController implements AuthApi { - private final ConcurrentHashMap tempPlatformCode = new ConcurrentHashMap<>(); + private final ConcurrentHashMap tempPlatformCode; private final AuthService authService; private final JwtTokenProvider jwtTokenProvider; diff --git a/operation-api/src/main/java/org/sopt/makers/operation/common/config/ConcurrentHashMapConfig.java b/operation-api/src/main/java/org/sopt/makers/operation/common/config/ConcurrentHashMapConfig.java new file mode 100644 index 00000000..d7cc9f6a --- /dev/null +++ b/operation-api/src/main/java/org/sopt/makers/operation/common/config/ConcurrentHashMapConfig.java @@ -0,0 +1,14 @@ +package org.sopt.makers.operation.common.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.concurrent.ConcurrentHashMap; + +@Configuration +public class ConcurrentHashMapConfig { + @Bean + public ConcurrentHashMap registerConcurrentHashMap() { + return new ConcurrentHashMap<>(); + } +} From ae174b63766ca63d25af90622b626d74afa81101 Mon Sep 17 00:00:00 2001 From: KWY Date: Sun, 2 Jun 2024 00:30:31 +0900 Subject: [PATCH 05/13] =?UTF-8?q?#261=5FT-10856=20[chore]=20controller=20t?= =?UTF-8?q?est=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit concurrent hash map mock bean 추가 uri -> authorizeUri로 변수명 변경 --- .../operation/auth/api/AuthApiControllerTest.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/operation-api/src/test/java/org/sopt/makers/operation/auth/api/AuthApiControllerTest.java b/operation-api/src/test/java/org/sopt/makers/operation/auth/api/AuthApiControllerTest.java index 8cbfb160..8fdfe241 100644 --- a/operation-api/src/test/java/org/sopt/makers/operation/auth/api/AuthApiControllerTest.java +++ b/operation-api/src/test/java/org/sopt/makers/operation/auth/api/AuthApiControllerTest.java @@ -19,6 +19,8 @@ import org.springframework.test.context.ContextConfiguration; import org.springframework.test.web.servlet.MockMvc; +import java.util.concurrent.ConcurrentHashMap; + import static org.mockito.BDDMockito.given; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -32,11 +34,13 @@ class AuthApiControllerTest { AuthService authService; @MockBean JwtTokenProvider jwtTokenProvider; + @MockBean + ConcurrentHashMap tempPlatformCode; @Autowired MockMvc mockMvc; @Autowired ObjectMapper objectMapper; - final String uri = "/api/v1/authorize"; + final String authorizeUri = "/api/v1/authorize"; @Nested @DisplayName("API 통신 성공 테스트") @@ -55,7 +59,7 @@ void successTest(String type, String code, String clientId, String redirectUri) given(jwtTokenProvider.generatePlatformCode(clientId, redirectUri, 1L)).willReturn("Platform Code"); // when, then - mockMvc.perform(get(uri) + mockMvc.perform(get(authorizeUri) .contentType(MediaType.APPLICATION_JSON) .param("type", type) .param("code", code) @@ -80,7 +84,7 @@ class QueryParameterValidateTest { }) void validateTest(String type, String code, String clientId, String redirectUri) throws Exception { // when, then - mockMvc.perform(get(uri) + mockMvc.perform(get(authorizeUri) .contentType(MediaType.APPLICATION_JSON) .param("type", type) .param("code", code) @@ -99,7 +103,7 @@ void validateTest2(String type, String code, String clientId, String redirectUri given(authService.checkRegisteredTeamOAuthInfo(clientId, redirectUri)).willReturn(false); // when, then - mockMvc.perform(get(uri) + mockMvc.perform(get(authorizeUri) .contentType(MediaType.APPLICATION_JSON) .param("type", type) .param("code", code) @@ -120,7 +124,7 @@ void validateTest3(String type, String code, String clientId, String redirectUri given(authService.checkRegisteredTeamOAuthInfo(clientId, redirectUri)).willReturn(true); // when, then - mockMvc.perform(get(uri) + mockMvc.perform(get(authorizeUri) .contentType(MediaType.APPLICATION_JSON) .param("type", type) .param("code", code) From 6ff62d7b707985cf0e928230aabee732ecd90fba Mon Sep 17 00:00:00 2001 From: KWY Date: Sun, 2 Jun 2024 01:02:06 +0900 Subject: [PATCH 06/13] =?UTF-8?q?#261=5FT-10856=20[feat]=20token=20api=20?= =?UTF-8?q?=EC=84=B1=EA=B3=B5=20=EB=B0=8F=20=EC=8B=A4=ED=8C=A8=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../operation/code/failure/auth/AuthFailureCode.java | 10 +++++++++- .../operation/code/success/auth/AuthSuccessCode.java | 3 ++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/operation-common/src/main/java/org/sopt/makers/operation/code/failure/auth/AuthFailureCode.java b/operation-common/src/main/java/org/sopt/makers/operation/code/failure/auth/AuthFailureCode.java index b519a879..b487a341 100644 --- a/operation-common/src/main/java/org/sopt/makers/operation/code/failure/auth/AuthFailureCode.java +++ b/operation-common/src/main/java/org/sopt/makers/operation/code/failure/auth/AuthFailureCode.java @@ -7,16 +7,24 @@ import static org.springframework.http.HttpStatus.BAD_REQUEST; import static org.springframework.http.HttpStatus.NOT_FOUND; +import static org.springframework.http.HttpStatus.UNAUTHORIZED; @Getter @RequiredArgsConstructor public enum AuthFailureCode implements FailureCode { // 400 - NOT_NULL_PARAMS(BAD_REQUEST, "쿼리 파라미터 중 데이터가 들어오지 않았습니다."), + NOT_NULL_GRANT_TYPE(BAD_REQUEST, "grantType 데이터가 들어오지 않았습니다."), INVALID_SOCIAL_TYPE(BAD_REQUEST, "유효하지 않은 social type 입니다."), INVALID_ID_TOKEN(BAD_REQUEST, "유효하지 않은 id token 입니다."), INVALID_SOCIAL_CODE(BAD_REQUEST, "유효하지 않은 social code 입니다."), FAILURE_READ_PRIVATE_KEY(BAD_REQUEST, "Private key 읽기 실패"), + INVALID_GRANT_TYPE(BAD_REQUEST, "유효하지 않은 grantType 입니다."), + NOT_NULL_CODE(BAD_REQUEST, "플랫폼 인가코드가 들어오지 않았습니다."), + USED_PLATFORM_CODE(BAD_REQUEST, "이미 사용한 플랫폼 인가코드입니다."), + NOT_NULL_REFRESH_TOKEN(BAD_REQUEST, "리프레쉬 토큰이 들어오지 않았습니다."), + // 401 + EXPIRED_PLATFORM_CODE(UNAUTHORIZED, "만료된 플랫폼 인가 코드입니다."), + EXPIRED_REFRESH_TOKEN(UNAUTHORIZED, "만료된 리프레쉬 토큰입니다."), // 404 NOT_FOUNT_REGISTERED_TEAM(NOT_FOUND, "등록되지 않은 팀입니다."), NOT_FOUND_USER_SOCIAL_IDENTITY_INFO(NOT_FOUND, "등록된 소셜 정보가 없습니다."), diff --git a/operation-common/src/main/java/org/sopt/makers/operation/code/success/auth/AuthSuccessCode.java b/operation-common/src/main/java/org/sopt/makers/operation/code/success/auth/AuthSuccessCode.java index d86b1081..9801b921 100644 --- a/operation-common/src/main/java/org/sopt/makers/operation/code/success/auth/AuthSuccessCode.java +++ b/operation-common/src/main/java/org/sopt/makers/operation/code/success/auth/AuthSuccessCode.java @@ -10,7 +10,8 @@ @Getter @RequiredArgsConstructor public enum AuthSuccessCode implements SuccessCode { - SUCCESS_GET_AUTHORIZATION_CODE(OK, "플랫폼 인가코드 발급 성공"); + SUCCESS_GET_AUTHORIZATION_CODE(OK, "플랫폼 인가코드 발급 성공"), + SUCCESS_GENERATE_TOKEN(OK, "토큰 발급 성공"); private final HttpStatus status; private final String message; } From 7a0c6496643bceccd3442bf28e498a774f916a4e Mon Sep 17 00:00:00 2001 From: KWY Date: Sun, 2 Jun 2024 01:02:39 +0900 Subject: [PATCH 07/13] =?UTF-8?q?#261=5FT-10856=20[feat]=20/token=20api=20?= =?UTF-8?q?=EC=8A=A4=ED=8E=99=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../makers/operation/auth/api/AuthApi.java | 34 +++++++++++++++++++ .../auth/dto/request/AccessTokenRequest.java | 21 ++++++++++++ .../auth/dto/response/TokenResponse.java | 9 +++++ 3 files changed, 64 insertions(+) create mode 100644 operation-api/src/main/java/org/sopt/makers/operation/auth/dto/request/AccessTokenRequest.java create mode 100644 operation-api/src/main/java/org/sopt/makers/operation/auth/dto/response/TokenResponse.java diff --git a/operation-api/src/main/java/org/sopt/makers/operation/auth/api/AuthApi.java b/operation-api/src/main/java/org/sopt/makers/operation/auth/api/AuthApi.java index ad898260..7ca616c9 100644 --- a/operation-api/src/main/java/org/sopt/makers/operation/auth/api/AuthApi.java +++ b/operation-api/src/main/java/org/sopt/makers/operation/auth/api/AuthApi.java @@ -3,6 +3,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import org.sopt.makers.operation.auth.dto.request.AccessTokenRequest; import org.sopt.makers.operation.dto.BaseResponse; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RequestParam; @@ -45,4 +46,37 @@ ResponseEntity> authorize( @RequestParam String clientId, @RequestParam String redirectUri ); + + @Operation( + security = @SecurityRequirement(name = "Authorization"), + summary = "인증 토큰 발급 API", + responses = { + @ApiResponse( + responseCode = "200", + description = "토큰 발급 성공" + ), + @ApiResponse( + responseCode = "400", + description = """ + 1. grantType 데이터가 들어오지 않았습니다.\n + 2. 유효하지 않은 grantType 입니다.\n + 3. 플랫폼 인가코드가 들어오지 않았습니다.\n + 4. 이미 사용한 플랫폼 인가코드입니다.\n + 5. 리프레쉬 토큰이 들어오지 않았습니다. + """ + ), + @ApiResponse( + responseCode = "401", + description = """ + 1. 만료된 플랫폼 인가 코드입니다.\n + 2. 만료된 리프레쉬 토큰입니다. + """ + ), + @ApiResponse( + responseCode = "500", + description = "서버 내부 오류" + ) + } + ) + ResponseEntity> token(AccessTokenRequest accessTokenRequest); } diff --git a/operation-api/src/main/java/org/sopt/makers/operation/auth/dto/request/AccessTokenRequest.java b/operation-api/src/main/java/org/sopt/makers/operation/auth/dto/request/AccessTokenRequest.java new file mode 100644 index 00000000..045b4365 --- /dev/null +++ b/operation-api/src/main/java/org/sopt/makers/operation/auth/dto/request/AccessTokenRequest.java @@ -0,0 +1,21 @@ +package org.sopt.makers.operation.auth.dto.request; + +public record AccessTokenRequest( + String grantType, + String clientId, + String redirectUri, + String code, + String refreshToken +) { + public boolean isNullGrantType() { + return grantType == null; + } + + public boolean isNullCode() { + return code == null; + } + + public boolean isNullRefreshToken() { + return refreshToken == null; + } +} diff --git a/operation-api/src/main/java/org/sopt/makers/operation/auth/dto/response/TokenResponse.java b/operation-api/src/main/java/org/sopt/makers/operation/auth/dto/response/TokenResponse.java new file mode 100644 index 00000000..faeb3436 --- /dev/null +++ b/operation-api/src/main/java/org/sopt/makers/operation/auth/dto/response/TokenResponse.java @@ -0,0 +1,9 @@ +package org.sopt.makers.operation.auth.dto.response; + +public record TokenResponse(String tokenType, String accessToken, String refreshToken) { + private static final String BEARER_TOKEN_TYPE = "Bearer"; + + public static TokenResponse of(String accessToken, String refreshToken) { + return new TokenResponse(BEARER_TOKEN_TYPE, accessToken, refreshToken); + } +} From a30c861807eefbb403259d9cca8d5b3b8229d495 Mon Sep 17 00:00:00 2001 From: KWY Date: Sun, 2 Jun 2024 01:03:06 +0900 Subject: [PATCH 08/13] =?UTF-8?q?#261=5FT-10856=20[feat]=20/token=20api=20?= =?UTF-8?q?=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../operation/auth/api/AuthApiController.java | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/operation-api/src/main/java/org/sopt/makers/operation/auth/api/AuthApiController.java b/operation-api/src/main/java/org/sopt/makers/operation/auth/api/AuthApiController.java index 59064252..d04a752e 100644 --- a/operation-api/src/main/java/org/sopt/makers/operation/auth/api/AuthApiController.java +++ b/operation-api/src/main/java/org/sopt/makers/operation/auth/api/AuthApiController.java @@ -2,28 +2,45 @@ import lombok.RequiredArgsConstructor; import lombok.val; +import org.sopt.makers.operation.auth.dto.request.AccessTokenRequest; import org.sopt.makers.operation.auth.dto.response.AuthorizationCodeResponse; +import org.sopt.makers.operation.auth.dto.response.TokenResponse; import org.sopt.makers.operation.auth.service.AuthService; import org.sopt.makers.operation.dto.BaseResponse; import org.sopt.makers.operation.exception.AuthException; import org.sopt.makers.operation.jwt.JwtTokenProvider; import org.sopt.makers.operation.user.domain.SocialType; import org.sopt.makers.operation.util.ApiResponseUtil; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.concurrent.ConcurrentHashMap; +import static org.sopt.makers.operation.code.failure.auth.AuthFailureCode.EXPIRED_PLATFORM_CODE; +import static org.sopt.makers.operation.code.failure.auth.AuthFailureCode.EXPIRED_REFRESH_TOKEN; +import static org.sopt.makers.operation.code.failure.auth.AuthFailureCode.INVALID_GRANT_TYPE; import static org.sopt.makers.operation.code.failure.auth.AuthFailureCode.INVALID_SOCIAL_TYPE; import static org.sopt.makers.operation.code.failure.auth.AuthFailureCode.NOT_FOUNT_REGISTERED_TEAM; +import static org.sopt.makers.operation.code.failure.auth.AuthFailureCode.NOT_NULL_CODE; +import static org.sopt.makers.operation.code.failure.auth.AuthFailureCode.NOT_NULL_GRANT_TYPE; +import static org.sopt.makers.operation.code.failure.auth.AuthFailureCode.USED_PLATFORM_CODE; +import static org.sopt.makers.operation.code.success.auth.AuthSuccessCode.SUCCESS_GENERATE_TOKEN; import static org.sopt.makers.operation.code.success.auth.AuthSuccessCode.SUCCESS_GET_AUTHORIZATION_CODE; +import static org.sopt.makers.operation.jwt.JwtTokenType.PLATFORM_CODE; +import static org.sopt.makers.operation.jwt.JwtTokenType.REFRESH_TOKEN; @RestController @RequiredArgsConstructor public class AuthApiController implements AuthApi { + private static final String AUTHORIZATION_CODE_GRANT_TYPE = "authorizationCode"; + private static final String REFRESH_TOKEN_GRANT_TYPE = "refreshToken"; + private final ConcurrentHashMap tempPlatformCode; private final AuthService authService; private final JwtTokenProvider jwtTokenProvider; @@ -48,6 +65,60 @@ public ResponseEntity> authorize( return ApiResponseUtil.success(SUCCESS_GET_AUTHORIZATION_CODE, new AuthorizationCodeResponse(platformCode)); } + @Override + @PostMapping( + path = "/api/v1/token", + consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE + ) + public ResponseEntity> token(AccessTokenRequest accessTokenRequest) { + if (accessTokenRequest.isNullGrantType()) { + throw new AuthException(NOT_NULL_GRANT_TYPE); + } + + val grantType = accessTokenRequest.grantType(); + if (!(grantType.equals(AUTHORIZATION_CODE_GRANT_TYPE) || grantType.equals(REFRESH_TOKEN_GRANT_TYPE))) { + throw new AuthException(INVALID_GRANT_TYPE); + } + + val tokenResponse = grantType.equals(AUTHORIZATION_CODE_GRANT_TYPE) ? + generateTokenResponseByAuthorizationCode(accessTokenRequest) : generateTokenResponseByRefreshToken(accessTokenRequest); + return ApiResponseUtil.success(SUCCESS_GENERATE_TOKEN, tokenResponse); + } + + private TokenResponse generateTokenResponseByAuthorizationCode(AccessTokenRequest accessTokenRequest) { + if (accessTokenRequest.isNullCode()) { + throw new AuthException(NOT_NULL_CODE); + } + if (!tempPlatformCode.contains(accessTokenRequest.code())) { + throw new AuthException(USED_PLATFORM_CODE); + } + if (!jwtTokenProvider.validatePlatformCode(accessTokenRequest.code(), accessTokenRequest.clientId(), accessTokenRequest.redirectUri())) { + throw new AuthException(EXPIRED_PLATFORM_CODE); + } + + val authentication = jwtTokenProvider.getAuthentication(accessTokenRequest.code(), PLATFORM_CODE); + tempPlatformCode.remove(accessTokenRequest.code()); + return generateTokenResponse(authentication); + } + + private TokenResponse generateTokenResponseByRefreshToken(AccessTokenRequest accessTokenRequest) { + if (accessTokenRequest.isNullRefreshToken()) { + throw new AuthException(USED_PLATFORM_CODE); + } + if (!jwtTokenProvider.validateTokenExpiration(accessTokenRequest.refreshToken(), REFRESH_TOKEN)) { + throw new AuthException(EXPIRED_REFRESH_TOKEN); + } + + val authentication = jwtTokenProvider.getAuthentication(accessTokenRequest.refreshToken(), REFRESH_TOKEN); + return generateTokenResponse(authentication); + } + + private TokenResponse generateTokenResponse(Authentication authentication) { + val accessToken = jwtTokenProvider.generateAccessToken(authentication); + val refreshToken = jwtTokenProvider.generateRefreshToken(authentication); + return TokenResponse.of(accessToken, refreshToken); + } + private Long findUserIdBySocialTypeAndCode(String type, String code) { val socialType = SocialType.valueOf(type); val userSocialId = authService.getSocialUserInfo(socialType, code); From a08d8898933a2f7cecabea26578dff583655875a Mon Sep 17 00:00:00 2001 From: KWY Date: Sun, 2 Jun 2024 01:09:10 +0900 Subject: [PATCH 09/13] =?UTF-8?q?#261=5FT-10856=20[test]=20/token=20api=20?= =?UTF-8?q?=EC=84=B1=EA=B3=B5=20=EC=BC=80=EC=9D=B4=EC=8A=A4=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=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 --- .../auth/api/AuthApiControllerTest.java | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/operation-api/src/test/java/org/sopt/makers/operation/auth/api/AuthApiControllerTest.java b/operation-api/src/test/java/org/sopt/makers/operation/auth/api/AuthApiControllerTest.java index 8fdfe241..a76890c1 100644 --- a/operation-api/src/test/java/org/sopt/makers/operation/auth/api/AuthApiControllerTest.java +++ b/operation-api/src/test/java/org/sopt/makers/operation/auth/api/AuthApiControllerTest.java @@ -7,6 +7,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import org.sopt.makers.operation.auth.service.AuthService; +import org.sopt.makers.operation.authentication.AdminAuthentication; import org.sopt.makers.operation.common.handler.ErrorHandler; import org.sopt.makers.operation.jwt.JwtTokenProvider; import org.sopt.makers.operation.user.domain.SocialType; @@ -22,7 +23,10 @@ import java.util.concurrent.ConcurrentHashMap; import static org.mockito.BDDMockito.given; +import static org.sopt.makers.operation.jwt.JwtTokenType.PLATFORM_CODE; +import static org.sopt.makers.operation.jwt.JwtTokenType.REFRESH_TOKEN; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -41,6 +45,7 @@ class AuthApiControllerTest { @Autowired ObjectMapper objectMapper; final String authorizeUri = "/api/v1/authorize"; + final String tokenUri = "/api/v1/token"; @Nested @DisplayName("API 통신 성공 테스트") @@ -68,6 +73,60 @@ void successTest(String type, String code, String clientId, String redirectUri) .andExpect(status().isOk()) .andExpect(jsonPath("$.message").value("플랫폼 인가코드 발급 성공")); } + + @DisplayName("grantType 이 authorizationCode 이고, 유효한 clientId, redirectUri, code 값이 들어왔을 때, 액세스 토큰과 리프레시 토큰을 발급한다.") + @ParameterizedTest + @CsvSource({ + "authorizationCode,clientId,redirectUri,code" + }) + void tokenByAuthorizationCodeSuccessTest(String grantType, String clientId, String redirectUri, String code) throws Exception { + // given + val authentication = new AdminAuthentication(1L, null, null); + given(jwtTokenProvider.validatePlatformCode(code, clientId, redirectUri)).willReturn(true); + given(jwtTokenProvider.getAuthentication(code, PLATFORM_CODE)).willReturn(authentication); + given(jwtTokenProvider.generateAccessToken(authentication)).willReturn("access token"); + given(jwtTokenProvider.generateRefreshToken(authentication)).willReturn("refresh token"); + given(tempPlatformCode.contains(code)).willReturn(true); + + // when, then + mockMvc.perform(post(tokenUri) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param("grantType", grantType) + .param("clientId", clientId) + .param("redirectUri", redirectUri) + .param("code", code)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value("토큰 발급 성공")) + .andExpect(jsonPath("$.data.tokenType").value("Bearer")) + .andExpect(jsonPath("$.data.accessToken").value("access token")) + .andExpect(jsonPath("$.data.refreshToken").value("refresh token")); + } + + @DisplayName("grantType 이 refreshToken 이고, 유효한 refreshToken 값이 들어왔을 때, 액세스 토큰과 리프레시 토큰을 발급한다.") + @ParameterizedTest + @CsvSource({ + "refreshToken,refreshToken" + }) + void tokenByRefreshTokenSuccessTest(String grantType, String refreshToken) throws Exception { + // given + val authentication = new AdminAuthentication(1L, null, null); + given(jwtTokenProvider.validateTokenExpiration(refreshToken, REFRESH_TOKEN)).willReturn(true); + given(jwtTokenProvider.getAuthentication(refreshToken, REFRESH_TOKEN)).willReturn(authentication); + given(jwtTokenProvider.generateAccessToken(authentication)).willReturn("access token"); + given(jwtTokenProvider.generateRefreshToken(authentication)).willReturn("refresh token"); + + // when, then + mockMvc.perform(post(tokenUri) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param("grantType", grantType) + .param("refreshToken", refreshToken) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value("토큰 발급 성공")) + .andExpect(jsonPath("$.data.tokenType").value("Bearer")) + .andExpect(jsonPath("$.data.accessToken").value("access token")) + .andExpect(jsonPath("$.data.refreshToken").value("refresh token")); + } } @Nested From 8b566813dd4234c86598b9e3c3a75b70d106bed4 Mon Sep 17 00:00:00 2001 From: KWY Date: Sun, 2 Jun 2024 01:12:46 +0900 Subject: [PATCH 10/13] =?UTF-8?q?#261=5FT-10856=20[chore]=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EB=B3=80=EC=88=98?= =?UTF-8?q?=EB=AA=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sopt/makers/operation/auth/api/AuthApiControllerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/operation-api/src/test/java/org/sopt/makers/operation/auth/api/AuthApiControllerTest.java b/operation-api/src/test/java/org/sopt/makers/operation/auth/api/AuthApiControllerTest.java index a76890c1..9298d3b7 100644 --- a/operation-api/src/test/java/org/sopt/makers/operation/auth/api/AuthApiControllerTest.java +++ b/operation-api/src/test/java/org/sopt/makers/operation/auth/api/AuthApiControllerTest.java @@ -130,7 +130,7 @@ void tokenByRefreshTokenSuccessTest(String grantType, String refreshToken) throw } @Nested - @DisplayName("쿼리 파라미터 유효성 검사 테스트") + @DisplayName("/authorize API 쿼리 파라미터 유효성 검사 테스트") class QueryParameterValidateTest { @DisplayName("type, code, clientId, redirectUri 중 하나라도 null 이 들어오면 400을 반환한다.") From 8a3baa428175b82365a46bd18af83de0325cb14c Mon Sep 17 00:00:00 2001 From: KWY Date: Sun, 2 Jun 2024 01:36:17 +0900 Subject: [PATCH 11/13] =?UTF-8?q?#261=5FT-10856=20[fix]=20=EC=8B=A4?= =?UTF-8?q?=ED=8C=A8=20=EB=A9=94=EC=8B=9C=EC=A7=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/sopt/makers/operation/auth/api/AuthApiController.java | 3 ++- .../makers/operation/code/failure/auth/AuthFailureCode.java | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/operation-api/src/main/java/org/sopt/makers/operation/auth/api/AuthApiController.java b/operation-api/src/main/java/org/sopt/makers/operation/auth/api/AuthApiController.java index d04a752e..828251c4 100644 --- a/operation-api/src/main/java/org/sopt/makers/operation/auth/api/AuthApiController.java +++ b/operation-api/src/main/java/org/sopt/makers/operation/auth/api/AuthApiController.java @@ -28,6 +28,7 @@ import static org.sopt.makers.operation.code.failure.auth.AuthFailureCode.NOT_FOUNT_REGISTERED_TEAM; import static org.sopt.makers.operation.code.failure.auth.AuthFailureCode.NOT_NULL_CODE; import static org.sopt.makers.operation.code.failure.auth.AuthFailureCode.NOT_NULL_GRANT_TYPE; +import static org.sopt.makers.operation.code.failure.auth.AuthFailureCode.NOT_NULL_REFRESH_TOKEN; import static org.sopt.makers.operation.code.failure.auth.AuthFailureCode.USED_PLATFORM_CODE; import static org.sopt.makers.operation.code.success.auth.AuthSuccessCode.SUCCESS_GENERATE_TOKEN; import static org.sopt.makers.operation.code.success.auth.AuthSuccessCode.SUCCESS_GET_AUTHORIZATION_CODE; @@ -103,7 +104,7 @@ private TokenResponse generateTokenResponseByAuthorizationCode(AccessTokenReques private TokenResponse generateTokenResponseByRefreshToken(AccessTokenRequest accessTokenRequest) { if (accessTokenRequest.isNullRefreshToken()) { - throw new AuthException(USED_PLATFORM_CODE); + throw new AuthException(NOT_NULL_REFRESH_TOKEN); } if (!jwtTokenProvider.validateTokenExpiration(accessTokenRequest.refreshToken(), REFRESH_TOKEN)) { throw new AuthException(EXPIRED_REFRESH_TOKEN); diff --git a/operation-common/src/main/java/org/sopt/makers/operation/code/failure/auth/AuthFailureCode.java b/operation-common/src/main/java/org/sopt/makers/operation/code/failure/auth/AuthFailureCode.java index b487a341..d1cf4c1a 100644 --- a/operation-common/src/main/java/org/sopt/makers/operation/code/failure/auth/AuthFailureCode.java +++ b/operation-common/src/main/java/org/sopt/makers/operation/code/failure/auth/AuthFailureCode.java @@ -23,7 +23,7 @@ public enum AuthFailureCode implements FailureCode { USED_PLATFORM_CODE(BAD_REQUEST, "이미 사용한 플랫폼 인가코드입니다."), NOT_NULL_REFRESH_TOKEN(BAD_REQUEST, "리프레쉬 토큰이 들어오지 않았습니다."), // 401 - EXPIRED_PLATFORM_CODE(UNAUTHORIZED, "만료된 플랫폼 인가 코드입니다."), + EXPIRED_PLATFORM_CODE(UNAUTHORIZED, "만료된 플랫폼 인가코드입니다."), EXPIRED_REFRESH_TOKEN(UNAUTHORIZED, "만료된 리프레쉬 토큰입니다."), // 404 NOT_FOUNT_REGISTERED_TEAM(NOT_FOUND, "등록되지 않은 팀입니다."), From 1390ea275d76af87b437f9ad938787cfa6cbbe2b Mon Sep 17 00:00:00 2001 From: KWY Date: Sun, 2 Jun 2024 01:38:52 +0900 Subject: [PATCH 12/13] =?UTF-8?q?#261=5FT-10856=20[fix]=20hash=20map=20?= =?UTF-8?q?=EB=82=B4=20=EC=9D=B8=EA=B0=80=EC=BD=94=EB=93=9C=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 만료된 플랫폼 인가코드가 map에 존재하는 것을 방지한다 --- .../org/sopt/makers/operation/auth/api/AuthApiController.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/operation-api/src/main/java/org/sopt/makers/operation/auth/api/AuthApiController.java b/operation-api/src/main/java/org/sopt/makers/operation/auth/api/AuthApiController.java index 828251c4..f1e5aa21 100644 --- a/operation-api/src/main/java/org/sopt/makers/operation/auth/api/AuthApiController.java +++ b/operation-api/src/main/java/org/sopt/makers/operation/auth/api/AuthApiController.java @@ -93,12 +93,13 @@ private TokenResponse generateTokenResponseByAuthorizationCode(AccessTokenReques if (!tempPlatformCode.contains(accessTokenRequest.code())) { throw new AuthException(USED_PLATFORM_CODE); } + tempPlatformCode.remove(accessTokenRequest.code()); + if (!jwtTokenProvider.validatePlatformCode(accessTokenRequest.code(), accessTokenRequest.clientId(), accessTokenRequest.redirectUri())) { throw new AuthException(EXPIRED_PLATFORM_CODE); } val authentication = jwtTokenProvider.getAuthentication(accessTokenRequest.code(), PLATFORM_CODE); - tempPlatformCode.remove(accessTokenRequest.code()); return generateTokenResponse(authentication); } From dea7924119b2e7ec17f0eb3ffc5e26a1d314111a Mon Sep 17 00:00:00 2001 From: KWY Date: Sun, 2 Jun 2024 01:39:07 +0900 Subject: [PATCH 13/13] =?UTF-8?q?#261=5FT-10856=20[test]=20/token=20api=20?= =?UTF-8?q?=EC=8B=A4=ED=8C=A8=20=EC=BC=80=EC=9D=B4=EC=8A=A4=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=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 --- .../auth/api/AuthApiControllerTest.java | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) diff --git a/operation-api/src/test/java/org/sopt/makers/operation/auth/api/AuthApiControllerTest.java b/operation-api/src/test/java/org/sopt/makers/operation/auth/api/AuthApiControllerTest.java index 9298d3b7..64ba7938 100644 --- a/operation-api/src/test/java/org/sopt/makers/operation/auth/api/AuthApiControllerTest.java +++ b/operation-api/src/test/java/org/sopt/makers/operation/auth/api/AuthApiControllerTest.java @@ -193,4 +193,130 @@ void validateTest3(String type, String code, String clientId, String redirectUri .andExpect(jsonPath("$.message").value("유효하지 않은 social type 입니다.")); } } + + @Nested + @DisplayName("/token API 오류 케이스 테스트") + class TokenAPIErrorCaseTest { + + @DisplayName("grantType 이 null 일 때 400 에러를 반환한다.") + @ParameterizedTest + @CsvSource({ + ",clientId,redirectUri,code" + }) + void errorTest(String grantType, String clientId, String redirectUri, String code) throws Exception { + // when, then + mockMvc.perform(post(tokenUri) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param("grantType", grantType) + .param("clientId", clientId) + .param("redirectUri", redirectUri) + .param("code", code)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value("grantType 데이터가 들어오지 않았습니다.")); + } + + @DisplayName("grantType 이 authorizationCode 또는 refreshToken 외의 값이 들어온다면 400 에러를 반환한다.") + @ParameterizedTest + @CsvSource({ + "error,clientId,redirectUri,code" + }) + void errorTest2(String grantType, String clientId, String redirectUri, String code) throws Exception { + // when, then + mockMvc.perform(post(tokenUri) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param("grantType", grantType) + .param("clientId", clientId) + .param("redirectUri", redirectUri) + .param("code", code)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value("유효하지 않은 grantType 입니다.")); + } + + @DisplayName("grantType 이 authorizationCode 일 때, code 값으로 null 값이 들어온다면 400 에러를 반환한다.") + @ParameterizedTest + @CsvSource({ + "authorizationCode,clientId,redirectUri," + }) + void errorTest3(String grantType, String clientId, String redirectUri, String code) throws Exception { + // when, then + mockMvc.perform(post(tokenUri) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param("grantType", grantType) + .param("clientId", clientId) + .param("redirectUri", redirectUri) + .param("code", code)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value("플랫폼 인가코드가 들어오지 않았습니다.")); + } + + @DisplayName("grantType 이 authorizationCode 일 때, code 값이 hashmap인 tempPlatformCode 내에 없다면 400 에러를 반환한다.") + @ParameterizedTest + @CsvSource({ + "authorizationCode,clientId,redirectUri,code" + }) + void errorTest4(String grantType, String clientId, String redirectUri, String code) throws Exception { + // given + given(tempPlatformCode.contains(code)).willReturn(false); + + // when, then + mockMvc.perform(post(tokenUri) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param("grantType", grantType) + .param("clientId", clientId) + .param("redirectUri", redirectUri) + .param("code", code)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value("이미 사용한 플랫폼 인가코드입니다.")); + } + + @DisplayName("grantType 이 refreshToken 일 때, refreshToken 값으로 null 값이 들어온다면 400 에러를 반환한다.") + @ParameterizedTest + @CsvSource({ + "refreshToken," + }) + void errorTest5(String grantType, String refreshToken) throws Exception { + // when, then + mockMvc.perform(post(tokenUri) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param("grantType", grantType) + .param("refreshToken", refreshToken)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value("리프레쉬 토큰이 들어오지 않았습니다.")); + } + + @DisplayName("grantType 이 authorizationCode 일 때, code 값이 만료되었다면 401 에러를 반환한다.") + @ParameterizedTest + @CsvSource({ + "authorizationCode,clientId,redirectUri,code" + }) + void errorTest6(String grantType, String clientId, String redirectUri, String code) throws Exception { + given(jwtTokenProvider.validatePlatformCode(code, clientId, redirectUri)).willReturn(false); + given(tempPlatformCode.contains(code)).willReturn(true); + // when, then + mockMvc.perform(post(tokenUri) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param("grantType", grantType) + .param("clientId", clientId) + .param("redirectUri", redirectUri) + .param("code", code)) + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.message").value("만료된 플랫폼 인가코드입니다.")); + } + + @DisplayName("grantType 이 refreshToken 일 때, refreshToken 값이 만료되었다면 401 에러를 반환한다.") + @ParameterizedTest + @CsvSource({ + "refreshToken,refreshToken" + }) + void errorTest7(String grantType, String refreshToken) throws Exception { + given(jwtTokenProvider.validateTokenExpiration(refreshToken, REFRESH_TOKEN)).willReturn(false); + // when, then + mockMvc.perform(post(tokenUri) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param("grantType", grantType) + .param("refreshToken", refreshToken)) + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.message").value("만료된 리프레쉬 토큰입니다.")); + } + } } \ No newline at end of file