feat: refresh 토큰 완전 회전 + 재사용 탐지

- RefreshSession(패밀리) 테이블 도입 — 한 로그인=1행(currentJti/prevJti/expiresAt)
- /auth/refresh 회전형으로 교체: 현재 jti→새 jti 회전, 옛 토큰 재제출 시 재사용 탐지→패밀리 폐기(401)
- 동시 갱신 오탐 방지: 직전 jti 15초 유예 + 프론트 useApi single-flight refresh
- 로그아웃=해당 세션만 폐기, revokeAllSessions(전 기기) 준비
- tokenVersion·jwt-refresh.strategy·JwtRefreshGuard 제거(refresh는 가드 없이 서비스 직접 처리)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-19 22:57:01 +09:00
parent df268d3b5e
commit db6a80a6f4
13 changed files with 220 additions and 102 deletions
+2 -2
View File
@@ -88,7 +88,7 @@ IDOR/교차 저장소 위조 불가, 알림 타인 접근 불가, mass-assignmen
- **H1 ✅** 각 전략이 제공자 이메일 검증 플래그를 읽어 `OAuthProfile.emailVerified` 전달, `validateOrCreateOAuthUser` 가 미검증이면 403 거부(검증 이메일일 때만 매칭/생성/미인증 로컬 승격). Google `_json.email_verified`, Kakao `kakao_account.is_email_verified`.
- **H2 ✅** 세션 없는 구조라 `CookieStateStore`(passport-oauth2 store 인터페이스) 신설 — 인가 시작 시 httpOnly 1회용 난수 state 쿠키 발급, 콜백에서 일치 검증. 두 전략에 `store` 주입(새 의존성 0). *런타임은 실제 제공자 콜백으로만 최종 확인 가능.*
- **H3 ✅** `User.tokenVersion`(int) 추가, refresh JWT payload 에 `ver` 서명, `jwt-refresh.strategy``ver===tokenVersion` 검증(구토큰은 0 호환). **logout 이 `revokeSession`(refresh 검증 후 tokenVersion 증가)** 으로 발급된 refresh 일괄 폐기. (완전 회전+재사용탐지는 후속.)
- **H3 ✅ → 완전 회전+재사용 탐지로 강화(2026-06-19)**: 초기엔 `tokenVersion` 폐기 수준이었으나, **세션(패밀리) 테이블 기반 회전형 refresh** 로 교체. `RefreshSession`(id=fid, currentJti, prevJti+prevAt, expiresAt) 한 로그인당 1행. `/auth/refresh` 가 현재 jti면 새 jti로 **회전**, 이미 회전된 옛 토큰 재제출이면 **재사용 탐지 → 패밀리 전체 폐기(401)**. 동시 갱신 오탐은 직전 jti 15초 유예 + 프론트 single-flight로 방지. logout=해당 패밀리만 폐기, `revokeAllSessions`(전 기기) 메서드 준비(비번 재설정 시 사용). `tokenVersion`/`jwt-refresh.strategy`/`JwtRefreshGuard` 제거(refresh는 가드 없이 서비스에서 직접 처리).
- **M1 ✅** 업로드 `fileFilter` MIME 화이트리스트(html/svg 등 차단) + 거부 시 400, 다운로드 `Content-Disposition: attachment` + `X-Content-Type-Options: nosniff`.
- **M2 ✅** `@Throttle` 라우트별 — login 10/분, signup 5/분, resend 3/분(IP 기준). 계정 잠금/CAPTCHA·이메일별 쿨다운은 후속.
- **M3 ✅** `removeFile` 가 업로더 본인 또는 admin 만 허용(`getAttachmentOrThrow` 에 uploader 로드).
@@ -110,4 +110,4 @@ IDOR/교차 저장소 위조 불가, 알림 타인 접근 불가, mass-assignmen
**검증**: 백엔드 build/lint 0 · 20 tests. 프론트 변경 없음.
**남은 항목**: L1(에이전트 v-html, 에이전트 작업 시 함께) + §2 후속 강화(토큰 완전회전·계정잠금/CAPTCHA·M5) + §4 기능공백(비번 재설정·SMTP·파일 콘텐츠 검증). **런타임 미검증**: H1/H2/H3 OAuth·세션 경로는 실제 제공자 콜백 + DB 로 최종 확인 필요.
**남은 항목**: L1(에이전트 v-html, 에이전트 작업 시 함께) + §2 후속 강화(~~토큰 완전회전~~ **완료** / 계정잠금·CAPTCHA / M5) + §4 기능공백(비번 재설정·SMTP·파일 콘텐츠 검증). access 토큰 즉시 폐기(denylist)는 여전히 미적용(15분 잔존, 수용). **런타임 미검증**: H1/H2/H3 OAuth·세션 경로는 실제 제공자 콜백 + DB 로 최종 확인 필요(특히 회전형 refresh의 동시성·재사용 탐지 동작).