diff --git a/src/main/java/com/depromeet/stonebed/domain/auth/application/AuthService.java b/src/main/java/com/depromeet/stonebed/domain/auth/application/AuthService.java index a8fbee42..017222bf 100644 --- a/src/main/java/com/depromeet/stonebed/domain/auth/application/AuthService.java +++ b/src/main/java/com/depromeet/stonebed/domain/auth/application/AuthService.java @@ -1,6 +1,7 @@ package com.depromeet.stonebed.domain.auth.application; import com.depromeet.stonebed.domain.auth.application.apple.AppleClient; +import com.depromeet.stonebed.domain.auth.application.kakao.KakaoClient; import com.depromeet.stonebed.domain.auth.domain.OAuthProvider; import com.depromeet.stonebed.domain.auth.dto.RefreshTokenDto; import com.depromeet.stonebed.domain.auth.dto.request.RefreshTokenRequest; @@ -14,7 +15,6 @@ import com.depromeet.stonebed.global.error.ErrorCode; import com.depromeet.stonebed.global.error.exception.CustomException; import com.depromeet.stonebed.global.util.MemberUtil; -import java.security.InvalidParameterException; import java.util.Optional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -28,6 +28,7 @@ public class AuthService { private final AppleClient appleClient; + private final KakaoClient kakaoClient; private final MemberService memberService; private final JwtTokenService jwtTokenService; private final MemberUtil memberUtil; @@ -39,9 +40,7 @@ public SocialClientResponse authenticateFromProvider(OAuthProvider provider, Str */ return switch (provider) { case APPLE -> appleClient.authenticateFromApple(token); - // TODO: 추후 카카오 개발 예정 - // case KAKAO -> authenticateFromKakao(accessToken); - default -> throw new InvalidParameterException(); + case KAKAO -> kakaoClient.authenticateFromKakao(token); }; } diff --git a/src/main/java/com/depromeet/stonebed/domain/auth/application/JwtTokenService.java b/src/main/java/com/depromeet/stonebed/domain/auth/application/JwtTokenService.java index ce581690..517ca1f6 100644 --- a/src/main/java/com/depromeet/stonebed/domain/auth/application/JwtTokenService.java +++ b/src/main/java/com/depromeet/stonebed/domain/auth/application/JwtTokenService.java @@ -70,7 +70,7 @@ public AccessTokenDto retrieveAccessToken(String accessTokenValue) { public RefreshTokenDto retrieveRefreshToken(String refreshTokenValue) { RefreshTokenDto refreshTokenDto = parseRefreshToken(refreshTokenValue); - System.out.println("refreshTokenDto: " + refreshTokenDto); + if (refreshTokenDto == null) { return null; } diff --git a/src/main/java/com/depromeet/stonebed/domain/auth/application/apple/AppleClient.java b/src/main/java/com/depromeet/stonebed/domain/auth/application/apple/AppleClient.java index f4534a7d..e49e7ec9 100644 --- a/src/main/java/com/depromeet/stonebed/domain/auth/application/apple/AppleClient.java +++ b/src/main/java/com/depromeet/stonebed/domain/auth/application/apple/AppleClient.java @@ -50,14 +50,55 @@ public class AppleClient { private static final int APPLE_TOKEN_EXPIRE_MINUTES = 5; + /** + * Apple로부터 받은 idToken 검증하고 identifier를 추출합니다. + * + * @param authorizationCode + * @return + */ + public SocialClientResponse authenticateFromApple(String authorizationCode) { + AppleTokenRequest tokenRequest = + AppleTokenRequest.of( + authorizationCode, + appleProperties.dev().clientId(), + generateAppleClientSecret(), + APPLE_GRANT_TYPE); + AppleTokenResponse appleTokenResponse = getAppleToken(tokenRequest); + + AppleKeyResponse[] keys = retrieveAppleKeys(); + try { + String[] tokenParts = appleTokenResponse.idToken().split("\\."); + String headerPart = new String(Base64.getDecoder().decode(tokenParts[0])); + JsonNode headerNode = objectMapper.readTree(headerPart); + String kid = headerNode.get("kid").asText(); + String alg = headerNode.get("alg").asText(); + + AppleKeyResponse matchedKey = + Arrays.stream(keys) + .filter(key -> key.kid().equals(kid) && key.alg().equals(alg)) + .findFirst() + // 일치하는 키가 없음 => 만료된 토큰 or 이상한 토큰 => throw + .orElseThrow(InvalidParameterException::new); + + Claims claims = parseIdentifierFromAppleToken(matchedKey, appleTokenResponse.idToken()); + + String oauthId = claims.get("sub", String.class); + String email = claims.get("email", String.class); + + return new SocialClientResponse(email, oauthId); + } catch (Exception ex) { + throw new CustomException(ErrorCode.INTERNAL_SERVER_ERROR); + } + } + // apple server에서 받아온 id_token private AppleTokenResponse getAppleToken(AppleTokenRequest appleTokenRequest) { // Prepare form data MultiValueMap formData = new LinkedMultiValueMap<>(); - formData.add("client_id", appleTokenRequest.client_id()); - formData.add("client_secret", appleTokenRequest.client_secret()); + formData.add("client_id", appleTokenRequest.clientId()); + formData.add("client_secret", appleTokenRequest.clientSecret()); formData.add("code", appleTokenRequest.code()); - formData.add("grant_type", appleTokenRequest.grant_type()); + formData.add("grant_type", appleTokenRequest.grantType()); return restClient .post() @@ -107,48 +148,6 @@ private PrivateKey getPrivateKey() { } } - /** - * Apple로부터 받은 idToken 검증하고 identifier를 추출합니다. - * - * @param authorizationCode - * @return - */ - public SocialClientResponse authenticateFromApple(String authorizationCode) { - AppleTokenRequest tokenRequest = - AppleTokenRequest.of( - authorizationCode, - appleProperties.dev().clientId(), - generateAppleClientSecret(), - APPLE_GRANT_TYPE); - AppleTokenResponse appleTokenResponse = getAppleToken(tokenRequest); - - AppleKeyResponse[] keys = retrieveAppleKeys(); - try { - String[] tokenParts = appleTokenResponse.id_token().split("\\."); - String headerPart = new String(Base64.getDecoder().decode(tokenParts[0])); - JsonNode headerNode = objectMapper.readTree(headerPart); - String kid = headerNode.get("kid").asText(); - String alg = headerNode.get("alg").asText(); - - AppleKeyResponse matchedKey = - Arrays.stream(keys) - .filter(key -> key.kid().equals(kid) && key.alg().equals(alg)) - .findFirst() - // 일치하는 키가 없음 => 만료된 토큰 or 이상한 토큰 => throw - .orElseThrow(InvalidParameterException::new); - - Claims claims = - parseIdentifierFromAppleToken(matchedKey, appleTokenResponse.id_token()); - - String oauthId = claims.get("sub", String.class); - String email = claims.get("email", String.class); - - return new SocialClientResponse(email, oauthId); - } catch (Exception ex) { - throw new CustomException(ErrorCode.INTERNAL_SERVER_ERROR); - } - } - private AppleKeyResponse[] retrieveAppleKeys() { AppleKeyListResponse keyListResponse = restClient diff --git a/src/main/java/com/depromeet/stonebed/domain/auth/application/kakao/KakaoClient.java b/src/main/java/com/depromeet/stonebed/domain/auth/application/kakao/KakaoClient.java new file mode 100644 index 00000000..0734a876 --- /dev/null +++ b/src/main/java/com/depromeet/stonebed/domain/auth/application/kakao/KakaoClient.java @@ -0,0 +1,38 @@ +package com.depromeet.stonebed.domain.auth.application.kakao; + +import static com.depromeet.stonebed.global.common.constants.SecurityConstants.*; + +import com.depromeet.stonebed.domain.auth.dto.response.KakaoAuthResponse; +import com.depromeet.stonebed.domain.auth.dto.response.SocialClientResponse; +import com.depromeet.stonebed.global.error.ErrorCode; +import com.depromeet.stonebed.global.error.exception.CustomException; +import java.util.Objects; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClient; + +@Component +@RequiredArgsConstructor +public class KakaoClient { + private final RestClient restClient; + + public SocialClientResponse authenticateFromKakao(String token) { + KakaoAuthResponse kakaoAuthResponse = + restClient + .get() + .uri(KAKAO_USER_ME_URL) + .header("Authorization", TOKEN_PREFIX + token) + .exchange( + (request, response) -> { + if (!response.getStatusCode().is2xxSuccessful()) { + throw new CustomException( + ErrorCode.APPLE_TOKEN_CLIENT_FAILED); + } + return Objects.requireNonNull( + response.bodyTo(KakaoAuthResponse.class)); + }); + + return new SocialClientResponse( + kakaoAuthResponse.kakaoAccount().email(), kakaoAuthResponse.id().toString()); + } +} diff --git a/src/main/java/com/depromeet/stonebed/domain/auth/dto/request/AppleTokenRequest.java b/src/main/java/com/depromeet/stonebed/domain/auth/dto/request/AppleTokenRequest.java index ea0819ea..3c442521 100644 --- a/src/main/java/com/depromeet/stonebed/domain/auth/dto/request/AppleTokenRequest.java +++ b/src/main/java/com/depromeet/stonebed/domain/auth/dto/request/AppleTokenRequest.java @@ -1,8 +1,13 @@ package com.depromeet.stonebed.domain.auth.dto.request; +import com.fasterxml.jackson.annotation.JsonProperty; + public record AppleTokenRequest( // 외부 통신 시 snake_case로 요청 및 응답 - String code, String client_id, String client_secret, String grant_type) { + @JsonProperty("code") String code, + @JsonProperty("client_id") String clientId, + @JsonProperty("client_secret") String clientSecret, + @JsonProperty("grant_type") String grantType) { public static AppleTokenRequest of( String code, String clientId, String clientSecret, String grantType) { return new AppleTokenRequest(code, clientId, clientSecret, grantType); diff --git a/src/main/java/com/depromeet/stonebed/domain/auth/dto/response/AppleTokenResponse.java b/src/main/java/com/depromeet/stonebed/domain/auth/dto/response/AppleTokenResponse.java index fffb12aa..ef937a1e 100644 --- a/src/main/java/com/depromeet/stonebed/domain/auth/dto/response/AppleTokenResponse.java +++ b/src/main/java/com/depromeet/stonebed/domain/auth/dto/response/AppleTokenResponse.java @@ -1,12 +1,14 @@ package com.depromeet.stonebed.domain.auth.dto.response; +import com.fasterxml.jackson.annotation.JsonProperty; + public record AppleTokenResponse( // 외부 통신 시 snake_case로 요청 및 응답 - String access_token, - Long expires_in, - String id_token, - String refresh_token, - String token_type) { + @JsonProperty("access_token") String accessToken, + @JsonProperty("expires_in") Long expiresIn, + @JsonProperty("id_token") String idToken, + @JsonProperty("refresh_token") String refreshToken, + @JsonProperty("token_type") String tokenType) { public static AppleTokenResponse of( String accessToken, Long expiresIn, diff --git a/src/main/java/com/depromeet/stonebed/domain/auth/dto/response/KakaoAuthResponse.java b/src/main/java/com/depromeet/stonebed/domain/auth/dto/response/KakaoAuthResponse.java new file mode 100644 index 00000000..d2a94ccd --- /dev/null +++ b/src/main/java/com/depromeet/stonebed/domain/auth/dto/response/KakaoAuthResponse.java @@ -0,0 +1,13 @@ +package com.depromeet.stonebed.domain.auth.dto.response; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record KakaoAuthResponse( + Long id, + @JsonProperty("kakao_account") KakaoAccountResponse kakaoAccount, + @JsonProperty("properties") PropertiesResponse properties) { + public static record KakaoAccountResponse(String email) {} + + public static record PropertiesResponse( + String nickname, String profile_image, String thumbnail_image) {} +} diff --git a/src/main/java/com/depromeet/stonebed/global/common/constants/SecurityConstants.java b/src/main/java/com/depromeet/stonebed/global/common/constants/SecurityConstants.java index 709ab0c5..590e3384 100644 --- a/src/main/java/com/depromeet/stonebed/global/common/constants/SecurityConstants.java +++ b/src/main/java/com/depromeet/stonebed/global/common/constants/SecurityConstants.java @@ -2,12 +2,18 @@ public final class SecurityConstants { - public static final String TOKEN_ROLE_NAME = "role"; - public static final String TOKEN_PREFIX = "Bearer "; + // apple public static final String APPLE_JWK_SET_URL = "https://appleid.apple.com/auth/keys"; public static final String APPLE_ISSUER = "https://appleid.apple.com"; public static final String APPLE_TOKEN_URL = "https://appleid.apple.com/auth/token"; public static final String APPLE_GRANT_TYPE = "authorization_code"; + + // kakao + public static final String KAKAO_USER_ME_URL = "https://kapi.kakao.com/v2/user/me"; + + // security + public static final String TOKEN_ROLE_NAME = "role"; + public static final String TOKEN_PREFIX = "Bearer "; public static final String ACCESS_TOKEN_COOKIE_NAME = "accessToken"; public static final String REFRESH_TOKEN_COOKIE_NAME = "refreshToken"; public static final String APPLICATION_URLENCODED = "application/x-www-form-urlencoded"; diff --git a/src/main/java/com/depromeet/stonebed/global/error/ErrorCode.java b/src/main/java/com/depromeet/stonebed/global/error/ErrorCode.java index da2766e4..988335f4 100644 --- a/src/main/java/com/depromeet/stonebed/global/error/ErrorCode.java +++ b/src/main/java/com/depromeet/stonebed/global/error/ErrorCode.java @@ -21,6 +21,9 @@ public enum ErrorCode { APPLE_TOKEN_CLIENT_FAILED(HttpStatus.BAD_REQUEST, "애플 토큰 생성에 실패했습니다."), APPLE_PRIVATE_KEY_ENCODING_FAILED(HttpStatus.BAD_REQUEST, "애플 개인키 인코딩에 실패했습니다."), + // kakao client + KAKAO_TOKEN_CLIENT_FAILED(HttpStatus.BAD_REQUEST, "카카오 통신에 실패했습니다."), + // member MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 회원을 찾을 수 없습니다."), ALREADY_EXISTS_MEMBER(HttpStatus.CONFLICT, "이미 존재하는 회원입니다."),