diff --git a/src/main/java/com/eactive/apim/portal/apps/app/controller/MyAppController.java b/src/main/java/com/eactive/apim/portal/apps/app/controller/MyAppController.java index c5c6a3c..3ae473c 100644 --- a/src/main/java/com/eactive/apim/portal/apps/app/controller/MyAppController.java +++ b/src/main/java/com/eactive/apim/portal/apps/app/controller/MyAppController.java @@ -13,6 +13,9 @@ import com.eactive.apim.portal.apps.app.dto.AppRequestDTO; import com.eactive.apim.portal.apps.app.dto.ClientDTO; import com.eactive.apim.portal.apps.app.service.AdminGatewayClient; import com.eactive.apim.portal.apps.app.service.AppServiceFacade; +import com.eactive.apim.portal.apps.auth.twofactor.StepUpProtectedPaths; +import com.eactive.apim.portal.apps.auth.twofactor.TwoFactorProperties; +import com.eactive.apim.portal.apps.auth.twofactor.TwoFactorService; import com.eactive.apim.portal.common.user.PortalAuthenticatedUser; import com.eactive.apim.portal.common.util.ApiServiceHelper; import com.eactive.apim.portal.common.util.SecurityUtil; @@ -82,6 +85,8 @@ public class MyAppController { private final ApiServiceHelper apiServiceHelper; private final FileTypeDetector fileTypeDetector; private final AdminGatewayClient adminGatewayClient; + private final TwoFactorService twoFactorService; + private final TwoFactorProperties twoFactorProperties; private static final long MAX_APP_ICON_BYTES = 2L * 1024 * 1024; // 2MB @@ -893,6 +898,8 @@ public class MyAppController { setupStepModel(model, 2); model.addAttribute("apiServices", apiServices); model.addAttribute("modification", modification); + // 최종 반영(저장) 직전 2FA 필요 여부 → 폼 JS 분기용 + model.addAttribute("twofaRequired", isAppModifyTwofaRequired()); return new ModelAndView(API_KEY_MODIFY_STEP2); } @@ -927,6 +934,7 @@ public class MyAppController { @RequestParam(value = "selectedApis", required = false) List selectedApis, @ModelAttribute("apiKeyModification") ApiKeyRegistrationDTO modification, SessionStatus sessionStatus, + HttpSession session, RedirectAttributes redirectAttributes) { // 1단계가 완료되었는지 검증 @@ -950,6 +958,14 @@ public class MyAppController { return new ModelAndView("redirect:/myapikey/modify/step1?clientId=" + modification.getClientId()); } + // 반영 직전 2FA: 통과권이 없으면 커밋하지 않고 step2 로 되돌린다(프론트가 먼저 2FA 팝업을 띄운다). + // 진입(step1)이 아닌 최종 반영 시점에만 인증을 요구해 다단계 진행 중 중복 인증을 막는다. + if (isAppModifyTwofaRequired() + && !twoFactorService.consumeStepUpPass(session, StepUpProtectedPaths.APP_MODIFY_COMMIT)) { + redirectAttributes.addFlashAttribute("error", "추가 인증(2FA) 후 다시 시도해 주세요."); + return new ModelAndView("redirect:/myapikey/modify/step2"); + } + PortalAuthenticatedUser user = SecurityUtil.getPortalAuthenticatedUser(); try { @@ -1021,6 +1037,12 @@ public class MyAppController { } } + /** 앱 수정 최종 반영 직전 2FA(step-up)가 현재 활성인지 — 전체/지점 스위치 AND */ + private boolean isAppModifyTwofaRequired() { + return twoFactorProperties.isStepUpEnabled() + && twoFactorProperties.isStepUpPointEnabled(StepUpProtectedPaths.APP_MODIFY_COMMIT); + } + } diff --git a/src/main/java/com/eactive/apim/portal/apps/auth/twofactor/StepUpProtectedPaths.java b/src/main/java/com/eactive/apim/portal/apps/auth/twofactor/StepUpProtectedPaths.java index 8e127a2..f39ff17 100644 --- a/src/main/java/com/eactive/apim/portal/apps/auth/twofactor/StepUpProtectedPaths.java +++ b/src/main/java/com/eactive/apim/portal/apps/auth/twofactor/StepUpProtectedPaths.java @@ -11,7 +11,8 @@ import java.util.Set; *

검증 레벨(완화 정책)

* @@ -43,8 +44,8 @@ public final class StepUpProtectedPaths { public static final String REVEAL_SECRET = "/myapikey/credential/reveal-secret"; /** 앱 해지 신청 (AJAX POST) */ public static final String APP_KEY_DELETE = "/myapikey/api_key_delete"; - /** 앱 정보 수정 페이지 진입 (GET) */ - public static final String APP_MODIFY_STEP1 = "/myapikey/modify/step1"; + /** 앱 정보 수정 최종 반영(commit, POST /modify/step2) — 반영 직전 2FA. 진입/중간 단계는 가드하지 않음 */ + public static final String APP_MODIFY_COMMIT = "/myapikey/modify/step2"; /** 개인정보 변경 페이지 진입 (GET, 정확 일치) — PASSWORD 레벨 */ public static final String MYPAGE = "/mypage"; /** 비밀번호 변경 반영(commit, POST) — 반영 직전 2FA. 진입(GET)은 가드하지 않음 */ @@ -63,7 +64,7 @@ public final class StepUpProtectedPaths { static { Map keys = new LinkedHashMap<>(); keys.put(REVEAL_SECRET, KEY_PREFIX + "reveal-secret"); - keys.put(APP_MODIFY_STEP1, KEY_PREFIX + "app-modify"); + keys.put(APP_MODIFY_COMMIT, KEY_PREFIX + "app-modify"); keys.put(APP_KEY_DELETE, KEY_PREFIX + "app-delete"); keys.put(MYPAGE, KEY_PREFIX + "mypage"); keys.put(PASSWORD_CHANGE, KEY_PREFIX + "password-change"); @@ -71,7 +72,7 @@ public final class StepUpProtectedPaths { Map levels = new LinkedHashMap<>(); levels.put(REVEAL_SECRET, Level.TWO_FACTOR); - levels.put(APP_MODIFY_STEP1, Level.TWO_FACTOR); + levels.put(APP_MODIFY_COMMIT, Level.TWO_FACTOR); levels.put(APP_KEY_DELETE, Level.TWO_FACTOR); levels.put(MYPAGE, Level.PASSWORD); levels.put(PASSWORD_CHANGE, Level.TWO_FACTOR); @@ -79,10 +80,11 @@ public final class StepUpProtectedPaths { // 인터셉터 진입 자동 차단: 2FA 레벨 중 "진입 시점" 보호가 필요한 경로만. // - PASSWORD_CHANGE 는 반영(POST commit) 직전에 컨트롤러가 통과권을 요구 → 제외 + // - APP_MODIFY_COMMIT 도 동일 — 다단계(step1→step2) 진행 중 중복 인증을 막기 위해 + // 최종 반영 직전에만 컨트롤러가 통과권을 요구 → 제외 // - MYPAGE 는 별도 확인 페이지로 컨트롤러가 유도(PASSWORD 레벨) → 제외 Set guarded = new java.util.LinkedHashSet<>(); guarded.add(REVEAL_SECRET); - guarded.add(APP_MODIFY_STEP1); guarded.add(APP_KEY_DELETE); INTERCEPTOR_GUARDED = Collections.unmodifiableSet(guarded); } diff --git a/src/main/resources/templates/views/apps/mypage/apiKeyModifyStep1.html b/src/main/resources/templates/views/apps/mypage/apiKeyModifyStep1.html index 385fa7d..75bf3c2 100644 --- a/src/main/resources/templates/views/apps/mypage/apiKeyModifyStep1.html +++ b/src/main/resources/templates/views/apps/mypage/apiKeyModifyStep1.html @@ -143,13 +143,6 @@ - -
- - -
-
@@ -357,7 +350,6 @@ form.addEventListener('submit', function (e) { const name = document.getElementById('appName').value.trim(); const desc = textarea.value.trim(); - const url = document.getElementById('callbackUrl').value.trim(); if (!name) { e.preventDefault(); @@ -372,15 +364,6 @@ textarea.focus(); return; } - - if (url) { - try { new URL(url); } catch (_) { - e.preventDefault(); - customPopups.showAlert('올바른 URL 형식이 아닙니다.\n예: https://example.com/callback'); - document.getElementById('callbackUrl').focus(); - return; - } - } }); }); diff --git a/src/main/resources/templates/views/apps/mypage/apiKeyModifyStep2.html b/src/main/resources/templates/views/apps/mypage/apiKeyModifyStep2.html index 834dbd9..f7b40b5 100644 --- a/src/main/resources/templates/views/apps/mypage/apiKeyModifyStep2.html +++ b/src/main/resources/templates/views/apps/mypage/apiKeyModifyStep2.html @@ -111,6 +111,38 @@
+ + + diff --git a/src/main/resources/templates/views/apps/mypage/apiKeyModifyStep3.html b/src/main/resources/templates/views/apps/mypage/apiKeyModifyStep3.html index ff7b91b..b41546b 100644 --- a/src/main/resources/templates/views/apps/mypage/apiKeyModifyStep3.html +++ b/src/main/resources/templates/views/apps/mypage/apiKeyModifyStep3.html @@ -151,7 +151,7 @@
-

앱 수정이 완료되었습니다.

+

앱 수정 신청이 완료되었습니다.

담당자 승인 후 변경 사항이 적용됩니다.