Skip to content

Commit

Permalink
Merge pull request #677 from woowacourse-teams/feature/#666
Browse files Browse the repository at this point in the history
회원 탈퇴 시 애플 서버에게 회원 권한 삭제를 요청하는 기능 구현
  • Loading branch information
Mingyum-Kim authored Oct 18, 2024
2 parents 04507dd + dbdcc30 commit 5661a23
Show file tree
Hide file tree
Showing 13 changed files with 138 additions and 68 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.springframework.web.client.RestClient;

import lombok.RequiredArgsConstructor;
import mouda.backend.auth.Infrastructure.response.AppleRefreshTokenResponse;
import mouda.backend.auth.implement.jwt.ClientSecretProvider;
import mouda.backend.auth.presentation.response.OauthResponse;

Expand All @@ -18,7 +19,7 @@
public class AppleOauthClient implements OauthClient {

public static final String CLIENT_ID = "site.mouda.backend";
private static final String APPLE_API_URL = "https://appleid.apple.com/auth/token";
private static final String APPLE_API_URL = "https://appleid.apple.com/auth";
private static final String GRANT_TYPE = "authorization_code";

private final RestClient restClient;
Expand All @@ -29,21 +30,43 @@ public class AppleOauthClient implements OauthClient {

@Override
public String getIdToken(String code) {
String tokenUrl = APPLE_API_URL + "/token";
MultiValueMap<String, String> formData = getFormData(code);

OauthResponse oauthResponse = restClient.method(HttpMethod.POST)
.uri(APPLE_API_URL)
.uri(tokenUrl)
.headers(httpHeaders -> httpHeaders.addAll(getHttpHeaders()))
.body(formData)
.retrieve()
.body(OauthResponse.class);
return oauthResponse.id_token();
}

private HttpHeaders getHttpHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
return headers;
public String getRefreshToken(String code) {
String tokenUrl = APPLE_API_URL + "/token";
MultiValueMap<String, String> formData = getFormData(code);

AppleRefreshTokenResponse response = restClient.method(HttpMethod.POST)
.uri(tokenUrl)
.headers(httpHeaders -> httpHeaders.addAll(getHttpHeaders()))
.body(formData)
.retrieve()
.body(AppleRefreshTokenResponse.class);
return response.refresh_token();
}

public void revoke(String refreshToken) {
String revokeUrl = APPLE_API_URL + "/oauth2/v2/revoke";
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
formData.add("client_id", CLIENT_ID);
formData.add("client_secret", clientSecretProvider.provide());
formData.add("token", refreshToken);
formData.add("token_hint_type", "refresh_token");

restClient.method(HttpMethod.POST)
.uri(revokeUrl)
.headers(httpHeaders -> httpHeaders.addAll(getHttpHeaders()))
.body(formData);
}

private MultiValueMap<String, String> getFormData(String code) {
Expand All @@ -55,4 +78,10 @@ private MultiValueMap<String, String> getFormData(String code) {
formData.add("redirect_uri", redirectUri);
return formData;
}

private HttpHeaders getHttpHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
return headers;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package mouda.backend.auth.Infrastructure.response;

public record AppleRefreshTokenResponse(String refresh_token) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,13 @@

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import mouda.backend.auth.business.result.LoginProcessResult;
import mouda.backend.auth.Infrastructure.AppleOauthClient;
import mouda.backend.auth.exception.AuthErrorMessage;
import mouda.backend.auth.exception.AuthException;
import mouda.backend.auth.implement.AppleOauthManager;
import mouda.backend.auth.implement.LoginManager;
import mouda.backend.auth.implement.jwt.AccessTokenProvider;
import mouda.backend.auth.presentation.controller.AppleUserInfoRequest;
import mouda.backend.auth.presentation.request.AppleOauthRequest;
import mouda.backend.auth.presentation.response.LoginResponse;
import mouda.backend.member.domain.LoginDetail;
import mouda.backend.member.domain.Member;
import mouda.backend.member.domain.OauthType;
Expand All @@ -34,39 +32,41 @@ public class AppleAuthService {
private final AppleOauthManager appleOauthManager;
private final AccessTokenProvider accessTokenProvider;
private final ObjectMapper objectMapper;
private final AppleOauthClient appleOauthClient;

public LoginResponse oauthLogin(AppleOauthRequest oauthRequest) {
Member member = memberFinder.findByNonce(oauthRequest.nonce());
// TODO: 사용자 전환 로직 실행 이전부터 애플 소셜 회원가입이 진행되어있음. 현재 카카오 사용자가 전환을 시도하여 애플 로그인하면 같은 애플 로그인 사용자가 두 명이 되면서 에러가 터질 것.
if (oauthRequest.memberId() != null) {
String accessToken = loginManager.updateOauth(oauthRequest.memberId(), OauthType.APPLE,
member.getSocialLoginId());
return new LoginResponse(accessToken);
}
LoginProcessResult result = loginManager.processAppleLogin(member);
return new LoginResponse(result.accessToken());
}
// TODO: 더 이상 사용하지 않는 로직. 로그인 프로세스 정착 후 제거할 것
// public LoginResponse oauthLogin(AppleOauthRequest oauthRequest) {
// TODO: 사용자 전환 로직 실행 이전부터 애플 소셜 회원가입이 진행되어있음. 현재 카카오 사용자가 전환을 시도하여 애플 로그인하면 같은 애플 로그인 사용자가 두 명이 되면서 에러가 터질 것.
// if (oauthRequest.memberId() != null) {
// String accessToken = loginManager.updateOauth(oauthRequest.memberId(), OauthType.APPLE,
// member.getSocialLoginId());
// return new LoginResponse(accessToken);
// }
// LoginProcessResult result = loginManager.processAppleLogin(member);
// return new LoginResponse(result.accessToken());ap
// }

public String getAccessToken(String idToken) {
String socialLoginId = appleOauthManager.getSocialLoginId(idToken);
Member member = memberFinder.findBySocialId(socialLoginId);
return accessTokenProvider.provide(member);
}

public void save(String idToken, String user) {
public void save(String code, String idToken, String user) {
String refreshToken = appleOauthClient.getRefreshToken(code);
try {
AppleUserInfoRequest request = objectMapper.readValue(user, AppleUserInfoRequest.class);
String firstName = request.name().firstName();
String lastName = request.name().lastName();
saveMember(idToken, firstName, lastName);
saveMember(refreshToken, idToken, firstName, lastName);
} catch (JsonProcessingException exception) {
throw new AuthException(HttpStatus.BAD_REQUEST, AuthErrorMessage.APPLE_USER_BAD_REQUEST);
}
}

private void saveMember(String idToken, String firstName, String lastName) {
private void saveMember(String refreshToken, String idToken, String firstName, String lastName) {
String socialLoginId = appleOauthManager.getSocialLoginId(idToken);
Member member = new Member(lastName + firstName, new LoginDetail(OauthType.APPLE, socialLoginId));
Member member = new Member(lastName + firstName, new LoginDetail(OauthType.APPLE, socialLoginId, refreshToken));
memberWriter.append(member);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package mouda.backend.auth.presentation.business;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import lombok.RequiredArgsConstructor;
import mouda.backend.auth.Infrastructure.AppleOauthClient;
import mouda.backend.member.domain.Member;
import mouda.backend.member.domain.OauthType;
import mouda.backend.member.implement.MemberWriter;

@Transactional
@Service
@RequiredArgsConstructor
public class CommonAuthService {

private final AppleOauthClient oauthClient;
private final MemberWriter memberWriter;

public void withdraw(Member member) {
if (OauthType.APPLE.equals(member.getOauthType())) {
oauthClient.revoke(member.getRefreshToken());
}
memberWriter.remove(member);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ public class AppleAuthController {

@PostMapping("/v1/oauth/apple")
public ResponseEntity<Void> test(
@RequestParam("code") String code,
@RequestParam("id_token") String id_token,
@RequestParam(name = "user", required = false) String user
) throws IOException {
// TODO: 이전에 가입한 적 있지만 DB를 갈아엎어서 user가 들어오지 않는 경우 save에 실패한다.
if (user != null) {
appleAuthService.save(id_token, user);
appleAuthService.save(code, id_token, user);
}
String accessToken = appleAuthService.getAccessToken(id_token);
HttpHeaders httpHeaders = new HttpHeaders();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import org.springframework.context.annotation.Profile;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
Expand All @@ -12,13 +13,15 @@
import mouda.backend.auth.business.AppleAuthService;
import mouda.backend.auth.business.GoogleAuthService;
import mouda.backend.auth.business.KakaoAuthService;
import mouda.backend.auth.presentation.business.CommonAuthService;
import mouda.backend.auth.presentation.controller.swagger.AuthSwagger;
import mouda.backend.auth.presentation.request.AppleOauthRequest;
import mouda.backend.auth.presentation.request.GoogleOauthRequest;
import mouda.backend.auth.presentation.request.OauthRequest;
import mouda.backend.auth.presentation.response.KakaoLoginResponse;
import mouda.backend.auth.presentation.response.LoginResponse;
import mouda.backend.common.config.argumentresolver.LoginMember;
import mouda.backend.common.response.RestResponse;
import mouda.backend.member.domain.Member;

@RestController
@RequestMapping("/v1/auth")
Expand All @@ -28,6 +31,7 @@ public class AuthController implements AuthSwagger {
private final KakaoAuthService kakaoAuthService;
private final AppleAuthService appleAuthService;
private final GoogleAuthService googleAuthService;
private final CommonAuthService commonAuthService;

@Override
@PostMapping("/kakao/oauth")
Expand Down Expand Up @@ -66,11 +70,19 @@ public ResponseEntity<RestResponse<LoginResponse>> loginGoogleOauth(
return ResponseEntity.ok().body(new RestResponse<>(response));
}

// @Override
// @PostMapping("/apple/oauth")
// public ResponseEntity<RestResponse<LoginResponse>> loginAppleOauth(@RequestBody AppleOauthRequest oauthRequest) {
// LoginResponse response = appleAuthService.oauthLogin(oauthRequest);
//
// return ResponseEntity.ok().body(new RestResponse<>(response));
// }

@Override
@PostMapping("/apple/oauth")
public ResponseEntity<RestResponse<LoginResponse>> loginAppleOauth(@RequestBody AppleOauthRequest oauthRequest) {
LoginResponse response = appleAuthService.oauthLogin(oauthRequest);
@DeleteMapping
public ResponseEntity<Void> withdraw(@LoginMember Member member) {
commonAuthService.withdraw(member);

return ResponseEntity.ok().body(new RestResponse<>(response));
return ResponseEntity.ok().build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import mouda.backend.auth.presentation.request.AppleOauthRequest;
import mouda.backend.auth.presentation.request.GoogleOauthRequest;
import mouda.backend.auth.presentation.request.OauthRequest;
import mouda.backend.auth.presentation.response.KakaoLoginResponse;
import mouda.backend.auth.presentation.response.LoginResponse;
import mouda.backend.common.config.argumentresolver.LoginMember;
import mouda.backend.common.response.RestResponse;
import mouda.backend.member.domain.Member;

public interface AuthSwagger {

Expand All @@ -33,15 +34,22 @@ public interface AuthSwagger {
})
ResponseEntity<RestResponse<LoginResponse>> loginBasicOauthHogee();

@Operation(summary = "애플 로그인", description = "애플 Oauth Code를 사용하여 로그인한다(accessToken 발급).")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "로그인 성공!"),
})
ResponseEntity<RestResponse<LoginResponse>> loginAppleOauth(@RequestBody AppleOauthRequest oauthRequest);
// @Operation(summary = "애플 로그인", description = "애플 Oauth Code를 사용하여 로그인한다(accessToken 발급).")
// @ApiResponses({
// @ApiResponse(responseCode = "200", description = "로그인 성공!"),
// })
// ResponseEntity<RestResponse<LoginResponse>> loginAppleOauth(@RequestBody AppleOauthRequest oauthRequest);

@Operation(summary = "구글 oauth 로그인", description = "구글 Oauth Code 를 사용하여 로그인한다(accessToken 발급).")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "로그인 성공!"),
})
ResponseEntity<RestResponse<LoginResponse>> loginGoogleOauth(@RequestBody GoogleOauthRequest googleOauthRequest);

@Operation(summary = "회원 탈퇴", description = "로그인한 회원을 서비스에서 탈퇴 처리한다.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "회원 탈퇴 성공!"),
})
ResponseEntity<Void> withdraw(@LoginMember Member member);

}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package mouda.backend.auth.presentation.request;

public record AppleOauthRequest(
Long memberId,
String nonce
Long memberId
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public class LoginDetail {

private String socialLoginId;

private String nonce;
private String refreshToken;

protected LoginDetail() {
}
Expand All @@ -26,10 +26,10 @@ public LoginDetail(OauthType oauthType, String socialLoginId) {
this.socialLoginId = socialLoginId;
}

public LoginDetail(OauthType oauthType, String socialLoginId, String nonce) {
public LoginDetail(OauthType oauthType, String socialLoginId, String refreshToken) {
this.oauthType = oauthType;
this.socialLoginId = socialLoginId;
this.nonce = nonce;
this.refreshToken = refreshToken;
}

@Override
Expand Down
8 changes: 6 additions & 2 deletions backend/src/main/java/mouda/backend/member/domain/Member.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,12 @@ public String getSocialLoginId() {
return loginDetail.getSocialLoginId();
}

public String getOauthType() {
return loginDetail.getOauthType().toString();
public OauthType getOauthType() {
return loginDetail.getOauthType();
}

public String getRefreshToken() {
return loginDetail.getRefreshToken();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,4 @@ public Member findByMemberId(long memberId) {
return memberRepository.findById(memberId)
.orElseThrow(() -> new AuthException(HttpStatus.NOT_FOUND, AuthErrorMessage.MEMBER_NOT_FOUND));
}

public Member findByNonce(String nonce) {
return memberRepository.findByLoginDetail_Nonce(nonce)
.orElseThrow(() -> new AuthException(HttpStatus.NOT_FOUND, AuthErrorMessage.MEMBER_NOT_FOUND));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ public interface MemberRepository extends JpaRepository<Member, Long> {

Optional<Member> findByLoginDetail_SocialLoginId(String socialLoginId);

Optional<Member> findByLoginDetail_Nonce(String nonce);

@Query("""
UPDATE Member m
SET m.loginDetail.oauthType = :oauthType, m.loginDetail.socialLoginId = :socialLoginId
Expand Down
Loading

0 comments on commit 5661a23

Please sign in to comment.