diff --git a/src/main/java/sopt/org/hmh/domain/user/controller/UserController.java b/src/main/java/sopt/org/hmh/domain/user/controller/UserController.java index 808fbbd7..e7359a7b 100644 --- a/src/main/java/sopt/org/hmh/domain/user/controller/UserController.java +++ b/src/main/java/sopt/org/hmh/domain/user/controller/UserController.java @@ -5,6 +5,7 @@ import org.springframework.web.bind.annotation.DeleteMapping; 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.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; @@ -14,6 +15,7 @@ import sopt.org.hmh.domain.user.dto.request.SocialSignUpRequest; import sopt.org.hmh.domain.user.service.UserService; import sopt.org.hmh.global.auth.UserId; +import sopt.org.hmh.global.auth.social.SocialAccessTokenResponse; import sopt.org.hmh.global.common.response.BaseResponse; import sopt.org.hmh.global.common.response.EmptyJsonResponse; @@ -90,4 +92,13 @@ public ResponseEntity> orderWithdraw(@UserId final Long userId) .status(UserSuccess.WITHDRAW_SUCCESS.getHttpStatus()) .body(BaseResponse.success(UserSuccess.WITHDRAW_SUCCESS, new EmptyJsonResponse())); } + + @GetMapping("/social/token/kakao") + public ResponseEntity> orderGetKakaoAccessToken( + @RequestParam("code") final String code + ) { + return ResponseEntity + .status(UserSuccess.GET_SOCIAL_ACCESS_TOKEN_SUCCESS.getHttpStatus()) + .body(BaseResponse.success(UserSuccess.GET_SOCIAL_ACCESS_TOKEN_SUCCESS, userService.getSocialAccessTokenByAuthorizationCode(code))); + } } \ No newline at end of file diff --git a/src/main/java/sopt/org/hmh/domain/user/domain/exception/UserSuccess.java b/src/main/java/sopt/org/hmh/domain/user/domain/exception/UserSuccess.java index c5407ac7..cae47ddb 100644 --- a/src/main/java/sopt/org/hmh/domain/user/domain/exception/UserSuccess.java +++ b/src/main/java/sopt/org/hmh/domain/user/domain/exception/UserSuccess.java @@ -14,6 +14,7 @@ public enum UserSuccess implements SuccessBase { GET_USER_INFO_SUCCESS(HttpStatus.OK, "유저의 정보를 불러오는데에 성공했습니다."), GET_USER_POINT_SUCCESS(HttpStatus.OK, "유저의 포인트 정보를 불러오는데에 성공했습니다."), WITHDRAW_SUCCESS(HttpStatus.OK, "회원 탈퇴를 성공하였습니다."), + GET_SOCIAL_ACCESS_TOKEN_SUCCESS(HttpStatus.OK, "소셜 액세스 토큰 발급을 성공했습니다."), // 201 CREATED SIGNUP_SUCCESS(HttpStatus.CREATED, "회원 가입에 성공했습니다."), diff --git a/src/main/java/sopt/org/hmh/domain/user/service/UserService.java b/src/main/java/sopt/org/hmh/domain/user/service/UserService.java index 6e0b931d..f38e8c48 100644 --- a/src/main/java/sopt/org/hmh/domain/user/service/UserService.java +++ b/src/main/java/sopt/org/hmh/domain/user/service/UserService.java @@ -28,6 +28,7 @@ import sopt.org.hmh.global.auth.jwt.exception.JwtException; import sopt.org.hmh.global.auth.redis.TokenService; import sopt.org.hmh.global.auth.social.SocialPlatform; +import sopt.org.hmh.global.auth.social.SocialAccessTokenResponse; import sopt.org.hmh.global.auth.social.apple.fegin.AppleOAuthProvider; import sopt.org.hmh.global.auth.social.kakao.fegin.KakaoLoginService; @@ -197,4 +198,9 @@ public void deleteExpiredUser(LocalDateTime currentDate) { userRepository.deleteAllById(expiredUserList); challengeService.deleteChallengeRelatedByUserId(expiredUserList); } + + public SocialAccessTokenResponse getSocialAccessTokenByAuthorizationCode(String code) { + return kakaoLoginService.getKakaoAccessToken(code); + } + } \ No newline at end of file diff --git a/src/main/java/sopt/org/hmh/global/auth/social/SocialAccessTokenResponse.java b/src/main/java/sopt/org/hmh/global/auth/social/SocialAccessTokenResponse.java new file mode 100644 index 00000000..c5fbe187 --- /dev/null +++ b/src/main/java/sopt/org/hmh/global/auth/social/SocialAccessTokenResponse.java @@ -0,0 +1,9 @@ +package sopt.org.hmh.global.auth.social; + +import lombok.Builder; + +@Builder +public record SocialAccessTokenResponse( + String socialAccessToken +) { +} diff --git a/src/main/java/sopt/org/hmh/global/auth/social/apple/fegin/AppleFeignClient.java b/src/main/java/sopt/org/hmh/global/auth/social/apple/fegin/AppleFeignClient.java index 757cea79..a281ce62 100644 --- a/src/main/java/sopt/org/hmh/global/auth/social/apple/fegin/AppleFeignClient.java +++ b/src/main/java/sopt/org/hmh/global/auth/social/apple/fegin/AppleFeignClient.java @@ -2,11 +2,11 @@ import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; -import sopt.org.hmh.global.auth.social.apple.request.ApplePublicKeys; +import sopt.org.hmh.global.auth.social.apple.response.ApplePublicKeysResponse; @FeignClient(name = "appleFeignClient", url = "${oauth2.apple.base-url}") public interface AppleFeignClient { @GetMapping("/auth/keys") - ApplePublicKeys getApplePublicKeys(); + ApplePublicKeysResponse getApplePublicKeys(); } diff --git a/src/main/java/sopt/org/hmh/global/auth/social/apple/fegin/AppleOAuthProvider.java b/src/main/java/sopt/org/hmh/global/auth/social/apple/fegin/AppleOAuthProvider.java index c49420ce..1bca06ee 100644 --- a/src/main/java/sopt/org/hmh/global/auth/social/apple/fegin/AppleOAuthProvider.java +++ b/src/main/java/sopt/org/hmh/global/auth/social/apple/fegin/AppleOAuthProvider.java @@ -7,7 +7,7 @@ import java.util.Map; import sopt.org.hmh.global.auth.jwt.exception.JwtError; import sopt.org.hmh.global.auth.jwt.exception.JwtException; -import sopt.org.hmh.global.auth.social.apple.request.ApplePublicKeys; +import sopt.org.hmh.global.auth.social.apple.response.ApplePublicKeysResponse; @RequiredArgsConstructor @Component @@ -19,8 +19,8 @@ public class AppleOAuthProvider { public String getApplePlatformId(String identityToken) { Map headers = appleIdentityTokenParser.parseHeaders(identityToken); - ApplePublicKeys applePublicKeys = appleFeignClient.getApplePublicKeys(); - PublicKey publicKey = applePublicKeyGenerator.generatePublicKeyWithApplePublicKeys(headers, applePublicKeys); + ApplePublicKeysResponse applePublicKeysResponse = appleFeignClient.getApplePublicKeys(); + PublicKey publicKey = applePublicKeyGenerator.generatePublicKeyWithApplePublicKeys(headers, applePublicKeysResponse); Claims claims = appleIdentityTokenParser.parseWithPublicKeyAndGetClaims(identityToken, publicKey); validateClaims(claims); return claims.getSubject(); diff --git a/src/main/java/sopt/org/hmh/global/auth/social/apple/fegin/ApplePublicKeyGenerator.java b/src/main/java/sopt/org/hmh/global/auth/social/apple/fegin/ApplePublicKeyGenerator.java index 4de64e1f..4294ff77 100644 --- a/src/main/java/sopt/org/hmh/global/auth/social/apple/fegin/ApplePublicKeyGenerator.java +++ b/src/main/java/sopt/org/hmh/global/auth/social/apple/fegin/ApplePublicKeyGenerator.java @@ -11,23 +11,23 @@ import java.math.BigInteger; import sopt.org.hmh.global.auth.jwt.exception.JwtError; import sopt.org.hmh.global.auth.jwt.exception.JwtException; -import sopt.org.hmh.global.auth.social.apple.request.ApplePublicKey; -import sopt.org.hmh.global.auth.social.apple.request.ApplePublicKeys; +import sopt.org.hmh.global.auth.social.apple.response.ApplePublicKeyResponse; +import sopt.org.hmh.global.auth.social.apple.response.ApplePublicKeysResponse; @Component public class ApplePublicKeyGenerator { - public PublicKey generatePublicKeyWithApplePublicKeys(Map headers, ApplePublicKeys applePublicKeys) { - ApplePublicKey applePublicKey = applePublicKeys + public PublicKey generatePublicKeyWithApplePublicKeys(Map headers, ApplePublicKeysResponse applePublicKeysResponse) { + ApplePublicKeyResponse applePublicKeyResponse = applePublicKeysResponse .getMatchesKey(headers.get("alg"), headers.get("kid")); - byte[] nBytes = Base64.getUrlDecoder().decode(applePublicKey.n()); - byte[] eBytes = Base64.getUrlDecoder().decode(applePublicKey.e()); + byte[] nBytes = Base64.getUrlDecoder().decode(applePublicKeyResponse.n()); + byte[] eBytes = Base64.getUrlDecoder().decode(applePublicKeyResponse.e()); RSAPublicKeySpec rsaPublicKeySpec = new RSAPublicKeySpec( new BigInteger(1, nBytes), new BigInteger(1, eBytes)); try { - KeyFactory keyFactory = KeyFactory.getInstance(applePublicKey.kty()); + KeyFactory keyFactory = KeyFactory.getInstance(applePublicKeyResponse.kty()); return keyFactory.generatePublic(rsaPublicKeySpec); } catch (NoSuchAlgorithmException | InvalidKeySpecException exception) { throw new JwtException(JwtError.UNABLE_TO_CREATE_APPLE_PUBLIC_KEY); diff --git a/src/main/java/sopt/org/hmh/global/auth/social/apple/request/ApplePublicKeys.java b/src/main/java/sopt/org/hmh/global/auth/social/apple/request/ApplePublicKeys.java deleted file mode 100644 index 59afc280..00000000 --- a/src/main/java/sopt/org/hmh/global/auth/social/apple/request/ApplePublicKeys.java +++ /dev/null @@ -1,18 +0,0 @@ -package sopt.org.hmh.global.auth.social.apple.request; - -import java.util.List; -import lombok.Getter; -import sopt.org.hmh.global.auth.jwt.exception.JwtError; -import sopt.org.hmh.global.auth.jwt.exception.JwtException; - -@Getter -public class ApplePublicKeys { - private List keys; - - public ApplePublicKey getMatchesKey(String alg, String kid) { - return keys.stream() - .filter(applePublicKey -> applePublicKey.alg().equals(alg) && applePublicKey.kid().equals(kid)) - .findFirst() - .orElseThrow(() -> new JwtException(JwtError.INVALID_IDENTITY_TOKEN)); - } -} \ No newline at end of file diff --git a/src/main/java/sopt/org/hmh/global/auth/social/apple/request/ApplePublicKey.java b/src/main/java/sopt/org/hmh/global/auth/social/apple/response/ApplePublicKeyResponse.java similarity index 56% rename from src/main/java/sopt/org/hmh/global/auth/social/apple/request/ApplePublicKey.java rename to src/main/java/sopt/org/hmh/global/auth/social/apple/response/ApplePublicKeyResponse.java index 0227895d..02de03d8 100644 --- a/src/main/java/sopt/org/hmh/global/auth/social/apple/request/ApplePublicKey.java +++ b/src/main/java/sopt/org/hmh/global/auth/social/apple/response/ApplePublicKeyResponse.java @@ -1,6 +1,6 @@ -package sopt.org.hmh.global.auth.social.apple.request; +package sopt.org.hmh.global.auth.social.apple.response; -public record ApplePublicKey( +public record ApplePublicKeyResponse( String kty, String kid, String use, diff --git a/src/main/java/sopt/org/hmh/global/auth/social/apple/response/ApplePublicKeysResponse.java b/src/main/java/sopt/org/hmh/global/auth/social/apple/response/ApplePublicKeysResponse.java new file mode 100644 index 00000000..21d60586 --- /dev/null +++ b/src/main/java/sopt/org/hmh/global/auth/social/apple/response/ApplePublicKeysResponse.java @@ -0,0 +1,18 @@ +package sopt.org.hmh.global.auth.social.apple.response; + +import java.util.List; +import lombok.Getter; +import sopt.org.hmh.global.auth.jwt.exception.JwtError; +import sopt.org.hmh.global.auth.jwt.exception.JwtException; + +@Getter +public class ApplePublicKeysResponse { + private List keys; + + public ApplePublicKeyResponse getMatchesKey(String alg, String kid) { + return keys.stream() + .filter(applePublicKeyResponse -> applePublicKeyResponse.alg().equals(alg) && applePublicKeyResponse.kid().equals(kid)) + .findFirst() + .orElseThrow(() -> new JwtException(JwtError.INVALID_IDENTITY_TOKEN)); + } +} \ No newline at end of file diff --git a/src/main/java/sopt/org/hmh/global/auth/social/kakao/fegin/KakaoAuthFeignClient.java b/src/main/java/sopt/org/hmh/global/auth/social/kakao/fegin/KakaoAuthFeignClient.java new file mode 100644 index 00000000..f0f45ef5 --- /dev/null +++ b/src/main/java/sopt/org/hmh/global/auth/social/kakao/fegin/KakaoAuthFeignClient.java @@ -0,0 +1,19 @@ +package sopt.org.hmh.global.auth.social.kakao.fegin; + +import sopt.org.hmh.global.auth.social.kakao.response.KakaoTokenResponse; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; + +@FeignClient(name = "kakaoAuthApiClient", url = "https://kauth.kakao.com") +public interface KakaoAuthFeignClient { + + @PostMapping(value = "/oauth/token", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) + KakaoTokenResponse getOAuth2AccessToken( + @RequestParam("grant_type") String grantType, + @RequestParam("client_id") String clientId, + @RequestParam("redirect_uri") String redirectUri, + @RequestParam("code") String code + ); +} diff --git a/src/main/java/sopt/org/hmh/global/auth/social/kakao/fegin/KakaoFeignClient.java b/src/main/java/sopt/org/hmh/global/auth/social/kakao/fegin/KakaoFeignClient.java index 531cbcbb..ba58a065 100644 --- a/src/main/java/sopt/org/hmh/global/auth/social/kakao/fegin/KakaoFeignClient.java +++ b/src/main/java/sopt/org/hmh/global/auth/social/kakao/fegin/KakaoFeignClient.java @@ -4,11 +4,11 @@ import org.springframework.http.HttpHeaders; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestHeader; -import sopt.org.hmh.global.auth.social.kakao.request.KakaoUserRequest; +import sopt.org.hmh.global.auth.social.kakao.response.KakaoUserResponse; @FeignClient(name = "kakaoApiClient", url = "${oauth2.kakao.base-url}") public interface KakaoFeignClient { @GetMapping(value = "/v2/user/me") - KakaoUserRequest getUserInformation(@RequestHeader(HttpHeaders.AUTHORIZATION) final String accessToken); + KakaoUserResponse getUserInformation(@RequestHeader(HttpHeaders.AUTHORIZATION) final String accessToken); } \ No newline at end of file diff --git a/src/main/java/sopt/org/hmh/global/auth/social/kakao/fegin/KakaoLoginService.java b/src/main/java/sopt/org/hmh/global/auth/social/kakao/fegin/KakaoLoginService.java index 460fdd66..c1aff229 100644 --- a/src/main/java/sopt/org/hmh/global/auth/social/kakao/fegin/KakaoLoginService.java +++ b/src/main/java/sopt/org/hmh/global/auth/social/kakao/fegin/KakaoLoginService.java @@ -2,13 +2,16 @@ import feign.FeignException; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import sopt.org.hmh.domain.user.domain.User; import sopt.org.hmh.global.auth.jwt.exception.JwtError; import sopt.org.hmh.global.auth.jwt.exception.JwtException; -import sopt.org.hmh.global.auth.social.kakao.request.KakaoUserRequest; +import sopt.org.hmh.global.auth.social.SocialAccessTokenResponse; +import sopt.org.hmh.global.auth.social.kakao.response.KakaoTokenResponse; +import sopt.org.hmh.global.auth.social.kakao.response.KakaoUserResponse; @Service @Transactional @@ -16,19 +19,21 @@ public class KakaoLoginService { private final KakaoFeignClient kakaoFeignClient; + private final KakaoAuthFeignClient kakaoAuthFeignClient; + + @Value("${oauth2.kakao.client-id}") + private String CLIENT_ID; + @Value("${oauth2.kakao.authorization-grant-type}") + private String GRANT_TYPE; + @Value("${oauth2.kakao.redirect-uri}") + private String REDIRECT_URL; - /** - * 카카오 Acess Token으로 유저의 소셜 Id 불러오는 함수 - */ public String getSocialIdByKakao(final String socialAccessToken) { return String.valueOf(getKakaoUserRequest(socialAccessToken).id()); } - /** - * 카카오 Access Token으로 유저 정보 업데이트 - */ public void updateUserInfoByKakao(User loginUser, final String socialAccessToken) { - KakaoUserRequest userRequest = getKakaoUserRequest(socialAccessToken); + KakaoUserResponse userRequest = getKakaoUserRequest(socialAccessToken); try { String nickname = userRequest.kakaoAccount().profile().nickname(); @@ -45,11 +50,25 @@ public void updateUserInfoByKakao(User loginUser, final String socialAccessToken } } - private KakaoUserRequest getKakaoUserRequest(final String socialAccessToken) { + private KakaoUserResponse getKakaoUserRequest(final String socialAccessToken) { try { return kakaoFeignClient.getUserInformation(socialAccessToken); } catch (FeignException exception) { throw new JwtException(JwtError.INVALID_SOCIAL_ACCESS_TOKEN); } } + + public SocialAccessTokenResponse getKakaoAccessToken(String code) { + + KakaoTokenResponse kakaoTokenResponse = kakaoAuthFeignClient.getOAuth2AccessToken( + GRANT_TYPE, + CLIENT_ID, + REDIRECT_URL, + code + ); + + return SocialAccessTokenResponse.builder() + .socialAccessToken(kakaoTokenResponse.accessToken()) + .build(); + } } \ No newline at end of file diff --git a/src/main/java/sopt/org/hmh/global/auth/social/kakao/request/KakaoAccount.java b/src/main/java/sopt/org/hmh/global/auth/social/kakao/response/KakaoAccount.java similarity index 81% rename from src/main/java/sopt/org/hmh/global/auth/social/kakao/request/KakaoAccount.java rename to src/main/java/sopt/org/hmh/global/auth/social/kakao/response/KakaoAccount.java index 7f17cb09..86732d7d 100644 --- a/src/main/java/sopt/org/hmh/global/auth/social/kakao/request/KakaoAccount.java +++ b/src/main/java/sopt/org/hmh/global/auth/social/kakao/response/KakaoAccount.java @@ -1,4 +1,4 @@ -package sopt.org.hmh.global.auth.social.kakao.request; +package sopt.org.hmh.global.auth.social.kakao.response; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonNaming; diff --git a/src/main/java/sopt/org/hmh/global/auth/social/kakao/response/KakaoTokenResponse.java b/src/main/java/sopt/org/hmh/global/auth/social/kakao/response/KakaoTokenResponse.java new file mode 100644 index 00000000..068eadff --- /dev/null +++ b/src/main/java/sopt/org/hmh/global/auth/social/kakao/response/KakaoTokenResponse.java @@ -0,0 +1,10 @@ +package sopt.org.hmh.global.auth.social.kakao.response; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public record KakaoTokenResponse( + String accessToken +) { +} diff --git a/src/main/java/sopt/org/hmh/global/auth/social/kakao/request/KakaoUserProfile.java b/src/main/java/sopt/org/hmh/global/auth/social/kakao/response/KakaoUserProfile.java similarity index 83% rename from src/main/java/sopt/org/hmh/global/auth/social/kakao/request/KakaoUserProfile.java rename to src/main/java/sopt/org/hmh/global/auth/social/kakao/response/KakaoUserProfile.java index 338fdbd4..5809a820 100644 --- a/src/main/java/sopt/org/hmh/global/auth/social/kakao/request/KakaoUserProfile.java +++ b/src/main/java/sopt/org/hmh/global/auth/social/kakao/response/KakaoUserProfile.java @@ -1,4 +1,4 @@ -package sopt.org.hmh.global.auth.social.kakao.request; +package sopt.org.hmh.global.auth.social.kakao.response; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonNaming; diff --git a/src/main/java/sopt/org/hmh/global/auth/social/kakao/request/KakaoUserRequest.java b/src/main/java/sopt/org/hmh/global/auth/social/kakao/response/KakaoUserResponse.java similarity index 73% rename from src/main/java/sopt/org/hmh/global/auth/social/kakao/request/KakaoUserRequest.java rename to src/main/java/sopt/org/hmh/global/auth/social/kakao/response/KakaoUserResponse.java index 0b6ae272..ea041cfa 100644 --- a/src/main/java/sopt/org/hmh/global/auth/social/kakao/request/KakaoUserRequest.java +++ b/src/main/java/sopt/org/hmh/global/auth/social/kakao/response/KakaoUserResponse.java @@ -1,10 +1,10 @@ -package sopt.org.hmh.global.auth.social.kakao.request; +package sopt.org.hmh.global.auth.social.kakao.response; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonNaming; @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) -public record KakaoUserRequest( +public record KakaoUserResponse( Long id, KakaoAccount kakaoAccount ) { diff --git a/src/main/java/sopt/org/hmh/global/config/SecurityConfig.java b/src/main/java/sopt/org/hmh/global/config/SecurityConfig.java index 08635b95..7c07c0c0 100644 --- a/src/main/java/sopt/org/hmh/global/config/SecurityConfig.java +++ b/src/main/java/sopt/org/hmh/global/config/SecurityConfig.java @@ -26,7 +26,9 @@ public class SecurityConfig { private final JwtAuthenticationEntryPoint customJwtAuthenticationEntryPoint; private static final String[] AUTH_WHITELIST = { - "/", "/error", "/health", "/api/v1/dummy/**", + // Global + "/health", + "/error", // Swagger "/swagger-ui/**", @@ -38,6 +40,10 @@ public class SecurityConfig { "/api/v1/user/login", "/api/v1/user/reissue", "/api/v1/user/signup", + "/api/v1/user/social/token/kakao", + + // etc + "/api/v1/dummy/**", }; @Bean