Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 회원 탈퇴 시 OAuth token을 revoke한다. #1

Merged
merged 9 commits into from
May 26, 2024
1 change: 1 addition & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ on:
- main
- dev


# 권한 설정
permissions: write-all

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<AppleUser, Long> {
Optional<AppleUser> findAppleUserByUserId(Long userId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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(
Expand All @@ -65,7 +70,6 @@ public User registerUser(OAuthUserInfo oauthUserInfo) {
return newUser;
}


@Transactional(readOnly = true)
public UserProfileResponse getUserProfile(Long userId) {
User user = userRepository.findById(userId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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", "회원의 대학 정보가 존재하지 않습니다."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -29,18 +32,18 @@
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
@RequiredArgsConstructor
public class AppleService implements OAuthAuthenticationHandler {

private final RestTemplate restTemplate;
private final AppleUserRepository appleUserRepository;

@Value("${spring.security.oauth2.apple.host}")
private String host;
Expand All @@ -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();
Expand Down Expand Up @@ -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<String, String> 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<MultiValueMap<String, String>> 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<String, Object> 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();
}
Expand All @@ -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<String, Claim> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ public OAuthUserDataResponse getOAuthUserData(OAuthUserDataRequest request) {
}

@Override
public void unlink(String token) {
public void unlink(Long userId) {

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ public interface OAuthAuthenticationHandler {

OAuthUserDataResponse getOAuthUserData(OAuthUserDataRequest request);

void unlink(String token);
void unlink(Long userId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,34 @@

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;
import java.util.Map;
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<OAuthProvider, OAuthAuthenticationHandler> oAuthAuthenticationHandlers;
private final UserRepository userRepository;

public OAuthService(List<OAuthAuthenticationHandler> oAuthAuthenticationHandlers) {
public OAuthService(List<OAuthAuthenticationHandler> oAuthAuthenticationHandlers, UserRepository userRepository) {
this.oAuthAuthenticationHandlers = oAuthAuthenticationHandlers.stream().collect(
Collectors.toConcurrentMap(OAuthAuthenticationHandler::getAuthProvider, Function.identity())
);
this.userRepository = userRepository;
}

public OAuthUserDataResponse login(LoginRequest loginRequest) {
Expand All @@ -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);
}
}
Loading