FeaturesAuthChange Password

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/adminOPERATOR 임시 비번 변경. 성공 후 / 로 이동. 임퍼소네이션 컨텍스트에서는 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.passwordMustChange true 면 진입 전 /onboarding/change-password 로 redirect.
  • BE 403 enforcement (이중)PasswordChangeEnforcementFilterpmc=true 계정의 allowlist 밖 보호 endpoint 호출을 403 PASSWORD_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 → backend POST /api/v1/auth/change-password

UI

AuthShell (standalone) — 현재 비밀번호 + 새 비밀번호 + 정책 체크리스트(실시간) + 강도 인디케이터 + 변경 button.

입력

  • currentPassword: required (현재 임시 비번)
  • newPassword: 8~100 자, currentPassword 와 달라야 함

동작

  1. submit → POST /api/auth/change-password (BFF)
  2. BFF → POST /api/v1/auth/change-password (backend, authenticated)
  3. 200 → backend 가 passwordMustChange = false 로 UPDATE + 모든 refresh token 무효화 + 새 access/refresh 발급. BFF 가 새 sealed cookie 부여.
  4. 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 를 403 PASSWORD_CHANGE_REQUIRED 로 차단 (#022). 판정은 access token claim pmc (요청당 DB 조회 0, 15분 TTL 로 stale 해소).
  • 자기 자신의 password 만 변경 가능 (sub claim 기준).
  • 임퍼소네이션 세션 (impersonatedBy claim 존재) 에서는 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 강제)