diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6492e79..2c6a067 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,6 +6,7 @@ on: - main - dev + # 권한 설정 permissions: write-all diff --git a/src/main/java/com/moneymong/domain/user/api/request/UserDeleteRequest.java b/src/main/java/com/moneymong/domain/user/api/request/UserDeleteRequest.java index 2d71605..68bf2f6 100644 --- a/src/main/java/com/moneymong/domain/user/api/request/UserDeleteRequest.java +++ b/src/main/java/com/moneymong/domain/user/api/request/UserDeleteRequest.java @@ -1,13 +1,14 @@ package com.moneymong.domain.user.api.request; import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.NoArgsConstructor; @Getter +@AllArgsConstructor +@NoArgsConstructor public class UserDeleteRequest { @NotBlank private String provider; - - @NotBlank - private String token; } diff --git a/src/main/java/com/moneymong/domain/user/entity/AppleUser.java b/src/main/java/com/moneymong/domain/user/entity/AppleUser.java index 8e30ad0..eefd5ec 100644 --- a/src/main/java/com/moneymong/domain/user/entity/AppleUser.java +++ b/src/main/java/com/moneymong/domain/user/entity/AppleUser.java @@ -1,13 +1,10 @@ package com.moneymong.domain.user.entity; -import com.moneymong.global.domain.BaseEntity; import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import org.hibernate.annotations.SQLDelete; -import org.hibernate.annotations.Where; import static lombok.AccessLevel.PROTECTED; @@ -17,9 +14,7 @@ @Builder @AllArgsConstructor @NoArgsConstructor(access = PROTECTED) -@Where(clause = "deleted = false") -@SQLDelete(sql = "UPDATE users SET deleted = true where id=?") -public class AppleUser extends BaseEntity { +public class AppleUser { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; diff --git a/src/main/java/com/moneymong/domain/user/repository/AppleUserRepository.java b/src/main/java/com/moneymong/domain/user/repository/AppleUserRepository.java index 4de8c0b..4098c43 100644 --- a/src/main/java/com/moneymong/domain/user/repository/AppleUserRepository.java +++ b/src/main/java/com/moneymong/domain/user/repository/AppleUserRepository.java @@ -3,5 +3,8 @@ import com.moneymong.domain.user.entity.AppleUser; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface AppleUserRepository extends JpaRepository { + Optional findAppleUserByUserId(Long userId); } diff --git a/src/main/java/com/moneymong/domain/user/service/UserFacadeService.java b/src/main/java/com/moneymong/domain/user/service/UserFacadeService.java index c694daa..c9ec903 100644 --- a/src/main/java/com/moneymong/domain/user/service/UserFacadeService.java +++ b/src/main/java/com/moneymong/domain/user/service/UserFacadeService.java @@ -40,14 +40,9 @@ public LoginSuccessResponse login(LoginRequest loginRequest) { @Transactional public void delete(Long userId) { + oAuthService.revoke(userId); userService.delete(userId); userUniversityService.delete(userId); agencyUserService.deleteAll(userId); } - - @Transactional - public void revoke(UserDeleteRequest deleteRequest, Long userId) { - oAuthService.revoke(deleteRequest); - delete(userId); - } } diff --git a/src/main/java/com/moneymong/domain/user/service/UserService.java b/src/main/java/com/moneymong/domain/user/service/UserService.java index 24baada..d7e26e0 100644 --- a/src/main/java/com/moneymong/domain/user/service/UserService.java +++ b/src/main/java/com/moneymong/domain/user/service/UserService.java @@ -13,12 +13,14 @@ import com.moneymong.global.security.oauth.dto.OAuthUserInfo; import com.moneymong.global.security.token.repository.RefreshTokenRepository; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.UUID; @Service +@Slf4j @RequiredArgsConstructor public class UserService { @@ -53,6 +55,9 @@ public User registerUser(OAuthUserInfo oauthUserInfo) { ); newUser = save(newUser); + log.info("[UserService] registerUserId = {}", newUser.getId()); + log.info("[UserService] refreshToken = {}", oauthUserInfo.getAppleRefreshToken()); + if (oauthUserInfo.getAppleRefreshToken() != null) { appleUserRepository.save( AppleUser.of( @@ -65,7 +70,6 @@ public User registerUser(OAuthUserInfo oauthUserInfo) { return newUser; } - @Transactional(readOnly = true) public UserProfileResponse getUserProfile(Long userId) { User user = userRepository.findById(userId) diff --git a/src/main/java/com/moneymong/global/exception/enums/ErrorCode.java b/src/main/java/com/moneymong/global/exception/enums/ErrorCode.java index 6150ff5..c8c3511 100644 --- a/src/main/java/com/moneymong/global/exception/enums/ErrorCode.java +++ b/src/main/java/com/moneymong/global/exception/enums/ErrorCode.java @@ -14,6 +14,7 @@ public enum ErrorCode { // ---- 유저 ---- // USER_NOT_FOUND(MoneymongConstant.NOT_FOUND, "USER-001", "존재하지 않는 회원입니다."), + USER_NOT_FOUND_APPLE(MoneymongConstant.NOT_FOUND, "USER-002", "존재하지 않는 APPLE 유저입니다."), // ---- 대학교 ---- // USER_UNIVERSITY_NOT_FOUND(MoneymongConstant.NOT_FOUND, "UNIVERSITY-001", "회원의 대학 정보가 존재하지 않습니다."), diff --git a/src/main/java/com/moneymong/global/security/oauth/handler/AppleService.java b/src/main/java/com/moneymong/global/security/oauth/handler/AppleService.java index ccb3bd2..a7df8ac 100644 --- a/src/main/java/com/moneymong/global/security/oauth/handler/AppleService.java +++ b/src/main/java/com/moneymong/global/security/oauth/handler/AppleService.java @@ -2,6 +2,9 @@ import com.auth0.jwt.JWT; import com.auth0.jwt.interfaces.DecodedJWT; +import com.moneymong.domain.user.entity.AppleUser; +import com.moneymong.domain.user.repository.AppleUserRepository; +import com.moneymong.global.exception.custom.NotFoundException; import com.moneymong.global.exception.enums.ErrorCode; import com.moneymong.global.security.oauth.dto.AppleUserData; import com.moneymong.global.security.oauth.dto.OAuthUserDataRequest; @@ -29,11 +32,10 @@ import java.net.URI; import java.security.PrivateKey; import java.security.Security; -import java.time.ZoneId; import java.time.ZonedDateTime; -import java.util.Base64; -import java.util.Date; -import java.util.Map; +import java.util.*; + +import static com.moneymong.global.exception.enums.ErrorCode.USER_NOT_FOUND_APPLE; @Slf4j @Component @@ -41,6 +43,7 @@ public class AppleService implements OAuthAuthenticationHandler { private final RestTemplate restTemplate; + private final AppleUserRepository appleUserRepository; @Value("${spring.security.oauth2.apple.host}") private String host; @@ -65,7 +68,7 @@ public OAuthProvider getAuthProvider() { @Override public OAuthUserDataResponse getOAuthUserData(OAuthUserDataRequest request) { if (request.getCode() == null) { - return decodePayload(request.getAccessToken(), request.getName()); + return decodePayload(request.getAccessToken(), request.getName(), null); } HttpHeaders httpHeaders = new HttpHeaders(); @@ -98,29 +101,59 @@ public OAuthUserDataResponse getOAuthUserData(OAuthUserDataRequest request) { String idToken = userData.getIdToken(); log.info("[AppleService] refreshToken = {}", refreshToken); - return decodePayload(idToken, request.getName()); + return decodePayload(idToken, request.getName(), refreshToken); } catch (RestClientException e) { - log.info("[AppleService] error message = {}", e.getMessage()); log.warn("[AppleService] failed to get OAuth User Data = {}", request.getAccessToken()); throw new HttpClientException(ErrorCode.HTTP_CLIENT_REQUEST_FAILED); } } @Override - public void unlink(String token) { + public void unlink(Long userId) { + AppleUser appleUser = appleUserRepository.findAppleUserByUserId(userId) + .orElseThrow(() -> new NotFoundException(USER_NOT_FOUND_APPLE)); + + String refreshToken = appleUser.getAppleRefreshToken(); + String clientSecret = createClientSecret(); + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("client_id", clientId); + params.add("client_secret", clientSecret); + params.add("token", refreshToken); + params.add("token_type_hint", "refresh_token"); + + URI uri = UriComponentsBuilder + .fromUriString(host + "/auth/oauth2/v2/revoke") + .build() + .toUri(); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); + HttpEntity> httpEntity = new HttpEntity<>(params, headers); + + try { + restTemplate.postForEntity(uri, httpEntity, AppleUserData.class); + appleUserRepository.delete(appleUser); + } catch (RestClientException e) { + throw new HttpClientException(ErrorCode.HTTP_CLIENT_REQUEST_FAILED); + } } private String createClientSecret() { - ZonedDateTime expiration = ZonedDateTime.now().plusMinutes(5); + Date expirationDate = Date.from(ZonedDateTime.now().plusDays(30).toInstant()); + + Map jwtHeader = new HashMap<>(); + jwtHeader.put(JwsHeader.KEY_ID, keyId); + jwtHeader.put(JwsHeader.ALGORITHM, "ES256"); return Jwts.builder() - .setHeaderParam(JwsHeader.KEY_ID, keyId) + .setHeaderParams(jwtHeader) .setIssuer(teamId) .setAudience(host) .setSubject(clientId) - .setExpiration(Date.from(expiration.withZoneSameInstant(ZoneId.systemDefault()).toInstant())) - .setIssuedAt(new Date()) + .setExpiration(expirationDate) + .setIssuedAt(new Date(System.currentTimeMillis())) .signWith(getPrivateKey(), SignatureAlgorithm.ES256) .compact(); } @@ -139,19 +172,22 @@ private PrivateKey getPrivateKey() { } } - private OAuthUserDataResponse decodePayload(String idToken, String nickname) { + private OAuthUserDataResponse decodePayload(String idToken, String nickname, String refreshToken) { try { DecodedJWT decoded = JWT.decode(idToken); Map claims = decoded.getClaims(); String providerUid = decoded.getSubject(); - String email = claims.get("email").asString(); + String email = claims.get("sub").asString(); + log.info("[AppleService] email = {}", email); + log.info("[AppleService] nickname = {}", nickname); return OAuthUserDataResponse.builder() .provider(getAuthProvider().toString()) .oauthId(providerUid) .email(email) .nickname(nickname) + .appleRefreshToken(refreshToken) .build(); } catch (Exception e) { throw new RuntimeException("Error decoding payload", e); diff --git a/src/main/java/com/moneymong/global/security/oauth/handler/KakaoService.java b/src/main/java/com/moneymong/global/security/oauth/handler/KakaoService.java index ce3d71c..e889907 100644 --- a/src/main/java/com/moneymong/global/security/oauth/handler/KakaoService.java +++ b/src/main/java/com/moneymong/global/security/oauth/handler/KakaoService.java @@ -69,7 +69,7 @@ public OAuthUserDataResponse getOAuthUserData(OAuthUserDataRequest request) { } @Override - public void unlink(String token) { + public void unlink(Long userId) { } } diff --git a/src/main/java/com/moneymong/global/security/oauth/handler/OAuthAuthenticationHandler.java b/src/main/java/com/moneymong/global/security/oauth/handler/OAuthAuthenticationHandler.java index 7e07524..dfee4aa 100644 --- a/src/main/java/com/moneymong/global/security/oauth/handler/OAuthAuthenticationHandler.java +++ b/src/main/java/com/moneymong/global/security/oauth/handler/OAuthAuthenticationHandler.java @@ -9,5 +9,5 @@ public interface OAuthAuthenticationHandler { OAuthUserDataResponse getOAuthUserData(OAuthUserDataRequest request); - void unlink(String token); + void unlink(Long userId); } diff --git a/src/main/java/com/moneymong/global/security/service/OAuthService.java b/src/main/java/com/moneymong/global/security/service/OAuthService.java index a6161a1..af79056 100644 --- a/src/main/java/com/moneymong/global/security/service/OAuthService.java +++ b/src/main/java/com/moneymong/global/security/service/OAuthService.java @@ -2,9 +2,14 @@ import com.moneymong.domain.user.api.request.LoginRequest; import com.moneymong.domain.user.api.request.UserDeleteRequest; +import com.moneymong.domain.user.entity.User; +import com.moneymong.domain.user.repository.UserRepository; +import com.moneymong.global.exception.custom.NotFoundException; +import com.moneymong.global.exception.enums.ErrorCode; import com.moneymong.global.security.oauth.dto.OAuthUserDataRequest; import com.moneymong.global.security.oauth.dto.OAuthUserDataResponse; import com.moneymong.global.security.oauth.handler.OAuthAuthenticationHandler; +import jakarta.persistence.EntityNotFoundException; import org.springframework.stereotype.Service; import java.util.List; @@ -12,15 +17,19 @@ import java.util.function.Function; import java.util.stream.Collectors; +import static com.moneymong.global.exception.enums.ErrorCode.USER_NOT_FOUND; + @Service public class OAuthService { private final Map oAuthAuthenticationHandlers; + private final UserRepository userRepository; - public OAuthService(List oAuthAuthenticationHandlers) { + public OAuthService(List oAuthAuthenticationHandlers, UserRepository userRepository) { this.oAuthAuthenticationHandlers = oAuthAuthenticationHandlers.stream().collect( Collectors.toConcurrentMap(OAuthAuthenticationHandler::getAuthProvider, Function.identity()) ); + this.userRepository = userRepository; } public OAuthUserDataResponse login(LoginRequest loginRequest) { @@ -37,10 +46,14 @@ public OAuthUserDataResponse login(LoginRequest loginRequest) { return oAuthHandler.getOAuthUserData(request); } - public void revoke(UserDeleteRequest deleteRequest) { - OAuthProvider oAuthProvider = OAuthProvider.get(deleteRequest.getProvider()); + public void revoke(Long userId) { + User user = userRepository + .findById(userId) + .orElseThrow(() -> new NotFoundException(USER_NOT_FOUND)); + + OAuthProvider oAuthProvider = OAuthProvider.get(user.getProvider()); OAuthAuthenticationHandler oAuthHandler = this.oAuthAuthenticationHandlers.get(oAuthProvider); - oAuthHandler.unlink(deleteRequest.getToken()); + oAuthHandler.unlink(userId); } }