diff --git a/clokey-api/src/main/java/org/clokey/domain/auth/handler/OidcLoginFailureHandler.java b/clokey-api/src/main/java/org/clokey/domain/auth/handler/OidcLoginFailureHandler.java index 512e24c1..d57dd244 100644 --- a/clokey-api/src/main/java/org/clokey/domain/auth/handler/OidcLoginFailureHandler.java +++ b/clokey-api/src/main/java/org/clokey/domain/auth/handler/OidcLoginFailureHandler.java @@ -5,12 +5,15 @@ import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.clokey.exception.GlobalBaseErrorCode; import org.clokey.response.BaseResponse; import org.springframework.security.core.AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; import org.springframework.stereotype.Component; +@Slf4j @Component @RequiredArgsConstructor public class OidcLoginFailureHandler extends SimpleUrlAuthenticationFailureHandler { @@ -24,15 +27,58 @@ public void onAuthenticationFailure( AuthenticationException exception) throws IOException { + String requestURI = request.getRequestURI(); + String queryString = request.getQueryString(); + String errorCode = null; + String errorDescription = null; + + log.error( + "[OIDC Failure] 로그인 실패 핸들러 시작 - RequestURI: {}, QueryString: {}", + requestURI, + queryString); + log.error( + "[OIDC Failure] 예외 정보 - ExceptionType: {}, Message: {}", + exception.getClass().getSimpleName(), + exception.getMessage()); + + if (exception instanceof OAuth2AuthenticationException oauth2Exception) { + errorCode = oauth2Exception.getError().getErrorCode(); + errorDescription = oauth2Exception.getError().getDescription(); + log.error( + "[OIDC Failure] OAuth2 예외 상세 - ErrorCode: {}, Description: {}", + errorCode, + errorDescription); + + if (oauth2Exception.getCause() != null) { + log.error( + "[OIDC Failure] 원인 예외 - CauseType: {}, CauseMessage: {}", + oauth2Exception.getCause().getClass().getSimpleName(), + oauth2Exception.getCause().getMessage(), + oauth2Exception.getCause()); + } + } + + // 요청 파라미터 로깅 + request.getParameterMap() + .forEach( + (key, values) -> + log.error( + "[OIDC Failure] Request Parameter - {}: {}", + key, + String.join(", ", values))); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType("application/json"); response.setCharacterEncoding("UTF-8"); + String errorMessage = errorDescription != null ? errorDescription : exception.getMessage(); BaseResponse failureResponse = BaseResponse.onFailure( - GlobalBaseErrorCode.UNAUTHORIZED.getCode(), exception.getMessage(), null); + GlobalBaseErrorCode.UNAUTHORIZED.getCode(), errorMessage, null); String json = objectMapper.writeValueAsString(failureResponse); response.getWriter().write(json); + + log.error("[OIDC Failure] 실패 응답 전송 완료 - ErrorCode: {}", errorCode); } } diff --git a/clokey-api/src/main/java/org/clokey/domain/auth/handler/OidcLoginSuccessHandler.java b/clokey-api/src/main/java/org/clokey/domain/auth/handler/OidcLoginSuccessHandler.java index a8972682..c8736d58 100644 --- a/clokey-api/src/main/java/org/clokey/domain/auth/handler/OidcLoginSuccessHandler.java +++ b/clokey-api/src/main/java/org/clokey/domain/auth/handler/OidcLoginSuccessHandler.java @@ -34,31 +34,52 @@ public void onAuthenticationSuccess( HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { - CustomPrincipal principal = (CustomPrincipal) authentication.getPrincipal(); - Member member = principal.getMember(); + log.info("[OIDC Success] 로그인 성공 핸들러 시작 - RequestURI: {}", request.getRequestURI()); - String accessToken = - jwtTokenService.createAccessToken(member.getId(), member.getMemberRole()); - String refreshToken = jwtTokenService.createRefreshToken(member.getId()); + try { + CustomPrincipal principal = (CustomPrincipal) authentication.getPrincipal(); + Member member = principal.getMember(); + log.info( + "[OIDC Success] 인증 정보 추출 완료 - MemberId: {}, Email: {}", + member.getId(), + member.getEmail()); - TokenResponse tokenResponse = TokenResponse.of(accessToken, refreshToken); - BaseResponse jsonResponse = - BaseResponse.onSuccess(GlobalBaseSuccessCode.CREATED, tokenResponse); - String jsonData = objectMapper.writeValueAsString(jsonResponse); + log.info("[OIDC Success] JWT 토큰 생성 시작 - MemberId: {}", member.getId()); + String accessToken = + jwtTokenService.createAccessToken(member.getId(), member.getMemberRole()); + String refreshToken = jwtTokenService.createRefreshToken(member.getId()); + log.info("[OIDC Success] JWT 토큰 생성 완료 - MemberId: {}", member.getId()); - String redirectUrl = - UriComponentsBuilder.fromUriString(redirectScheme + "://oauth/callback") - .queryParam("accessToken", accessToken) - .queryParam("refreshToken", refreshToken) - .build() - .toUriString(); + TokenResponse tokenResponse = TokenResponse.of(accessToken, refreshToken); + BaseResponse jsonResponse = + BaseResponse.onSuccess(GlobalBaseSuccessCode.CREATED, tokenResponse); + String jsonData = objectMapper.writeValueAsString(jsonResponse); - String html = buildHtmlPage(jsonData, redirectUrl); + String redirectUrl = + UriComponentsBuilder.fromUriString(redirectScheme + "://oauth/callback") + .queryParam("accessToken", accessToken) + .queryParam("refreshToken", refreshToken) + .build() + .toUriString(); - response.setContentType("text/html; charset=UTF-8"); - response.setCharacterEncoding("UTF-8"); - response.getWriter().write(html); - response.getWriter().flush(); + log.info("[OIDC Success] 리다이렉트 URL 생성 완료 - RedirectUrl: {}", redirectUrl); + + String html = buildHtmlPage(jsonData, redirectUrl); + + response.setContentType("text/html; charset=UTF-8"); + response.setCharacterEncoding("UTF-8"); + response.getWriter().write(html); + response.getWriter().flush(); + + log.info("[OIDC Success] 로그인 성공 처리 완료 - MemberId: {}", member.getId()); + } catch (Exception e) { + log.error( + "[OIDC Success] 성공 핸들러 처리 중 오류 발생 - Error: {}, Message: {}", + e.getClass().getSimpleName(), + e.getMessage(), + e); + throw e; + } } // NOTE : 개발자들을 위해서 웹에서 보여주면서도 딥링크 리다이랙트를 동시에 보내줌. diff --git a/clokey-api/src/main/java/org/clokey/domain/auth/service/CustomOAuth2UserService.java b/clokey-api/src/main/java/org/clokey/domain/auth/service/CustomOAuth2UserService.java index 890d7476..4fc50d5e 100644 --- a/clokey-api/src/main/java/org/clokey/domain/auth/service/CustomOAuth2UserService.java +++ b/clokey-api/src/main/java/org/clokey/domain/auth/service/CustomOAuth2UserService.java @@ -13,6 +13,8 @@ import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -29,33 +31,85 @@ public class CustomOAuth2UserService extends OidcUserService { @Override @Transactional public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException { - OidcUser oidcUser = super.loadUser(userRequest); - String provider = userRequest.getClientRegistration().getRegistrationId(); - String oauthId = oidcUser.getName(); - String email = oidcUser.getAttribute("email"); - - OauthProvider oauthProvider = OauthProvider.valueOf(provider.toUpperCase()); - OauthInfo oauthInfo = OauthInfo.createOauthInfo(oauthId, oauthProvider); - - Member member = - memberRepository - .findByOauthInfo(oauthInfo) - .orElseGet( - () -> { - Member newMember = - Member.createMember( - email, - uniqueUtil.generateRandomNickname(), - oauthInfo); - memberRepository.save(newMember); - eventPublisher.publishEvent( - MeiliSearchSyncEvent.of( - MeiliSearchSyncEvent.EntityType.MEMBER, - newMember.getId())); - return newMember; - }); - - return new CustomPrincipal(member, oidcUser.getAttributes(), oidcUser.getIdToken()); + String clientId = userRequest.getClientRegistration().getClientId(); + String clientAuthenticationMethod = + userRequest.getClientRegistration().getClientAuthenticationMethod().getValue(); + + log.info( + "[OIDC] 사용자 로드 시작 - Provider: {}, ClientId: {}, AuthMethod: {}", + provider, + clientId, + clientAuthenticationMethod); + + try { + log.info("[OIDC] Access Token 교환 시도 - Provider: {}", provider); + OidcUser oidcUser = super.loadUser(userRequest); + log.info("[OIDC] Access Token 교환 성공 - Provider: {}", provider); + + String oauthId = oidcUser.getName(); + String email = oidcUser.getAttribute("email"); + log.info( + "[OIDC] 사용자 정보 추출 - Provider: {}, OAuthId: {}, Email: {}", + provider, + oauthId, + email); + + OauthProvider oauthProvider = OauthProvider.valueOf(provider.toUpperCase()); + OauthInfo oauthInfo = OauthInfo.createOauthInfo(oauthId, oauthProvider); + + log.info("[OIDC] 회원 조회 시작 - Provider: {}, OAuthId: {}", provider, oauthId); + Member member = + memberRepository + .findByOauthInfo(oauthInfo) + .orElseGet( + () -> { + log.info( + "[OIDC] 신규 회원 생성 - Provider: {}, Email: {}", + provider, + email); + Member newMember = + Member.createMember( + email, + uniqueUtil.generateRandomNickname(), + oauthInfo); + memberRepository.save(newMember); + eventPublisher.publishEvent( + MeiliSearchSyncEvent.of( + MeiliSearchSyncEvent.EntityType.MEMBER, + newMember.getId())); + log.info( + "[OIDC] 신규 회원 생성 완료 - MemberId: {}", + newMember.getId()); + return newMember; + }); + + log.info("[OIDC] 사용자 로드 완료 - Provider: {}, MemberId: {}", provider, member.getId()); + return new CustomPrincipal(member, oidcUser.getAttributes(), oidcUser.getIdToken()); + } catch (OAuth2AuthenticationException e) { + // OAuth2AuthenticationException은 그대로 전달 (이미 OAuth2Error 포함) + log.error( + "[OIDC] 사용자 로드 실패 - Provider: {}, ErrorCode: {}, Description: {}", + provider, + e.getError() != null ? e.getError().getErrorCode() : "UNKNOWN", + e.getError() != null ? e.getError().getDescription() : e.getMessage(), + e); + throw e; + } catch (Exception e) { + // 일반 Exception을 OAuth2AuthenticationException으로 변환 + log.error( + "[OIDC] 예상치 못한 오류 발생 - Provider: {}, Error: {}, Message: {}", + provider, + e.getClass().getSimpleName(), + e.getMessage(), + e); + + OAuth2Error oauth2Error = + new OAuth2Error( + OAuth2ErrorCodes.SERVER_ERROR, + "사용자 로드 중 오류 발생: " + e.getMessage(), + null); + throw new OAuth2AuthenticationException(oauth2Error, e); + } } } diff --git a/clokey-api/src/main/resources/application-dev.yml b/clokey-api/src/main/resources/application-dev.yml index f857d785..2c749b0b 100644 --- a/clokey-api/src/main/resources/application-dev.yml +++ b/clokey-api/src/main/resources/application-dev.yml @@ -48,6 +48,7 @@ spring: client-secret: ${APPLE_CLIENT_SECRET} redirect-uri: ${APPLE_REDIRECT_URI} authorization-grant-type: authorization_code + client-authentication-method: private_key_jwt scope: - openid provider: apple diff --git a/clokey-api/src/main/resources/application-local.yml b/clokey-api/src/main/resources/application-local.yml index ed35d833..6eb9d0fb 100644 --- a/clokey-api/src/main/resources/application-local.yml +++ b/clokey-api/src/main/resources/application-local.yml @@ -48,6 +48,7 @@ spring: client-secret: ${APPLE_CLIENT_SECRET} redirect-uri: ${APPLE_REDIRECT_URI} authorization-grant-type: authorization_code + client-authentication-method: private_key_jwt scope: - openid provider: apple diff --git a/clokey-api/src/main/resources/application-prod.yml b/clokey-api/src/main/resources/application-prod.yml index 2b1ec3b2..4e42e0c5 100644 --- a/clokey-api/src/main/resources/application-prod.yml +++ b/clokey-api/src/main/resources/application-prod.yml @@ -48,6 +48,7 @@ spring: client-secret: ${APPLE_CLIENT_SECRET} redirect-uri: ${APPLE_REDIRECT_URI} authorization-grant-type: authorization_code + client-authentication-method: private_key_jwt scope: - openid provider: apple