Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,18 @@ public class UnRegisteredUser extends BaseEntity {
@Column(name = "provider_id", length = 255)
private String providerId;

@Column(name = "apple_refresh_token", length = 1024)
private String appleRefreshToken;

@Builder
private UnRegisteredUser(Integer id, String email, Provider provider, String providerId) {
this.id = id;
this.email = email;
this.provider = provider;
this.providerId = providerId;
}

public void updateAppleRefreshToken(String appleRefreshToken) {
this.appleRefreshToken = appleRefreshToken;
}
}
12 changes: 11 additions & 1 deletion src/main/java/gg/agit/konect/domain/user/model/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ public class User extends BaseEntity {
@Column(name = "image_url")
private String imageUrl;

@Column(name = "apple_refresh_token", length = 1024)
private String appleRefreshToken;

@Builder
private User(
Integer id,
Expand All @@ -99,7 +102,8 @@ private User(
String providerId,
UserRole role,
Boolean isMarketingAgreement,
String imageUrl
String imageUrl,
String appleRefreshToken
) {
this.id = id;
this.university = university;
Expand All @@ -112,6 +116,7 @@ private User(
this.role = role == null ? UserRole.USER : role;
this.isMarketingAgreement = isMarketingAgreement;
this.imageUrl = imageUrl;
this.appleRefreshToken = appleRefreshToken;
}

public static User of(
Expand All @@ -131,6 +136,7 @@ public static User of(
.providerId(tempUser.getProviderId())
.isMarketingAgreement(isMarketingAgreement)
.imageUrl(imageUrl)
.appleRefreshToken(tempUser.getAppleRefreshToken())
.build();
}

Expand Down Expand Up @@ -161,4 +167,8 @@ public String getStudentNumberYear() {
public boolean isAdmin() {
return this.role.equals(UserRole.ADMIN);
}

public void updateAppleRefreshToken(String appleRefreshToken) {
this.appleRefreshToken = appleRefreshToken;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import gg.agit.konect.domain.user.repository.UserRepository;
import gg.agit.konect.global.code.ApiResponseCode;
import gg.agit.konect.global.exception.CustomException;
import gg.agit.konect.infrastructure.oauth.AppleTokenRevocationService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

Expand All @@ -56,6 +57,7 @@ public class UserService {
private final CouncilNoticeReadRepository councilNoticeReadRepository;
private final StudyTimeQueryService studyTimeQueryService;
private final ApplicationEventPublisher applicationEventPublisher;
private final AppleTokenRevocationService appleTokenRevocationService;

@Transactional
public Integer signup(String email, String providerId, Provider provider, SignupRequest request) {
Expand Down Expand Up @@ -196,6 +198,11 @@ public void deleteUser(Integer userId) {

validateNotClubPresident(userId);
validatePaidFees(userId);

if (user.getProvider() == Provider.APPLE) {
appleTokenRevocationService.revoke(user.getAppleRefreshToken());
}

userRepository.delete(user);

applicationEventPublisher.publishEvent(UserWithdrawnEvent.from(user.getEmail()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,20 @@
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Value;
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.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import gg.agit.konect.domain.user.enums.Provider;
import gg.agit.konect.domain.user.model.UnRegisteredUser;
import gg.agit.konect.domain.user.model.User;
import gg.agit.konect.domain.user.repository.UnRegisteredUserRepository;
import gg.agit.konect.domain.user.repository.UserRepository;
import gg.agit.konect.domain.user.service.RefreshTokenService;
import gg.agit.konect.domain.user.service.SignupTokenService;
import gg.agit.konect.global.auth.web.AuthCookieService;
Expand All @@ -24,7 +30,9 @@
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
@RequiredArgsConstructor
public class OAuth2LoginSuccessHandler implements AuthenticationSuccessHandler {
Expand All @@ -37,6 +45,9 @@ public class OAuth2LoginSuccessHandler implements AuthenticationSuccessHandler {
private final SignupTokenService signupTokenService;
private final RefreshTokenService refreshTokenService;
private final AuthCookieService authCookieService;
private final OAuth2AuthorizedClientService authorizedClientService;
private final UserRepository userRepository;
private final UnRegisteredUserRepository unRegisteredUserRepository;

@Override
public void onAuthenticationSuccess(
Expand All @@ -60,6 +71,8 @@ public void onAuthenticationSuccess(
}
}

String appleRefreshTokenValue = extractAppleRefreshToken(oauthToken);

user = oauthLoginHelper.findUserByProvider(provider, email, providerId);

if (user.isEmpty()) {
Expand All @@ -71,10 +84,12 @@ public void onAuthenticationSuccess(
}
}

saveAppleRefreshTokenForUnRegisteredUser(provider, providerId, email, appleRefreshTokenValue);
sendAdditionalInfoRequiredResponse(request, response, email, provider, providerId);
return;
}

saveAppleRefreshTokenForUser(provider, user.get(), appleRefreshTokenValue);
sendLoginSuccessResponse(request, response, user.get());
}

Expand Down Expand Up @@ -158,4 +173,53 @@ private String extractProviderId(OAuth2User oauthUser) {

return providerId;
}

private String extractAppleRefreshToken(OAuth2AuthenticationToken oauthToken) {
String registrationId = oauthToken.getAuthorizedClientRegistrationId();

if (!Provider.APPLE.name().equalsIgnoreCase(registrationId)) {
return null;
}

OAuth2AuthorizedClient client = authorizedClientService.loadAuthorizedClient(
registrationId, oauthToken.getName()
);

if (client == null) {
return null;
}

OAuth2RefreshToken refreshToken = client.getRefreshToken();
return refreshToken != null ? refreshToken.getTokenValue() : null;
}

private void saveAppleRefreshTokenForUser(Provider provider, User user, String appleRefreshToken) {
if (provider != Provider.APPLE || !StringUtils.hasText(appleRefreshToken)) {
return;
}

user.updateAppleRefreshToken(appleRefreshToken);
userRepository.save(user);
}

private void saveAppleRefreshTokenForUnRegisteredUser(
Provider provider, String providerId, String email, String appleRefreshToken
) {
if (provider != Provider.APPLE || !StringUtils.hasText(appleRefreshToken)) {
return;
}

Optional<UnRegisteredUser> unRegisteredUser;

if (StringUtils.hasText(providerId)) {
unRegisteredUser = unRegisteredUserRepository.findByProviderIdAndProvider(providerId, Provider.APPLE);
} else {
unRegisteredUser = unRegisteredUserRepository.findByEmailAndProvider(email, Provider.APPLE);
}

unRegisteredUser.ifPresent(u -> {
u.updateAppleRefreshToken(appleRefreshToken);
unRegisteredUserRepository.save(u);
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package gg.agit.konect.infrastructure.oauth;

import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
@RequiredArgsConstructor
public class AppleTokenRevocationService {

private static final String APPLE_REVOKE_URL = "https://appleid.apple.com/auth/revoke";

private final RestTemplate restTemplate;
private final AppleClientSecretProvider appleClientSecretProvider;
private final AppleOAuthProperties appleOAuthProperties;

public void revoke(String appleRefreshToken) {
if (!StringUtils.hasText(appleRefreshToken)) {
log.warn("Apple refresh token이 없어 revoke를 건너뜁니다.");
return;
}

MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("client_id", appleOAuthProperties.getClientId());
params.add("client_secret", appleClientSecretProvider.getClientSecret());
params.add("token", appleRefreshToken);
params.add("token_type_hint", "refresh_token");

HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(params, headers);

try {
ResponseEntity<String> response = restTemplate.postForEntity(APPLE_REVOKE_URL, request, String.class);

if (response.getStatusCode().is2xxSuccessful()) {
log.info("Apple 토큰 revoke 완료");
}
} catch (HttpClientErrorException e) {
log.error("Apple 토큰 revoke 실패. status={}, body={}", e.getStatusCode(), e.getResponseBodyAsString());
throw new IllegalStateException("Apple 토큰 revoke에 실패했습니다.", e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE users ADD COLUMN apple_refresh_token VARCHAR(1024) NULL;
ALTER TABLE unregistered_user ADD COLUMN apple_refresh_token VARCHAR(1024) NULL;