Change Password (/onboarding/change-password)
SPEC #007 정합. BE v0.4.0 · FE v0.2.0 도입. BE enforcement: SPEC #022 · BE v0.15.0.
Overview
신규 계정 (me.passwordMustChange === true) 의 임시 비번 강제 변경. v1 에서는 강제 변경 흐름만 노출 — self-service 변경 UI 는 후속.
구현 표면 2곳 (동일 폼·BFF 패턴, 시안 ops-change-password.jsx 공용):
- 운영사 콘솔
apps/admin—OPERATOR임시 비번 변경. 성공 후/로 이동. 임퍼소네이션 컨텍스트에서는IMPERSONATION_FORBIDDEN_ACTION으로 차단(폼 disable + 배너). - 매장 클라이언트
apps/space(본사HQ_MANAGER· 점장STORE_MANAGER공용) —/onboarding/change-password단일 관문. 성공 후 역할별 목적지(HQ→/admin·STORE→/store)로 이동.apps/space는 실 로그인만 다루므로 임퍼소네이션 분기 없음(sealed 임퍼소네이션 세션은 apps/admin 전용).
강제 흐름은 FE layout redirect + BE enforcement 이중이다 (SPEC #022):
- FE layout redirect (주 경로) — protected/admin layout 의 server component 가
me.passwordMustChangetrue 면 진입 전/onboarding/change-password로 redirect. - BE 403 enforcement (이중) —
PasswordChangeEnforcementFilter가pmc=true계정의 allowlist 밖 보호 endpoint 호출을 403PASSWORD_CHANGE_REQUIRED로 차단. FE redirect 를 우회한 직접 API 호출까지 막는다. allowlist:POST /api/v1/auth/change-password·GET /api/v1/auth/me·POST /api/v1/auth/logout·POST /api/v1/auth/refresh(+ OPTIONS).
Spec
URL
- 강제 변경 화면:
/onboarding/change-password - BFF route:
POST /api/auth/change-password→ backendPOST /api/v1/auth/change-password
UI
AuthShell (standalone) — 현재 비밀번호 + 새 비밀번호 + 정책 체크리스트(실시간) + 강도 인디케이터 + 변경 button.
입력
- currentPassword: required (현재 임시 비번)
- newPassword: 8~100 자, currentPassword 와 달라야 함
동작
- submit → POST
/api/auth/change-password(BFF) - BFF → POST
/api/v1/auth/change-password(backend, authenticated) - 200 → backend 가
passwordMustChange = false로 UPDATE + 모든 refresh token 무효화 + 새 access/refresh 발급. BFF 가 새 sealed cookie 부여. - FE 가
/로 redirect → protected layout 통과
Strong gate (passwordMustChange 강제 흐름)
passwordMustChange = true 인 동안 어떤 protected 라우트도 접근 불가:
// apps/admin/src/app/(protected)/layout.tsx — server-side redirect
// loadMeRefreshAware: 만료 임박 선제 refresh → backendMe → 401 시 reactive refresh.
const result = await loadMeRefreshAware(session); // { ok, me } | { ok: false }
if (result.ok && result.me.passwordMustChange) {
redirect("/onboarding/change-password");
}middleware (apps/admin/middleware.ts) 는 인증 여부만 확인 (lm_session cookie 존재 여부). passwordMustChange 분기는 protected layout 의 server component 에서 처리 — me 응답을 활용하기 위해서.
매장 클라이언트(apps/space)의 이중 가드: 두 셸 layout(admin/layout.tsx·store/layout.tsx)이 각각 loadMeRefreshAware 로 me 를 로드해 passwordMustChange true 면 /onboarding/change-password 로 redirect 한다(직접 URL·새로고침 대응). login-form 도 로그인 직후 같은 분기를 first-paint UX 로 수행한다. 본사 셸은 /hq/me(BE enforcement allowlist 밖 — pmc=true 면 403)를 호출하기 전에 me 로 먼저 가드한다(403 회피). 무한 루프 방지: /onboarding layout 은 세션 presence-only(me 미호출)라 layout↔layout redirect 가 생기지 않고, /onboarding/change-password page 자신만 me 를 1회 로드해 역할별 목적지를 도출(이미 변경 완료면 그 목적지로 되돌림)한다.
BE 이중 강제 (SPEC #022): layout redirect 는 화면 진입 차단이고, BE PasswordChangeEnforcementFilter 는 access token claim pmc=true 인 계정이 allowlist 밖 보호 endpoint 를 직접 호출하면 403 PASSWORD_CHANGE_REQUIRED 로 막는다. UI 를 우회한 직접 API 호출까지 차단하는 defense-in-depth. 임퍼소네이션 토큰은 pmc=false 강제라 무영향. change-password 성공 후 pmc=false 토큰 재발급으로 enforcement 해제.
States & Edge Cases
| 상태 | 처리 |
|---|---|
currentPassword 불일치 (AUTH_INVALID_CREDENTIALS) | “현재 비밀번호 불일치” |
newPassword == currentPassword (PASSWORD_REUSED) | “새 비밀번호는 현재와 달라야 합니다” |
임퍼소네이션 컨텍스트 (IMPERSONATION_FORBIDDEN_ACTION) | “임퍼소네이션 모드에서는 비밀번호를 변경할 수 없습니다” + form disable |
변경 전 보호 API 직접 호출 (PASSWORD_CHANGE_REQUIRED, 403) | BE filter 가 차단 — allowlist 밖 호출. 정상 흐름에선 FE redirect 로 도달 X (우회 방어용) |
형식 위반 (VALIDATION_ERROR) | “비밀번호 형식을 확인해 주세요. (8~100자)“ |
| 5xx | ”서버에 일시적인 문제가 있습니다” |
Constraints
- backend
/api/v1/auth/change-password는 authenticated (allowlist —pmc=true여도 호출 가능). - 강제 변경 (
passwordMustChange=true) 일 때 protected layout 의 server-side redirect 로 다른 모든 화면 접근 차단 — middleware 는 인증 여부만 검증. - 직접 API 우회 방어: BE
PasswordChangeEnforcementFilter가 allowlist(POST /api/v1/auth/change-password·GET /api/v1/auth/me·POST /api/v1/auth/logout·POST /api/v1/auth/refresh) 밖 보호 endpoint 를 403PASSWORD_CHANGE_REQUIRED로 차단 (#022). 판정은 access token claimpmc(요청당 DB 조회 0, 15분 TTL 로 stale 해소). - 자기 자신의 password 만 변경 가능 (
subclaim 기준). - 임퍼소네이션 세션 (
impersonatedByclaim 존재) 에서는 backend 가 403 으로 차단.
Roadmap
- self-service 변경 흐름 (passwordMustChange=false 일 때) UI — 후속 SPEC
- 비밀번호 reset 흐름 (이메일 링크) — 후속
References
- SPEC #007 · SPEC #022 (passwordMustChange BE enforcement) · SPEC #050 (apps/space 매장 클라이언트)
- 운영사 콘솔:
linkmusic-frontend-space/apps/admin/src/app/onboarding/change-password/·apps/admin/src/app/(protected)/layout.tsx(mustChange 가드) ·apps/admin/src/app/api/auth/change-password/route.ts(BFF) - 매장 클라이언트(본사·점장):
linkmusic-frontend-space/apps/space/src/app/onboarding/change-password/(page = me 로드·역할별 목적지·완료 우회 + change-password-form = 공용 폼) ·apps/space/src/app/admin/layout.tsx·apps/space/src/app/store/layout.tsx(mustChange 가드) ·apps/space/src/app/(auth)/login/login-form.tsx(로그인 직후 관문 redirect) ·apps/space/src/app/api/auth/change-password/route.ts(BFF) linkmusic-msa-space-was/.../PasswordChangeEnforcementFilter.kt(#022 BE 403 강제)