From e2136cc095282dda856db71b90fe7104af0c1a8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Thu, 15 Jan 2026 13:46:40 +0900 Subject: [PATCH 01/16] =?UTF-8?q?feat:=20=EC=95=A0=ED=94=8C=20OAuth=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=EC=9D=84=20=EC=9C=84=ED=95=9C=20fly?= =?UTF-8?q?way=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../db/migration/V10__add_provider_id_to_user_tables.sql | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/main/resources/db/migration/V10__add_provider_id_to_user_tables.sql diff --git a/src/main/resources/db/migration/V10__add_provider_id_to_user_tables.sql b/src/main/resources/db/migration/V10__add_provider_id_to_user_tables.sql new file mode 100644 index 00000000..a8a706b6 --- /dev/null +++ b/src/main/resources/db/migration/V10__add_provider_id_to_user_tables.sql @@ -0,0 +1,9 @@ +ALTER TABLE users + ADD COLUMN provider_id VARCHAR(255) NULL AFTER provider, + MODIFY COLUMN provider ENUM ('GOOGLE', 'KAKAO', 'NAVER', 'APPLE') NOT NULL, + ADD CONSTRAINT uq_users_provider_provider_id UNIQUE (provider, provider_id); + +ALTER TABLE unregistered_user + ADD COLUMN provider_id VARCHAR(255) NULL AFTER provider, + MODIFY COLUMN provider ENUM ('GOOGLE', 'KAKAO', 'NAVER', 'APPLE') NOT NULL, + ADD CONSTRAINT uq_unregistered_user_provider_provider_id UNIQUE (provider, provider_id); From 0d459fc783def85da7fe623c9f7ecce4bbda1a34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Thu, 15 Jan 2026 13:48:10 +0900 Subject: [PATCH 02/16] =?UTF-8?q?feat:=20Provider=EC=97=90=20=EC=95=A0?= =?UTF-8?q?=ED=94=8C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/gg/agit/konect/domain/user/enums/Provider.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/gg/agit/konect/domain/user/enums/Provider.java b/src/main/java/gg/agit/konect/domain/user/enums/Provider.java index 92c5b061..b4d8e76d 100644 --- a/src/main/java/gg/agit/konect/domain/user/enums/Provider.java +++ b/src/main/java/gg/agit/konect/domain/user/enums/Provider.java @@ -4,7 +4,8 @@ public enum Provider { GOOGLE("email"), NAVER("response.email"), - KAKAO("kakao_account.email"); + KAKAO("kakao_account.email"), + APPLE("email"); private final String emailPath; From 2a501e5d58db5d966a5da8ca5911b42578402458 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Thu, 15 Jan 2026 13:48:44 +0900 Subject: [PATCH 03/16] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=97=94=ED=8B=B0=ED=8B=B0=EC=97=90=20provider=5Fi?= =?UTF-8?q?d=20=EC=BB=AC=EB=9F=BC=20=EB=A7=A4=ED=95=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../konect/domain/user/model/UnRegisteredUser.java | 10 +++++++++- .../java/gg/agit/konect/domain/user/model/User.java | 9 +++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/main/java/gg/agit/konect/domain/user/model/UnRegisteredUser.java b/src/main/java/gg/agit/konect/domain/user/model/UnRegisteredUser.java index b0bd2374..20e70a10 100644 --- a/src/main/java/gg/agit/konect/domain/user/model/UnRegisteredUser.java +++ b/src/main/java/gg/agit/konect/domain/user/model/UnRegisteredUser.java @@ -25,6 +25,10 @@ @UniqueConstraint( name = "uq_unregistered_user_email_provider", columnNames = {"email", "provider"} + ), + @UniqueConstraint( + name = "uq_unregistered_user_provider_provider_id", + columnNames = {"provider", "provider_id"} ) } ) @@ -43,10 +47,14 @@ public class UnRegisteredUser extends BaseEntity { @Enumerated(EnumType.STRING) private Provider provider; + @Column(name = "provider_id", length = 255) + private String providerId; + @Builder - private UnRegisteredUser(Integer id, String email, Provider provider) { + private UnRegisteredUser(Integer id, String email, Provider provider, String providerId) { this.id = id; this.email = email; this.provider = provider; + this.providerId = providerId; } } diff --git a/src/main/java/gg/agit/konect/domain/user/model/User.java b/src/main/java/gg/agit/konect/domain/user/model/User.java index df3398da..0a8a963e 100644 --- a/src/main/java/gg/agit/konect/domain/user/model/User.java +++ b/src/main/java/gg/agit/konect/domain/user/model/User.java @@ -36,6 +36,10 @@ @UniqueConstraint( name = "uq_users_university_id_student_number", columnNames = {"university_id", "student_number"} + ), + @UniqueConstraint( + name = "uq_users_provider_provider_id", + columnNames = {"provider", "provider_id"} ) } ) @@ -69,6 +73,9 @@ public class User extends BaseEntity { @Enumerated(EnumType.STRING) private Provider provider; + @Column(name = "provider_id", length = 255) + private String providerId; + @Column(name = "is_marketing_agreement", nullable = false) private Boolean isMarketingAgreement; @@ -84,6 +91,7 @@ private User( String phoneNumber, String studentNumber, Provider provider, + String providerId, Boolean isMarketingAgreement, String imageUrl ) { @@ -94,6 +102,7 @@ private User( this.phoneNumber = phoneNumber; this.studentNumber = studentNumber; this.provider = provider; + this.providerId = providerId; this.isMarketingAgreement = isMarketingAgreement; this.imageUrl = imageUrl; } From 3a1072945b96b8342ece442445c872540441ddf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Thu, 15 Jan 2026 13:58:25 +0900 Subject: [PATCH 04/16] =?UTF-8?q?fix:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/UserController.java | 3 +- .../UnRegisteredUserRepository.java | 16 ++++++++++ .../user/repository/UserRepository.java | 9 ++++++ .../domain/user/service/UserService.java | 31 +++++++++++++++---- 4 files changed, 52 insertions(+), 7 deletions(-) diff --git a/src/main/java/gg/agit/konect/domain/user/controller/UserController.java b/src/main/java/gg/agit/konect/domain/user/controller/UserController.java index 8617c3fa..f0f341d1 100644 --- a/src/main/java/gg/agit/konect/domain/user/controller/UserController.java +++ b/src/main/java/gg/agit/konect/domain/user/controller/UserController.java @@ -33,12 +33,13 @@ public ResponseEntity signup( ) { String email = (String)session.getAttribute("email"); Provider provider = (Provider)session.getAttribute("provider"); + String providerId = (String)session.getAttribute("providerId"); if (email == null || provider == null) { throw CustomException.of(ApiResponseCode.INVALID_SESSION); } - Integer userId = userService.signup(email, provider, request); + Integer userId = userService.signup(email, providerId, provider, request); session.invalidate(); diff --git a/src/main/java/gg/agit/konect/domain/user/repository/UnRegisteredUserRepository.java b/src/main/java/gg/agit/konect/domain/user/repository/UnRegisteredUserRepository.java index 4362a0dc..da0ca880 100644 --- a/src/main/java/gg/agit/konect/domain/user/repository/UnRegisteredUserRepository.java +++ b/src/main/java/gg/agit/konect/domain/user/repository/UnRegisteredUserRepository.java @@ -6,11 +6,27 @@ import gg.agit.konect.domain.user.enums.Provider; import gg.agit.konect.domain.user.model.UnRegisteredUser; +import gg.agit.konect.global.code.ApiResponseCode; +import gg.agit.konect.global.exception.CustomException; public interface UnRegisteredUserRepository extends Repository { Optional findByEmailAndProvider(String email, Provider provider); + Optional findByProviderIdAndProvider(String providerId, Provider provider); + + boolean existsByProviderIdAndProvider(String providerId, Provider provider); + + default UnRegisteredUser getByEmailAndProvider(String email, Provider provider) { + return findByEmailAndProvider(email, provider) + .orElseThrow(() -> CustomException.of(ApiResponseCode.NOT_FOUND_UNREGISTERED_USER)); + } + + default UnRegisteredUser getByProviderIdAndProvider(String providerId, Provider provider) { + return findByProviderIdAndProvider(providerId, provider) + .orElseThrow(() -> CustomException.of(ApiResponseCode.NOT_FOUND_UNREGISTERED_USER)); + } + void save(UnRegisteredUser user); void delete(UnRegisteredUser user); diff --git a/src/main/java/gg/agit/konect/domain/user/repository/UserRepository.java b/src/main/java/gg/agit/konect/domain/user/repository/UserRepository.java index 13beaf1b..72c0719c 100644 --- a/src/main/java/gg/agit/konect/domain/user/repository/UserRepository.java +++ b/src/main/java/gg/agit/konect/domain/user/repository/UserRepository.java @@ -16,6 +16,10 @@ public interface UserRepository extends Repository { Optional findByEmailAndProvider(String email, Provider provider); + Optional findByProviderIdAndProvider(String providerId, Provider provider); + + boolean existsByProviderIdAndProvider(String providerId, Provider provider); + Optional findById(Integer id); default User getById(Integer id) { @@ -23,6 +27,11 @@ default User getById(Integer id) { CustomException.of(ApiResponseCode.NOT_FOUND_USER)); } + default User getByProviderIdAndProvider(String providerId, Provider provider) { + return findByProviderIdAndProvider(providerId, provider) + .orElseThrow(() -> CustomException.of(ApiResponseCode.NOT_FOUND_USER)); + } + boolean existsByUniversityIdAndStudentNumberAndIdNot(Integer universityId, String studentNumber, Integer id); boolean existsByUniversityIdAndStudentNumber(Integer universityId, String studentNumber); diff --git a/src/main/java/gg/agit/konect/domain/user/service/UserService.java b/src/main/java/gg/agit/konect/domain/user/service/UserService.java index fd1baf3c..ee648bb2 100644 --- a/src/main/java/gg/agit/konect/domain/user/service/UserService.java +++ b/src/main/java/gg/agit/konect/domain/user/service/UserService.java @@ -45,15 +45,24 @@ public class UserService { private final StudyTimeQueryService studyTimeQueryService; @Transactional - public Integer signup(String email, Provider provider, SignupRequest request) { + public Integer signup(String email, String providerId, Provider provider, SignupRequest request) { + if (provider == Provider.APPLE && !StringUtils.hasText(providerId)) { + throw CustomException.of(ApiResponseCode.INVALID_SESSION); + } + + if (StringUtils.hasText(providerId)) { + userRepository.findByProviderIdAndProvider(providerId, provider) + .ifPresent(u -> { + throw CustomException.of(ApiResponseCode.ALREADY_REGISTERED_USER); + }); + } + userRepository.findByEmailAndProvider(email, provider) .ifPresent(u -> { throw CustomException.of(ApiResponseCode.ALREADY_REGISTERED_USER); }); - UnRegisteredUser tempUser = unRegisteredUserRepository - .findByEmailAndProvider(email, provider) - .orElseThrow(() -> CustomException.of(ApiResponseCode.NOT_FOUND_UNREGISTERED_USER)); + UnRegisteredUser tempUser = findUnregisteredUser(email, providerId, provider); University university = universityRepository.findById(request.universityId()) .orElseThrow(() -> CustomException.of(ApiResponseCode.UNIVERSITY_NOT_FOUND)); @@ -66,6 +75,7 @@ public Integer signup(String email, Provider provider, SignupRequest request) { .name(request.name()) .studentNumber(request.studentNumber()) .provider(tempUser.getProvider()) + .providerId(tempUser.getProviderId()) .isMarketingAgreement(request.isMarketingAgreement()) .imageUrl("https://stage-static.koreatech.in/konect/User_02.png") .build(); @@ -77,11 +87,20 @@ public Integer signup(String email, Provider provider, SignupRequest request) { return savedUser.getId(); } + private UnRegisteredUser findUnregisteredUser(String email, String providerId, Provider provider) { + if (StringUtils.hasText(providerId)) { + if (unRegisteredUserRepository.existsByProviderIdAndProvider(providerId, provider)) { + return unRegisteredUserRepository.getByProviderIdAndProvider(providerId, provider); + } + } + + return unRegisteredUserRepository.getByEmailAndProvider(email, provider); + } + public UserInfoResponse getUserInfo(Integer userId) { User user = userRepository.getById(userId); List clubMembers = clubMemberRepository.findAllByUserId(user.getId()); - boolean isClubManager = clubMembers.stream() - .anyMatch(ClubMember::isPresident); + boolean isClubManager = clubMembers.stream().anyMatch(ClubMember::isPresident); int joinedClubCount = clubMembers.size(); Long unreadCouncilNoticeCount = councilNoticeReadRepository.countUnreadNoticesByUserId(user.getId()); Long studyTime = studyTimeQueryService.getTotalStudyTime(userId); From 4998fbace12b4dfe2c48d03eaee9bd2f0b575de8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Thu, 15 Jan 2026 14:05:54 +0900 Subject: [PATCH 05/16] =?UTF-8?q?refactor:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EC=8B=9C=20=EC=9C=A0=EC=A0=80=20=EC=A0=95=EC=A0=81?= =?UTF-8?q?=20=ED=8C=A9=ED=86=A0=EB=A6=AC=20=EB=A9=94=EC=86=8C=EB=93=9C?= =?UTF-8?q?=EB=A1=9C=20=EC=83=9D=EC=84=B1=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../agit/konect/domain/user/model/User.java | 20 +++++++++++++++++++ .../domain/user/service/UserService.java | 18 ++++++++--------- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/src/main/java/gg/agit/konect/domain/user/model/User.java b/src/main/java/gg/agit/konect/domain/user/model/User.java index 0a8a963e..9d1bb9e8 100644 --- a/src/main/java/gg/agit/konect/domain/user/model/User.java +++ b/src/main/java/gg/agit/konect/domain/user/model/User.java @@ -107,6 +107,26 @@ private User( this.imageUrl = imageUrl; } + public static User of( + University university, + UnRegisteredUser tempUser, + String name, + String studentNumber, + Boolean isMarketingAgreement, + String imageUrl + ) { + return User.builder() + .university(university) + .email(tempUser.getEmail()) + .name(name) + .studentNumber(studentNumber) + .provider(tempUser.getProvider()) + .providerId(tempUser.getProviderId()) + .isMarketingAgreement(isMarketingAgreement) + .imageUrl(imageUrl) + .build(); + } + public void updateInfo(String name, String studentNumber, String phoneNumber) { this.name = name; this.studentNumber = studentNumber; diff --git a/src/main/java/gg/agit/konect/domain/user/service/UserService.java b/src/main/java/gg/agit/konect/domain/user/service/UserService.java index ee648bb2..3e497bac 100644 --- a/src/main/java/gg/agit/konect/domain/user/service/UserService.java +++ b/src/main/java/gg/agit/konect/domain/user/service/UserService.java @@ -69,16 +69,14 @@ public Integer signup(String email, String providerId, Provider provider, Signup validateStudentNumberDuplicationOnSignup(university.getId(), request.studentNumber()); - User newUser = User.builder() - .university(university) - .email(tempUser.getEmail()) - .name(request.name()) - .studentNumber(request.studentNumber()) - .provider(tempUser.getProvider()) - .providerId(tempUser.getProviderId()) - .isMarketingAgreement(request.isMarketingAgreement()) - .imageUrl("https://stage-static.koreatech.in/konect/User_02.png") - .build(); + User newUser = User.of( + university, + tempUser, + request.name(), + request.studentNumber(), + request.isMarketingAgreement(), + "https://stage-static.koreatech.in/konect/User_02.png" + ); User savedUser = userRepository.save(newUser); From f4166c7d578749885dfd2693ad8f2e057da4d6ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Thu, 15 Jan 2026 14:22:38 +0900 Subject: [PATCH 06/16] =?UTF-8?q?feat:=20=EC=95=A0=ED=94=8C=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=EC=97=90=20=EB=8C=80=ED=95=9C=20=ED=95=B8?= =?UTF-8?q?=EB=93=A4=EB=9F=AC=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../handler/OAuth2LoginSuccessHandler.java | 56 +++++++++++++++++-- .../konect/global/code/ApiResponseCode.java | 1 + 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/src/main/java/gg/agit/konect/global/auth/handler/OAuth2LoginSuccessHandler.java b/src/main/java/gg/agit/konect/global/auth/handler/OAuth2LoginSuccessHandler.java index 99ec0f35..631059a1 100644 --- a/src/main/java/gg/agit/konect/global/auth/handler/OAuth2LoginSuccessHandler.java +++ b/src/main/java/gg/agit/konect/global/auth/handler/OAuth2LoginSuccessHandler.java @@ -12,6 +12,7 @@ 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.User; @@ -44,14 +45,28 @@ public void onAuthenticationSuccess( ) throws IOException { OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken)authentication; Provider provider = Provider.valueOf(oauthToken.getAuthorizedClientRegistrationId().toUpperCase()); - OAuth2User oauthUser = (OAuth2User)authentication.getPrincipal(); + + String providerId = null; String email = extractEmail(oauthUser, provider); + Optional user; + + if (provider == Provider.APPLE) { + providerId = extractProviderId(oauthUser); + + if (!StringUtils.hasText(providerId)) { + throw CustomException.of(ApiResponseCode.FAILED_EXTRACT_PROVIDER_ID); + } + } - Optional user = userRepository.findByEmailAndProvider(email, provider); + user = findUserByProvider(provider, email, providerId); if (user.isEmpty()) { - sendAdditionalInfoRequiredResponse(request, response, email, provider); + if (provider == Provider.APPLE && !StringUtils.hasText(email)) { + throw CustomException.of(ApiResponseCode.FAILED_EXTRACT_EMAIL); + } + + sendAdditionalInfoRequiredResponse(request, response, email, provider, providerId); return; } @@ -62,11 +77,17 @@ private void sendAdditionalInfoRequiredResponse( HttpServletRequest request, HttpServletResponse response, String email, - Provider provider + Provider provider, + String providerId ) throws IOException { HttpSession session = request.getSession(true); session.setAttribute("email", email); session.setAttribute("provider", provider); + + if (StringUtils.hasText(providerId)) { + session.setAttribute("providerId", providerId); + } + session.setMaxInactiveInterval(TEMP_SESSION_EXPIRATION_SECONDS); response.sendRedirect(frontendBaseUrl + "/signup"); @@ -88,18 +109,45 @@ private void sendLoginSuccessResponse( private String extractEmail(OAuth2User oauthUser, Provider provider) { Object current = oauthUser.getAttributes(); + boolean allowMissing = provider == Provider.APPLE; for (String key : provider.getEmailPath().split("\\.")) { if (!(current instanceof Map map)) { + if (allowMissing) { + return null; + } + throw CustomException.of(ApiResponseCode.FAILED_EXTRACT_EMAIL); } current = map.get(key); } + if (current == null && allowMissing) { + return null; + } + return (String)current; } + private String extractProviderId(OAuth2User oauthUser) { + String providerId = oauthUser.getAttribute("sub"); + + if (!StringUtils.hasText(providerId)) { + providerId = oauthUser.getName(); + } + + return providerId; + } + + private Optional findUserByProvider(Provider provider, String email, String providerId) { + if (provider == Provider.APPLE) { + return userRepository.findByProviderIdAndProvider(providerId, provider); + } + + return userRepository.findByEmailAndProvider(email, provider); + } + private String resolveSafeRedirect(String redirectUri) { if (redirectUri == null || redirectUri.isBlank()) { return frontendBaseUrl + "/home"; diff --git a/src/main/java/gg/agit/konect/global/code/ApiResponseCode.java b/src/main/java/gg/agit/konect/global/code/ApiResponseCode.java index 5d05aabb..2419c7d2 100644 --- a/src/main/java/gg/agit/konect/global/code/ApiResponseCode.java +++ b/src/main/java/gg/agit/konect/global/code/ApiResponseCode.java @@ -19,6 +19,7 @@ public enum ApiResponseCode { INVALID_JSON_FORMAT(HttpStatus.BAD_REQUEST, "요청 본문의 JSON 형식이 잘못되었습니다."), MISSING_REQUIRED_PARAMETER(HttpStatus.BAD_REQUEST, "필수 요청 파라미터가 누락되었습니다."), FAILED_EXTRACT_EMAIL(HttpStatus.BAD_REQUEST, "OAuth 로그인 과정에서 이메일 정보를 가져올 수 없습니다."), + FAILED_EXTRACT_PROVIDER_ID(HttpStatus.BAD_REQUEST, "OAuth 로그인 과정에서 제공자 식별자를 가져올 수 없습니다."), CANNOT_CREATE_CHAT_ROOM_WITH_SELF(HttpStatus.BAD_REQUEST, "자기 자신과는 채팅방을 만들 수 없습니다."), REQUIRED_CLUB_APPLY_ANSWER_MISSING(HttpStatus.BAD_REQUEST, "필수 가입 답변이 누락되었습니다."), CANNOT_DELETE_CLUB_PRESIDENT(HttpStatus.BAD_REQUEST, "동아리 회장인 경우 회장을 양도하고 탈퇴해야 합니다."), From 2d55f7e57adac9cbfa30874004d798db55ddeb2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Thu, 15 Jan 2026 14:28:56 +0900 Subject: [PATCH 07/16] =?UTF-8?q?feat:=20=EC=95=A0=ED=94=8C=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/oauth/AppleOAuthServiceImpl.java | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 src/main/java/gg/agit/konect/global/auth/oauth/AppleOAuthServiceImpl.java diff --git a/src/main/java/gg/agit/konect/global/auth/oauth/AppleOAuthServiceImpl.java b/src/main/java/gg/agit/konect/global/auth/oauth/AppleOAuthServiceImpl.java new file mode 100644 index 00000000..cd75285b --- /dev/null +++ b/src/main/java/gg/agit/konect/global/auth/oauth/AppleOAuthServiceImpl.java @@ -0,0 +1,79 @@ +package gg.agit.konect.global.auth.oauth; + +import java.util.Optional; + +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +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.global.code.ApiResponseCode; +import gg.agit.konect.global.exception.CustomException; +import lombok.RequiredArgsConstructor; + +@Service("apple") +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class AppleOAuthServiceImpl implements SocialOAuthService { + + private static final String OIDC_REQUEST_REQUIRED = "invalid_user_request"; + + private final UserRepository userRepository; + private final UnRegisteredUserRepository unRegisteredUserRepository; + private final OidcUserService oidcUserService = new OidcUserService(); + + @Transactional + @Override + public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { + if (!(userRequest instanceof OidcUserRequest oidcUserRequest)) { + throw new OAuth2AuthenticationException(new OAuth2Error(OIDC_REQUEST_REQUIRED)); + } + + OidcUser oidcUser = oidcUserService.loadUser(oidcUserRequest); + + String email = oidcUser.getAttribute("email"); + String providerId = oidcUser.getSubject(); + + String registrationId = userRequest.getClientRegistration().getRegistrationId().toUpperCase(); + Provider provider = Provider.valueOf(registrationId); + + if (userRepository.existsByProviderIdAndProvider(providerId, provider)) { + return oidcUser; + } + + if (StringUtils.hasText(email)) { + Optional registeredByEmail = userRepository.findByEmailAndProvider(email, provider); + + if (registeredByEmail.isPresent()) { + return oidcUser; + } + } + + if (!unRegisteredUserRepository.existsByProviderIdAndProvider(providerId, provider)) { + if (!StringUtils.hasText(email)) { + throw CustomException.of(ApiResponseCode.FAILED_EXTRACT_EMAIL); + } + + UnRegisteredUser newUser = UnRegisteredUser.builder() + .email(email) + .provider(provider) + .providerId(providerId) + .build(); + + unRegisteredUserRepository.save(newUser); + } + + return oidcUser; + } +} From 3a7051ccd0c86461de67eddc1d5633f05a206585 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Thu, 15 Jan 2026 15:17:47 +0900 Subject: [PATCH 08/16] =?UTF-8?q?chore.=20=EC=84=9C=EB=B8=8C=EB=AA=A8?= =?UTF-8?q?=EB=93=88=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/config b/src/main/resources/config index bef49114..ab0addfe 160000 --- a/src/main/resources/config +++ b/src/main/resources/config @@ -1 +1 @@ -Subproject commit bef49114523ea91253677495e24dba28b808d007 +Subproject commit ab0addfe266540d78d9f66654e741a50bf866cb3 From 76f3ddffe61f2f1006f7ca1fb3cdcab097918df8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Thu, 15 Jan 2026 18:41:19 +0900 Subject: [PATCH 09/16] =?UTF-8?q?feat:=20OidcUserService=20=EC=83=81?= =?UTF-8?q?=EC=86=8D=20=EB=B0=9B=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/auth/oauth/AppleOAuthServiceImpl.java | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/src/main/java/gg/agit/konect/global/auth/oauth/AppleOAuthServiceImpl.java b/src/main/java/gg/agit/konect/global/auth/oauth/AppleOAuthServiceImpl.java index cd75285b..fc5807cd 100644 --- a/src/main/java/gg/agit/konect/global/auth/oauth/AppleOAuthServiceImpl.java +++ b/src/main/java/gg/agit/konect/global/auth/oauth/AppleOAuthServiceImpl.java @@ -4,11 +4,8 @@ import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; -import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; -import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.oidc.user.OidcUser; -import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; @@ -25,22 +22,15 @@ @Service("apple") @RequiredArgsConstructor @Transactional(readOnly = true) -public class AppleOAuthServiceImpl implements SocialOAuthService { - - private static final String OIDC_REQUEST_REQUIRED = "invalid_user_request"; +public class AppleOAuthServiceImpl extends OidcUserService { private final UserRepository userRepository; private final UnRegisteredUserRepository unRegisteredUserRepository; - private final OidcUserService oidcUserService = new OidcUserService(); @Transactional @Override - public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { - if (!(userRequest instanceof OidcUserRequest oidcUserRequest)) { - throw new OAuth2AuthenticationException(new OAuth2Error(OIDC_REQUEST_REQUIRED)); - } - - OidcUser oidcUser = oidcUserService.loadUser(oidcUserRequest); + public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException { + OidcUser oidcUser = super.loadUser(userRequest); String email = oidcUser.getAttribute("email"); String providerId = oidcUser.getSubject(); From 996fd29c3a76b29be1e3a348921afff421df66f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Thu, 15 Jan 2026 18:49:52 +0900 Subject: [PATCH 10/16] =?UTF-8?q?feat:=20=EC=95=A0=ED=94=8C=20OAuth=20JWT?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1=20=EC=86=8D=EC=84=B1/=EC=A0=9C=EA=B3=B5?= =?UTF-8?q?=EC=9E=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/oauth/AppleClientSecretProvider.java | 154 ++++++++++++++++++ .../auth/oauth/AppleOAuthProperties.java | 21 +++ .../oauth/CustomRequestEntityConverter.java | 38 +++++ 3 files changed, 213 insertions(+) create mode 100644 src/main/java/gg/agit/konect/global/auth/oauth/AppleClientSecretProvider.java create mode 100644 src/main/java/gg/agit/konect/global/auth/oauth/AppleOAuthProperties.java create mode 100644 src/main/java/gg/agit/konect/global/auth/oauth/CustomRequestEntityConverter.java diff --git a/src/main/java/gg/agit/konect/global/auth/oauth/AppleClientSecretProvider.java b/src/main/java/gg/agit/konect/global/auth/oauth/AppleClientSecretProvider.java new file mode 100644 index 00000000..069b9206 --- /dev/null +++ b/src/main/java/gg/agit/konect/global/auth/oauth/AppleClientSecretProvider.java @@ -0,0 +1,154 @@ +package gg.agit.konect.global.auth.oauth; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyFactory; +import java.security.interfaces.ECPrivateKey; +import java.security.spec.PKCS8EncodedKeySpec; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Base64; + +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.crypto.ECDSASigner; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class AppleClientSecretProvider { + + private static final String APPLE_AUDIENCE = "https://appleid.apple.com"; + private static final int DEFAULT_TOKEN_VALIDITY_DAYS = 180; + private static final int DEFAULT_REFRESH_BEFORE_DAYS = 7; + + private final AppleOAuthProperties properties; + private volatile CachedSecret cachedSecret; + + public String getClientSecret() { + Instant now = Instant.now(); + CachedSecret current = cachedSecret; + + if (current != null && !current.shouldRefresh(now, resolveRefreshBeforeDays())) { + return current.token(); + } + + synchronized (this) { + current = cachedSecret; + if (current == null || current.shouldRefresh(now, resolveRefreshBeforeDays())) { + cachedSecret = createSecret(now); + } + } + + return cachedSecret.token(); + } + + private CachedSecret createSecret(Instant now) { + int tokenValidityDays = resolveTokenValidityDays(); + String token = generateToken(now, tokenValidityDays); + Instant expiresAt = now.plus(tokenValidityDays, ChronoUnit.DAYS); + return new CachedSecret(token, expiresAt); + } + + private String generateToken(Instant issuedAt, int tokenValidityDays) { + validateRequiredProperties(); + ECPrivateKey privateKey = parsePrivateKey(readPrivateKeyFromPath(properties.getPrivateKeyPath())); + + JWTClaimsSet claims = new JWTClaimsSet.Builder() + .issuer(properties.getTeamId()) + .subject(properties.getClientId()) + .audience(APPLE_AUDIENCE) + .issueTime(java.util.Date.from(issuedAt)) + .expirationTime(java.util.Date.from(issuedAt.plus(tokenValidityDays, ChronoUnit.DAYS))) + .build(); + + JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.ES256) + .keyID(properties.getKeyId()) + .build(); + + SignedJWT signedJWT = new SignedJWT(header, claims); + + try { + signedJWT.sign(new ECDSASigner(privateKey)); + } catch (JOSEException e) { + throw new IllegalStateException("Failed to sign Apple client secret.", e); + } + + return signedJWT.serialize(); + } + + private ECPrivateKey parsePrivateKey(String rawKey) { + String normalized = normalizePrivateKey(rawKey); + byte[] decoded = Base64.getDecoder().decode(normalized); + + try { + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(decoded); + KeyFactory keyFactory = KeyFactory.getInstance("EC"); + return (ECPrivateKey)keyFactory.generatePrivate(keySpec); + } catch (Exception e) { + throw new IllegalStateException("Failed to parse Apple private key.", e); + } + } + + private String normalizePrivateKey(String rawKey) { + if (!StringUtils.hasText(rawKey)) { + throw new IllegalStateException("Apple private key is missing."); + } + + String key = rawKey.replace("\\n", "\n"); + key = key.replace("-----BEGIN PRIVATE KEY-----", ""); + key = key.replace("-----END PRIVATE KEY-----", ""); + return key.replaceAll("\\s", ""); + } + + private String readPrivateKeyFromPath(String keyPath) { + try { + return Files.readString(Path.of(keyPath), StandardCharsets.UTF_8); + } catch (Exception e) { + throw new IllegalStateException("Failed to read Apple private key file.", e); + } + } + + private void validateRequiredProperties() { + if (!StringUtils.hasText(properties.getTeamId())) { + throw new IllegalStateException("Apple teamId is missing."); + } + + if (!StringUtils.hasText(properties.getClientId())) { + throw new IllegalStateException("Apple clientId is missing."); + } + + if (!StringUtils.hasText(properties.getKeyId())) { + throw new IllegalStateException("Apple keyId is missing."); + } + + if (!StringUtils.hasText(properties.getPrivateKeyPath())) { + throw new IllegalStateException("Apple private key path is missing."); + } + } + + private int resolveTokenValidityDays() { + int days = properties.getTokenValidityDays(); + return days > 0 ? days : DEFAULT_TOKEN_VALIDITY_DAYS; + } + + private int resolveRefreshBeforeDays() { + int days = properties.getRefreshBeforeDays(); + return days > 0 ? days : DEFAULT_REFRESH_BEFORE_DAYS; + } + + private record CachedSecret(String token, Instant expiresAt) { + private boolean shouldRefresh(Instant now, int refreshBeforeDays) { + Instant refreshAt = expiresAt.minus(refreshBeforeDays, ChronoUnit.DAYS); + return now.isAfter(refreshAt); + } + } +} diff --git a/src/main/java/gg/agit/konect/global/auth/oauth/AppleOAuthProperties.java b/src/main/java/gg/agit/konect/global/auth/oauth/AppleOAuthProperties.java new file mode 100644 index 00000000..05e76e94 --- /dev/null +++ b/src/main/java/gg/agit/konect/global/auth/oauth/AppleOAuthProperties.java @@ -0,0 +1,21 @@ +package gg.agit.konect.global.auth.oauth; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Component +@ConfigurationProperties(prefix = "app.apple.oauth") +public class AppleOAuthProperties { + + private String teamId; + private String clientId; + private String keyId; + private String privateKeyPath; + private int tokenValidityDays; + private int refreshBeforeDays; +} diff --git a/src/main/java/gg/agit/konect/global/auth/oauth/CustomRequestEntityConverter.java b/src/main/java/gg/agit/konect/global/auth/oauth/CustomRequestEntityConverter.java new file mode 100644 index 00000000..6c9b51e0 --- /dev/null +++ b/src/main/java/gg/agit/konect/global/auth/oauth/CustomRequestEntityConverter.java @@ -0,0 +1,38 @@ +package gg.agit.konect.global.auth.oauth; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; +import org.springframework.security.oauth2.client.endpoint.DefaultOAuth2TokenRequestParametersConverter; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.util.MultiValueMap; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class CustomRequestEntityConverter + implements Converter> { + + private static final String APPLE_REGISTRATION_ID = "apple"; + + private final DefaultOAuth2TokenRequestParametersConverter delegate = + new DefaultOAuth2TokenRequestParametersConverter<>(); + private final AppleClientSecretProvider clientSecretProvider; + + @Override + public MultiValueMap convert(OAuth2AuthorizationCodeGrantRequest request) { + MultiValueMap parameters = delegate.convert(request); + + if (!isApple(request)) { + return parameters; + } + + parameters.set(OAuth2ParameterNames.CLIENT_SECRET, clientSecretProvider.getClientSecret()); + + return parameters; + } + + private boolean isApple(OAuth2AuthorizationCodeGrantRequest request) { + return APPLE_REGISTRATION_ID.equals(request.getClientRegistration().getRegistrationId()); + } + +} From cc1a845cf44f5679fd07e6f85be3b7cbb15e089f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Thu, 15 Jan 2026 18:50:12 +0900 Subject: [PATCH 11/16] =?UTF-8?q?feat:=20=EC=95=A0=ED=94=8C=20OAuth=20?= =?UTF-8?q?=EB=B9=88=20=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../konect/global/config/SecurityConfig.java | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/main/java/gg/agit/konect/global/config/SecurityConfig.java b/src/main/java/gg/agit/konect/global/config/SecurityConfig.java index c7d45d51..6c548fe6 100644 --- a/src/main/java/gg/agit/konect/global/config/SecurityConfig.java +++ b/src/main/java/gg/agit/konect/global/config/SecurityConfig.java @@ -4,6 +4,8 @@ import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; @@ -12,11 +14,17 @@ import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; +import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; +import org.springframework.security.oauth2.client.endpoint.RestClientAuthorizationCodeTokenResponseClient; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter; import org.springframework.security.web.SecurityFilterChain; import gg.agit.konect.global.auth.filter.OAuth2RedirectUriSaveFilter; import gg.agit.konect.global.auth.handler.OAuth2LoginSuccessHandler; +import gg.agit.konect.global.auth.oauth.AppleOAuthServiceImpl; +import gg.agit.konect.global.auth.oauth.AppleClientSecretProvider; +import gg.agit.konect.global.auth.oauth.CustomRequestEntityConverter; import gg.agit.konect.global.auth.oauth.SocialOAuthService; import lombok.RequiredArgsConstructor; @@ -38,7 +46,11 @@ public class SecurityConfig { private OAuth2RedirectUriSaveFilter redirectUriSaveFilter; @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + public SecurityFilterChain filterChain( + HttpSecurity http, + OAuth2AccessTokenResponseClient appleAccessTokenResponseClient, + AppleOAuthServiceImpl appleOAuthService + ) throws Exception { http .csrf(AbstractHttpConfigurer::disable) .formLogin(AbstractHttpConfigurer::disable) @@ -54,11 +66,15 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { }) ) .oauth2Login(oauth2 -> oauth2 + .tokenEndpoint(token -> token + .accessTokenResponseClient(appleAccessTokenResponseClient) + ) .userInfoEndpoint(userInfo -> userInfo .userService(userRequest -> { String registrationId = userRequest.getClientRegistration().getRegistrationId(); return oAuthServices.get(registrationId).loadUser(userRequest); }) + .oidcUserService(appleOAuthService) ) .successHandler(oAuth2LoginSuccessHandler) .failureHandler((request, response, exception) -> { @@ -70,4 +86,13 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http.build(); } + + @Bean + public OAuth2AccessTokenResponseClient appleAccessTokenResponseClient( + AppleClientSecretProvider appleClientSecretProvider + ) { + RestClientAuthorizationCodeTokenResponseClient client = new RestClientAuthorizationCodeTokenResponseClient(); + client.setParametersConverter(new CustomRequestEntityConverter(appleClientSecretProvider)); + return client; + } } From f077bc218692392a3a404c67b547974904978964 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Thu, 15 Jan 2026 18:50:28 +0900 Subject: [PATCH 12/16] =?UTF-8?q?feat:=20=EA=B0=9C=EB=B0=9C=EC=9A=A9=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80?= =?UTF-8?q?=EC=97=90=20=EC=95=A0=ED=94=8C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EB=B2=84=ED=8A=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/static/login.html | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/resources/static/login.html b/src/main/resources/static/login.html index eee942f8..a1efe851 100644 --- a/src/main/resources/static/login.html +++ b/src/main/resources/static/login.html @@ -45,6 +45,9 @@ .kakao-btn { background: #FEE500; } + .apple-btn { + background: #000000; + } @@ -54,6 +57,7 @@

로그인

Google로 로그인 Naver로 로그인 Kakao로 로그인 + Apple로 로그인 From c62066040557f8bfd6bc698f2d017fd6153ef092 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Thu, 15 Jan 2026 19:05:34 +0900 Subject: [PATCH 13/16] =?UTF-8?q?chore:=20=EC=84=9C=EB=B8=8C=EB=AA=A8?= =?UTF-8?q?=EB=93=88=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/config b/src/main/resources/config index ab0addfe..ae1b3194 160000 --- a/src/main/resources/config +++ b/src/main/resources/config @@ -1 +1 @@ -Subproject commit ab0addfe266540d78d9f66654e741a50bf866cb3 +Subproject commit ae1b319425063535c62ad2f5eee2d5dac8d74fa4 From 21ca7d09bf4aebf312edb55d6473fe0f3d71eafa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Thu, 15 Jan 2026 19:09:07 +0900 Subject: [PATCH 14/16] =?UTF-8?q?chore:=20=EC=BD=94=EB=93=9C=20=ED=8F=AC?= =?UTF-8?q?=EB=A7=B7=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/auth/oauth/CustomRequestEntityConverter.java | 2 +- .../java/gg/agit/konect/global/config/SecurityConfig.java | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/java/gg/agit/konect/global/auth/oauth/CustomRequestEntityConverter.java b/src/main/java/gg/agit/konect/global/auth/oauth/CustomRequestEntityConverter.java index 6c9b51e0..a755bc8d 100644 --- a/src/main/java/gg/agit/konect/global/auth/oauth/CustomRequestEntityConverter.java +++ b/src/main/java/gg/agit/konect/global/auth/oauth/CustomRequestEntityConverter.java @@ -1,8 +1,8 @@ package gg.agit.konect.global.auth.oauth; import org.springframework.core.convert.converter.Converter; -import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; import org.springframework.security.oauth2.client.endpoint.DefaultOAuth2TokenRequestParametersConverter; +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.util.MultiValueMap; diff --git a/src/main/java/gg/agit/konect/global/config/SecurityConfig.java b/src/main/java/gg/agit/konect/global/config/SecurityConfig.java index 6c548fe6..a8f2ff08 100644 --- a/src/main/java/gg/agit/konect/global/config/SecurityConfig.java +++ b/src/main/java/gg/agit/konect/global/config/SecurityConfig.java @@ -4,8 +4,6 @@ import java.util.Map; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; @@ -22,8 +20,8 @@ import gg.agit.konect.global.auth.filter.OAuth2RedirectUriSaveFilter; import gg.agit.konect.global.auth.handler.OAuth2LoginSuccessHandler; -import gg.agit.konect.global.auth.oauth.AppleOAuthServiceImpl; import gg.agit.konect.global.auth.oauth.AppleClientSecretProvider; +import gg.agit.konect.global.auth.oauth.AppleOAuthServiceImpl; import gg.agit.konect.global.auth.oauth.CustomRequestEntityConverter; import gg.agit.konect.global.auth.oauth.SocialOAuthService; import lombok.RequiredArgsConstructor; From c79814fd558be4d3b9000861b1d5ce327b00a9db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Thu, 15 Jan 2026 19:24:38 +0900 Subject: [PATCH 15/16] =?UTF-8?q?chore:=20=EC=84=9C=EB=B8=8C=EB=AA=A8?= =?UTF-8?q?=EB=93=88=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/config b/src/main/resources/config index ae1b3194..db6efe45 160000 --- a/src/main/resources/config +++ b/src/main/resources/config @@ -1 +1 @@ -Subproject commit ae1b319425063535c62ad2f5eee2d5dac8d74fa4 +Subproject commit db6efe4587ad8074b807b0623417180e9411c0eb From 420e90260e8a6ad59a9423ebdb9657823265bc57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Thu, 15 Jan 2026 19:43:09 +0900 Subject: [PATCH 16/16] =?UTF-8?q?fix:=20=EC=95=A0=ED=94=8C=20=EC=9E=AC?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=8B=9C=20=EC=9E=84=EC=8B=9C=20?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=EC=97=90=EC=84=9C=20=EC=9D=B4=EB=A9=94?= =?UTF-8?q?=EC=9D=BC=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../handler/OAuth2LoginSuccessHandler.java | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/main/java/gg/agit/konect/global/auth/handler/OAuth2LoginSuccessHandler.java b/src/main/java/gg/agit/konect/global/auth/handler/OAuth2LoginSuccessHandler.java index 631059a1..8346e309 100644 --- a/src/main/java/gg/agit/konect/global/auth/handler/OAuth2LoginSuccessHandler.java +++ b/src/main/java/gg/agit/konect/global/auth/handler/OAuth2LoginSuccessHandler.java @@ -16,6 +16,7 @@ import gg.agit.konect.domain.user.enums.Provider; 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.global.code.ApiResponseCode; import gg.agit.konect.global.config.SecurityProperties; @@ -35,6 +36,7 @@ public class OAuth2LoginSuccessHandler implements AuthenticationSuccessHandler { private static final int TEMP_SESSION_EXPIRATION_SECONDS = 600; private final UserRepository userRepository; + private final UnRegisteredUserRepository unRegisteredUserRepository; private final SecurityProperties securityProperties; @Override @@ -63,7 +65,11 @@ public void onAuthenticationSuccess( if (user.isEmpty()) { if (provider == Provider.APPLE && !StringUtils.hasText(email)) { - throw CustomException.of(ApiResponseCode.FAILED_EXTRACT_EMAIL); + email = resolveAppleEmail(providerId); + + if (!StringUtils.hasText(email)) { + throw CustomException.of(ApiResponseCode.FAILED_EXTRACT_EMAIL); + } } sendAdditionalInfoRequiredResponse(request, response, email, provider, providerId); @@ -148,6 +154,16 @@ private Optional findUserByProvider(Provider provider, String email, Strin return userRepository.findByEmailAndProvider(email, provider); } + private String resolveAppleEmail(String providerId) { + if (!StringUtils.hasText(providerId)) { + return null; + } + + return unRegisteredUserRepository.findByProviderIdAndProvider(providerId, Provider.APPLE) + .map(unRegisteredUser -> unRegisteredUser.getEmail()) + .orElse(null); + } + private String resolveSafeRedirect(String redirectUri) { if (redirectUri == null || redirectUri.isBlank()) { return frontendBaseUrl + "/home";