Skip to content

Commit

Permalink
[NAYB-152] feat: 회원 탈퇴 시 인증 서버 액세스 토큰 만료 기간 검증
Browse files Browse the repository at this point in the history
[NAYB-152] feat: 회원 탈퇴 시 인증 서버 액세스 토큰 만료 기간 검증
  • Loading branch information
hseong3243 committed Sep 17, 2023
2 parents bc29383 + 766c1b8 commit b52b2e4
Show file tree
Hide file tree
Showing 7 changed files with 217 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public ResponseEntity<FindUserDetailResponse> findUser(@LoginUser Long userId) {
return ResponseEntity.ok(findUserDetailResponse);
}

@DeleteMapping("/users")
@DeleteMapping("/users/me")
public ResponseEntity<Void> deleteUser(@LoginUser Long userId) {
FindUserCommand findUserDetailCommand = FindUserCommand.from(userId);
FindUserDetailResponse findUserDetailResponse = userService.findUser(findUserDetailCommand);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,44 @@
import com.prgrms.nabmart.domain.user.service.response.FindUserDetailResponse;
import com.prgrms.nabmart.global.auth.exception.OAuthUnlinkFailureException;
import com.prgrms.nabmart.global.auth.oauth.dto.OAuthHttpMessage;
import java.text.MessageFormat;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AccessToken.TokenType;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;

@RequiredArgsConstructor
public class KakaoMessageProvider implements OAuthHttpMessageProvider {

private static final String UNLINK_URI = "https://kapi.kakao.com/v1/user/unlink";
private static final String ACCESS_TOKEN_REFRESH_URI = "https://kauth.kakao.com/oauth/token";
private static final String CONTENT_TYPE = "Content-Type";
private static final String AUTHORIZATION = "Authorization";
private static final String TARGET_ID_TYPE = "target_id_type";
private static final String USER_ID = "user_id";
private static final String TARGET_ID = "target_id";
private static final String ID = "id";
private static final String APPLICATION_X_WWW_FORM_URLENCODED_CHARSET_UTF_8
= "application/x-www-form-urlencoded;charset=utf-8";
private static final String GRANT_TYPE = "grant_type";
private static final String REFRESH_TOKEN = "refresh_token";
private static final String CLIENT_ID = "client_id";
private static final String CLIENT_SECRET = "client_secret";
private static final String ACCESS_TOKEN = "access_token";
private static final String EXPIRES_IN = "expires_in";

@Override
public OAuthHttpMessage createUnlinkHttpMessage(
public OAuthHttpMessage createUserUnlinkRequest(
final FindUserDetailResponse userDetailResponse,
final OAuth2AuthorizedClient authorizedClient) {
String accessToken = getAccessToken(authorizedClient);
Expand All @@ -42,22 +63,69 @@ private HttpEntity<MultiValueMap<String, String>> createUnlinkOAuthUserMessage(

private HttpHeaders createHeader(final String accessToken) {
HttpHeaders headers = new HttpHeaders();
headers.set("Content-Type", "application/x-www-form-urlencoded");
headers.set("Authorization", "Bearer " + accessToken);
headers.set(CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE);
headers.set(AUTHORIZATION, MessageFormat.format("Bearer {0}", accessToken));
return headers;
}

private MultiValueMap<String, String> createUnlinkMessageBody(
final FindUserDetailResponse userDetailResponse) {
MultiValueMap<String, String> multiValueMap = new LinkedMultiValueMap<>();
multiValueMap.add("target_id_type", "user_id");
multiValueMap.add("target_id", String.valueOf(userDetailResponse.providerId()));
multiValueMap.add(TARGET_ID_TYPE, USER_ID);
multiValueMap.add(TARGET_ID, String.valueOf(userDetailResponse.providerId()));
return multiValueMap;
}

@Override
public void checkSuccessUnlinkRequest(Map<String, Object> unlinkResponse) {
Optional.ofNullable(unlinkResponse.get("id"))
Optional.ofNullable(unlinkResponse.get(ID))
.orElseThrow(() -> new OAuthUnlinkFailureException("소셜 로그인 연동 해제가 실패하였습니다."));
}

@Override
public OAuthHttpMessage createRefreshAccessTokenRequest(OAuth2AuthorizedClient authorizedClient) {
return new OAuthHttpMessage(
ACCESS_TOKEN_REFRESH_URI,
createRefreshAccessTokenMessage(authorizedClient),
new HashMap<>());
}

private HttpEntity<MultiValueMap<String, String>> createRefreshAccessTokenMessage(
OAuth2AuthorizedClient authorizedClient) {
return new HttpEntity<>(
createRefreshAccessTokenMessageBody(authorizedClient),
createRefreshAccessTokenMessageHeader());
}

private MultiValueMap<String, String> createRefreshAccessTokenMessageHeader() {
MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
headers.add(CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED_CHARSET_UTF_8);
return headers;
}

private MultiValueMap<String, String> createRefreshAccessTokenMessageBody(
OAuth2AuthorizedClient authorizedClient) {
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add(GRANT_TYPE, REFRESH_TOKEN);
params.add(CLIENT_ID, authorizedClient.getClientRegistration().getClientId());
params.add(REFRESH_TOKEN, authorizedClient.getRefreshToken().getTokenValue());
params.add(CLIENT_SECRET, authorizedClient.getClientRegistration().getClientSecret());
return params;
}

@Override
public OAuth2AccessToken extractAccessToken(Map response) {
String accessToken = (String) response.get(ACCESS_TOKEN);
Instant now = Instant.now();
Integer expiresInSeconds = (Integer) response.get(EXPIRES_IN);
Instant expiresIn = now.plusSeconds(expiresInSeconds);
return new OAuth2AccessToken(TokenType.BEARER, accessToken, now, expiresIn);
}

@Override
public Optional<OAuth2RefreshToken> extractRefreshToken(Map response) {
String refreshToken = (String) response.get(REFRESH_TOKEN);
return Optional.ofNullable(refreshToken)
.map(token -> new OAuth2RefreshToken(token, Instant.now()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,46 @@
import com.prgrms.nabmart.domain.user.service.response.FindUserDetailResponse;
import com.prgrms.nabmart.global.auth.exception.OAuthUnlinkFailureException;
import com.prgrms.nabmart.global.auth.oauth.dto.OAuthHttpMessage;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AccessToken.TokenType;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;

@Slf4j
public class NaverMessageProvider implements OAuthHttpMessageProvider {

private static final String UNLINK_URI = "https://nid.naver.com/oauth2.0/token?"
+ "client_id={client_id}&client_secret={client_secret}&access_token={access_token}&"
+ "grant_type={grant_type}&service_provider={service_provider}";
private static final String REFRESH_ACCESS_TOKEN_URI = "https://nid.naver.com/oauth2.0/token?"
+ "grant_type=refresh_token&client_id={client_id}&"
+ "client_secret={client_secret}&refresh_token={refresh_token}";
private static final String CONTENT_TYPE = "Content-Type";
private static final String CLIENT_ID = "client_id";
private static final String CLIENT_SECRET = "client_secret";
private static final String ACCESS_TOKEN = "access_token";
private static final String GRANT_TYPE = "grant_type";
private static final String SERVICE_PROVIDER = "service_provider";
private static final String REFRESH_TOKEN = "refresh_token";
private static final String EXPIRES_IN = "expires_in";
private static final String DELETE = "delete";
private static final String NAVER = "NAVER";
private static final String RESULT = "result";
private static final String SUCCESS = "success";

@Override
public OAuthHttpMessage createUnlinkHttpMessage(
public OAuthHttpMessage createUserUnlinkRequest(
final FindUserDetailResponse userDetailResponse,
final OAuth2AuthorizedClient authorizedClient) {
String accessToken = getAccessToken(authorizedClient);
Expand All @@ -40,26 +63,66 @@ private HttpEntity<MultiValueMap<String, String>> createUnlinkOAuthUserMessage()

private HttpHeaders createHeader() {
HttpHeaders headers = new HttpHeaders();
headers.set("Content-Type", "application/json");
headers.set(CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
return headers;
}

private Map<String, String> createUnlinkUriVariables(
final ClientRegistration clientRegistration,
final String accessToken) {
Map<String, String> urlVariables = new HashMap<>();
urlVariables.put("client_id", clientRegistration.getClientId());
urlVariables.put("client_secret", clientRegistration.getClientSecret());
urlVariables.put("access_token", accessToken);
urlVariables.put("grant_type", "delete");
urlVariables.put("service_provider", "NAVER");
urlVariables.put(CLIENT_ID, clientRegistration.getClientId());
urlVariables.put(CLIENT_SECRET, clientRegistration.getClientSecret());
urlVariables.put(ACCESS_TOKEN, accessToken);
urlVariables.put(GRANT_TYPE, DELETE);
urlVariables.put(SERVICE_PROVIDER, NAVER);
return urlVariables;
}

@Override
public void checkSuccessUnlinkRequest(Map<String, Object> unlinkResponse) {
Optional.ofNullable(unlinkResponse.get("result"))
.filter(result -> result.equals("success"))
Optional.ofNullable(unlinkResponse.get(RESULT))
.filter(result -> result.equals(SUCCESS))
.orElseThrow(() -> new OAuthUnlinkFailureException("소셜 로그인 연동 해제가 실패하였습니다."));
}

@Override
public OAuthHttpMessage createRefreshAccessTokenRequest(
OAuth2AuthorizedClient authorizedClient) {
return new OAuthHttpMessage(
REFRESH_ACCESS_TOKEN_URI,
createEmptyMessage(),
createRefreshAccessTokenUriVariables(authorizedClient));
}

private HttpEntity<MultiValueMap<String, String>> createEmptyMessage() {
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
return new HttpEntity<>(map, map);
}

private Map<String, String> createRefreshAccessTokenUriVariables(
OAuth2AuthorizedClient authorizedClient) {
Map<String, String> variables = new HashMap<>();
variables.put(CLIENT_ID, authorizedClient.getClientRegistration().getClientId());
variables.put(CLIENT_SECRET, authorizedClient.getClientRegistration().getClientSecret());
variables.put(REFRESH_TOKEN, authorizedClient.getRefreshToken().getTokenValue());
variables.put(GRANT_TYPE, REFRESH_TOKEN);
return variables;
}

@Override
public OAuth2AccessToken extractAccessToken(Map response) {
String accessToken = (String) response.get(ACCESS_TOKEN);
String expiresInSeconds = (String) response.get(EXPIRES_IN);
Instant now = Instant.now();
Instant expiresIn = now.plusSeconds(Long.parseLong(expiresInSeconds));
return new OAuth2AccessToken(TokenType.BEARER, accessToken, now, expiresIn);
}

@Override
public Optional<OAuth2RefreshToken> extractRefreshToken(Map response) {
String refreshToken = (String) response.get(REFRESH_TOKEN);
return Optional.ofNullable(refreshToken)
.map(token -> new OAuth2RefreshToken(token, Instant.now()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,22 @@
import com.prgrms.nabmart.domain.user.service.response.FindUserDetailResponse;
import com.prgrms.nabmart.global.auth.oauth.dto.OAuthHttpMessage;
import java.util.Map;
import java.util.Optional;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;

public interface OAuthHttpMessageProvider {

OAuthHttpMessage createUnlinkHttpMessage(
OAuthHttpMessage createUserUnlinkRequest(
final FindUserDetailResponse userDetailResponse,
final OAuth2AuthorizedClient authorizedClient);

void checkSuccessUnlinkRequest(Map<String, Object> unlinkResponse);

OAuthHttpMessage createRefreshAccessTokenRequest(OAuth2AuthorizedClient authorizedClient);

OAuth2AccessToken extractAccessToken(Map response);

Optional<OAuth2RefreshToken> extractRefreshToken(Map response);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@
public interface OAuthRestClient {

void callUnlinkOAuthUser(FindUserDetailResponse userDetailResponse);

void refreshAccessToken(FindUserDetailResponse userDetailResponse);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,16 @@
import com.prgrms.nabmart.domain.user.service.response.FindUserDetailResponse;
import com.prgrms.nabmart.global.auth.oauth.dto.OAuthHttpMessage;
import com.prgrms.nabmart.global.auth.oauth.handler.OAuthProvider;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;

Expand All @@ -29,15 +35,60 @@ public void callUnlinkOAuthUser(final FindUserDetailResponse userDetailResponse)
OAuth2AuthorizedClient oAuth2AuthorizedClient = authorizedClientService.loadAuthorizedClient(
userDetailResponse.provider(),
userDetailResponse.providerId());
OAuthHttpMessage unlinkHttpMessage = oAuthHttpMessageProvider.createUnlinkHttpMessage(

Instant expiresAt = oAuth2AuthorizedClient.getAccessToken().getExpiresAt();
if(expiresAt.isBefore(Instant.now())) {
refreshAccessToken(userDetailResponse);
}

OAuthHttpMessage unlinkHttpMessage = oAuthHttpMessageProvider.createUserUnlinkRequest(
userDetailResponse, oAuth2AuthorizedClient);
Map<String, Object> response = restTemplate.postForObject(
unlinkHttpMessage.uri(),
unlinkHttpMessage.httpMessage(),
Map.class,
unlinkHttpMessage.uriVariables());
Map<String, Object> response = sendPostApiRequest(unlinkHttpMessage);
log.info("회원의 연결이 종료되었습니다. 회원 ID={}", response);
oAuthHttpMessageProvider.checkSuccessUnlinkRequest(response);
}

@Override
public void refreshAccessToken(final FindUserDetailResponse userDetailResponse) {
OAuthProvider oAuthProvider = OAuthProvider.getOAuthProvider(userDetailResponse.provider());
OAuthHttpMessageProvider oAuthHttpMessageProvider = oAuthProvider.getOAuthHttpMessageProvider();
OAuth2AuthorizedClient oAuth2AuthorizedClient = authorizedClientService.loadAuthorizedClient(
userDetailResponse.provider(),
userDetailResponse.providerId());
OAuthHttpMessage refreshAccessTokenRequest
= oAuthHttpMessageProvider.createRefreshAccessTokenRequest(oAuth2AuthorizedClient);

Map response = sendPostApiRequest(refreshAccessTokenRequest);

OAuth2AccessToken refreshedAccessToken
= oAuthHttpMessageProvider.extractAccessToken(response);
OAuth2RefreshToken refreshedRefreshToken
= oAuthHttpMessageProvider.extractRefreshToken(response)
.orElse(oAuth2AuthorizedClient.getRefreshToken());

OAuth2AuthorizedClient updatedAuthorizedClient = new OAuth2AuthorizedClient(
oAuth2AuthorizedClient.getClientRegistration(),
oAuth2AuthorizedClient.getPrincipalName(),
refreshedAccessToken,
refreshedRefreshToken);
String principalName = updatedAuthorizedClient.getPrincipalName();
Authentication authenticationForTokenRefresh
= getAuthenticationForTokenRefresh(principalName);
authorizedClientService.saveAuthorizedClient(
updatedAuthorizedClient,
authenticationForTokenRefresh);
}

private Authentication getAuthenticationForTokenRefresh(String principalName) {
return UsernamePasswordAuthenticationToken.authenticated(
principalName, null, List.of());
}

private Map sendPostApiRequest(OAuthHttpMessage refreshAccessTokenRequest) {
return restTemplate.postForObject(
refreshAccessTokenRequest.uri(),
refreshAccessTokenRequest.httpMessage(),
Map.class,
refreshAccessTokenRequest.uriVariables());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ void DeleteUser() throws Exception {
given(userService.findUser(any())).willReturn(findUserDetailResponse);

//when
ResultActions resultActions = mockMvc.perform(delete("/api/v1/users")
ResultActions resultActions = mockMvc.perform(delete("/api/v1/users/me")
.header("Authorization", accessToken));

//then
Expand Down

0 comments on commit b52b2e4

Please sign in to comment.