[TMT-272] feat: 세션·토큰 발급과 X-User-Id 스텁 제거 - #76
Conversation
| token: | ||
| # 기본값을 두지 않아 누락 시 기동 단계에서 바로 실패한다 — 토큰 서명 키가 없으면 | ||
| # 인증 전체가 무의미해진다 (TMT-272) | ||
| secret: ${JWT_SECRET} |
There was a problem hiding this comment.
[must] 여기가 #75보다 위험합니다 — 키가 없으면 배포가 서비스를 내립니다.
prod에 기본값을 안 둔 판단 자체는 맞습니다(ADDRESS_TOKEN_SECRET과 같은 정책). 문제는 배포 스크립트가 그걸 모른다는 것입니다.
cicd-release.yml의 순서가 이렇습니다.
- SSM에서 값 읽기 →
JWT_SECRET은 읽는 코드 자체가 없음 - 필수값 검사 루프 →
JWT_SECRET이 목록에 없어 통과 docker compose up -d --force-recreate→ 기존 컨테이너를 먼저 내림- 새 컨테이너가
JWT_SECRET없어 기동 실패 - 헬스체크 3분 실패 → 워크플로 실패. 그때는 이미 서비스가 내려가 있음
빈 값이면 로그인만 죽는 #75와 달리, 이건 앱 전체가 안 뜹니다. 어제 juso 키에서 같은 구조를 발견해 필수값 검사 루프에 넣어뒀는데(PR #52), JWT_SECRET도 같은 자리에 들어가야 합니다.
필요한 것 세 가지입니다.
cicd-release.yml에/tmt-prod/auth/*읽기 추가 (카카오 키 2종 +JWT_SECRET)- 같은 파일 필수값 검사 루프에
JWT_SECRET추가 — 이게 있어야 4번 전에 끊깁니다 infra/terraform/iam.tf의ssm:GetParameter범위에auth/*추가
#75에도 같은 코멘트를 남겼습니다. 두 PR이 같은 작업을 필요로 하니 후속 티켓 하나로 묶고 에픽 TMT-270에 릴리즈 차단 항목으로 달아두는 것이 어떨까요.
| ) : OncePerRequestFilter() { | ||
| private val mapper = JsonMapper.builder().build() | ||
|
|
||
| override fun shouldNotFilter(request: HttpServletRequest): Boolean = |
There was a problem hiding this comment.
[q] /v1/auth/ 접두 전체를 건너뜁니다.
지금은 로그인·재발급 둘뿐이라 맞습니다. 그리고 확인해보니 실패해도 안전한 방향입니다 — 나중에 /v1/auth/logout 같은 걸 추가해도 속성이 안 실려서 @UserId Long이 401을 던지지, 인증이 우회되지는 않습니다.
다만 그때 원인을 찾기가 어렵습니다(필터를 의심하기 전에 토큰을 의심하게 되죠). 경로 화이트리스트를 정확히 두는 편이 어떨까요 —
private val PUBLIC_PATHS = setOf("/v1/auth/login/kakao", "/v1/auth/token/refresh")지금 형태를 유지하신다면 KDoc에 "이 접두 아래는 전부 인증 없이 통과한다"를 한 줄 더 굵게 남겨주시면 좋겠습니다.
| } | ||
|
|
||
| val token = header.removePrefix(BEARER_PREFIX) | ||
| if (token == header) { |
There was a problem hiding this comment.
[want] Bearer 대소문자를 가립니다.
removePrefix("Bearer ")라 bearer eyJ...로 오면 접두가 안 떨어져 token == header가 되고 401입니다. RFC 7235에서 auth scheme은 대소문자를 구분하지 않습니다.
대부분의 클라이언트가 Bearer로 보내니 당장 문제는 아닌데, FE가 라이브러리를 바꾸거나 외부 도구로 테스트할 때 원인 찾기 어려운 401이 됩니다. header.regionMatches(0, BEARER_PREFIX, 0, 7, ignoreCase = true) 정도로 풀 수 있습니다.
| fun refreshToken( | ||
| @Valid @RequestBody request: TokenRefreshRequest, | ||
| ): TokenRefreshResponse { | ||
| val userId = tokenCodec.parseUserId(request.refreshToken, TokenUse.REFRESH) |
There was a problem hiding this comment.
[q] refresh 30일 + stateless라, 토큰이 탈취되면 30일간 막을 방법이 없습니다.
트레이드오프를 PR 본문에 적어주신 건 봤고 저장소를 안 붙인 판단도 이해합니다. 다만 그 전제에서 30일이 좀 긴 것 같습니다. 로그아웃도 클라이언트 삭제뿐이라, 기기를 잃어버린 사용자가 할 수 있는 게 없습니다.
런칭 초기에는 7일 정도로 줄여두고, 저장소를 붙일 때 늘리는 건 어떨까요. 설정값이라 나중에 늘리는 건 쉽고 줄이는 건 사용자가 재로그인해야 해서 비쌉니다.
[want] 그리고 재발급이 사용자 존재를 확인하지 않습니다. 서명만 맞으면 새 토큰이 나갑니다. 지금은 탈퇴 기능이 없어 문제가 안 되는데, 생기면 여기가 구멍이 됩니다. 지금 고치자는 건 아니고 그때 놓치지 않게 주석 한 줄이면 좋겠습니다.
mingdodev
left a comment
There was a problem hiding this comment.
X-User-Id를 걷어내는 변경이라 조심해서 봤는데, 설계가 탄탄합니다. 특히 네 가지가 좋았습니다.
use클레임으로 access·refresh 교차 사용 차단 — 서명 검증 뒤에 확인하는 순서도 맞습니다- 필터는 헤더가 없으면 통과시키고 필수 여부는 리졸버에 남긴 것 —
@UserId Long?계약이 그대로 유지되고 컨트롤러 61곳이 안 바뀝니다 - 필터 순서를
RequestIdFilter다음으로 둬서 401 본문에도requestId가 실리는 것 - jjwt 스코프 —
api만implementation,impl·jackson은runtimeOnly. production 의존성 추가를 본문에 근거와 함께 보고해주신 것도 규칙대로입니다
X-User-Id 잔재도 확인했습니다 — 남은 건 곧 삭제될 mock 주석과 "헤더를 더 이상 해석하지 않는다"를 검증하는 테스트뿐입니다.
[must] 이 PR은 CI가 한 번도 안 돌았습니다
base가 feat/TMT-271인데 ci-pull-request.yml이 branches: [main]에만 걸려 있습니다. 그래서 지금 붙어 있는 체크는 리뷰어·담당자 배정 하나뿐이고, ktlint도 테스트도 실행된 적이 없습니다.
#75가 머지되면 base가 main으로 자동 재타겟되면서 그때 처음 돕니다. 그 결과를 보고 머지해주세요. 32파일에 테스트 81곳을 마이그레이션한 변경이라 로컬 통과만으로 넘기기엔 범위가 큽니다.
머지 순서와 FE 공유
X-User-Id 제거는 FE가 반영하기 전에 배포되면 모든 인증 API가 401입니다. 본문에 적어주신 그대로고요.
지금은 main 자동 배포가 꺼져 있어(TMT-290) 머지 자체가 배포를 일으키지 않습니다. 그래서 머지는 편하게 하시고, 릴리즈 태그 시점에 FE pnpm api:sync와 맞추면 됩니다. 계약 변경 이력에는 Breaking으로 기록해주세요 — mock 전환 파생분은 태그 때 한 줄로 묶기로 했지만, 이건 전환과 무관한 인증 변경이라 별도로 적는 게 맞습니다.
시드 사용자
V4 시드(id 1~4)가 카카오 계정이 없어 로그인 불가라고 적어주셨는데, 맞습니다. 다만 TMT-231(시드 SQL)이 아직 미착수라 릴리즈 시점에 로그인 가능한 계정이 하나도 없을 수 있습니다. 카카오 실계정으로 들어가면 되니 막히는 건 아니지만, 시드 티켓에 "카카오 로그인 이후의 시드 계정을 어떻게 만들 것인가"가 안 적혀 있어 제가 따로 챙기겠습니다.
라인 코멘트 4개 남겼습니다. [must]는 배포 키 하나이고 나머지는 질문·제안입니다.
mingdodev
left a comment
There was a problem hiding this comment.
앞 리뷰의 시드 사용자 문단은 취소합니다 — 제 착오입니다.
V4__seed_mock_users.sql은 mock 기간에 사진 업로드 FK 위반을 막으려고 넣은 것(TMT-258)이고 TMT-231도 UT2용입니다. 전환 후에는 실사용자가 카카오로 들어오니 시드 계정이 필요 없습니다. "로그인 가능한 계정이 하나도 없을 수 있다"는 걱정은 성립하지 않습니다.
시드 사용자가 로그인 불가라고 적어주신 것도 그래서 문제가 아니라 정상입니다. mock 정리(TMT-274) 때 함께 걷어내는 것으로 충분합니다.
나머지 리뷰 내용은 그대로입니다.
wnsvy607
left a comment
There was a problem hiding this comment.
필터(헤더 없으면 통과)·리졸버(필수/선택 판단 유지) 분리로 컨트롤러 61곳 무수정을 지킨 설계가 깔끔하고, use 클레임 교차 차단·jjwt weak-key 강제·401 본문의 requestId 정합·기존 테스트 마이그레이션(X-User-Id 잔재 0건, grep으로 확인)까지 코드는 견고합니다. 다만 코드 밖 동반물 3건이 빠져 있어 needs changes입니다 — 특히 첫 번째는 릴리즈 시 서비스 전면 다운으로 직결됩니다.
[must] JWT_SECRET 배포 파이프라인 미동반 — application-prod.yml:26이 ${JWT_SECRET} 기본값 없음(의도된 fail-fast)인데, cicd-release.yml은 SSM prefix가 db/media/ai/address뿐이고 필수값 가드에도 없으며 docker-compose.prod.yml environment에도 JWT가 없습니다(전부 grep으로 확인). 이대로 머지 후 릴리즈하면 구 컨테이너를 내린 뒤 새 컨테이너가 기동 실패합니다 — 워크플로 주석이 경고하는 TMT-191 시나리오 그대로예요. SSM /tmt-prod/auth/jwt-secret 등록 + iam.tf 범위 + 워크플로 fetch/가드 + compose env가 이 PR과 같이(최소한 릴리즈 전에) 움직여야 합니다. #75의 카카오 키 2종도 같은 경로라 한 번에 처리하면 좋겠습니다.
[must] Breaking 계약 미기재 — 로그인 응답 3필드 추가, 전 인증 API Bearer 요구, X-User-Id 스펙 제거는 Breaking인데 [계약] API 변경 이력에 기록이 없고, 문서 §2는 아직 "인증은 X-User-Id: 1 헤더로 대체한다"라고 안내 중입니다. 기록 → FE 공유(pnpm api:sync) → 배포 순서가 지켜져야 합니다. FE 반영 전 배포 시 전 API 401은 본문에서도 인정하신 리스크고요.
[must] CLAUDE.md의 "인증은 카카오 로그인 전까지 X-User-Id 헤더 스텁이다" 서술이 이 PR로 거짓이 됩니다. "내 변경으로 문서 서술이 코드와 달라지면 같은 변경에서 갱신" 규칙에 따라 같은 PR에서 수정 부탁드립니다.
[q] V4 시드 사용자(14·901903)는 카카오 계정이 없어 배포 즉시 로그인 불가 — TMT-274로 미룬 건 봤는데, FE가 mock 데이터로 개발 중인 기간의 릴리즈 태그 타이밍은 어떻게 조율할 계획인가요?
[q] stateless refresh라 재발급이 회전 없이 이뤄져(이전 refresh 계속 유효) 유출 시 30일간 재발급 가능하고 서버 무효화 수단이 없습니다. 트레이드오프 문서화돼 있어 결정은 존중합니다 — 런칭 전 "계정 차단" 요구가 나올 가능성만 체크해 두면 될 것 같아요. 같은 맥락에서 refresh를 JSON 바디로 내리면 FE가 웹 스토리지에 두게 되는데(XSS 표면), HttpOnly 쿠키 대안은 FE와 논의해 보셨나요?
wnsvy607
left a comment
There was a problem hiding this comment.
파이프라인 동반 커밋 확인했습니다 — SSM prefix /tmt-prod/auth, 필수값 가드 3종 추가, .env·compose 전달, iam.tf 읽기 범위, CLAUDE.md 인증 서술 갱신까지 [must] 3건 중 2건(파이프라인·CLAUDE.md)은 해소됐습니다. 공개 경로 화이트리스트·refresh 7일 축소도 좋습니다.
남은 것 두 가지라 request-changes는 유지합니다:
[must] 계약 변경 이력 미기재 — [계약] API 변경 이력 문서(v38 기준)에 TMT-271·272 항목이 아직 없습니다. #76은 Breaking(전 인증 API Bearer, 응답 필드 추가, X-User-Id 제거)이라 기록 → FE 공유 → 배포 순서가 지켜져야 하고, #75의 Additive 항목도 같이 올라가야 합니다. §2의 "인증은 X-User-Id 헤더로 대체" 안내도 같이 고쳐주세요.
[must → 릴리즈 전 필수] SSM /tmt-prod/auth/* 파라미터 3종(kakao-rest-api-key·kakao-client-secret·jwt-secret)이 아직 등록돼 있지 않습니다 (방금 describe-parameters로 확인). 지금 구조에선 등록 없이 릴리즈하면 가드가 배포를 멈추는 안전한 실패라 서비스는 안 끊기지만, 릴리즈 자체가 안 됩니다. 키를 가진 분이 등록해야 하니 — read -s로 받아 aws ssm put-parameter --type SecureString로 넣는 방식이 안전합니다(터미널 히스토리에 안 남음). iam.tf 변경은 머지 후 terraform apply 1건(in-place)이 따라옵니다.
이 두 개 끝나면 바로 approve하겠습니다.
Related Issue
Why
@UserId가 X-User-Id 헤더 값을 검증 없이 신뢰한다(TMT-150). 누구나 남의 id로 요청할 수 있는상태라 실사용자 런칭(9/12) 전에 반드시 교체돼야 한다. 카카오 로그인(TMT-271)이 들어왔으므로
로그인 성공 시 토큰을 발급하고, 스텁을 걷어낸다.
What
pnpm api:sync필요. FE 반영 없이 배포되면 모든 인증 API가 401이다.accessToken·accessTokenExpiresIn(초) ·refreshToken추가."응답의 userId를 X-User-Id로 쓴다"는 과도기 계약 폐기
POST /v1/auth/token/refresh신설 — refresh로 새 토큰 쌍 발급AuthTokenFilter) 도입 —Authorization: Bearer검증, 실패 시 401.만료는
AUTH_TOKEN_EXPIRED(→ FE는 재발급), 그 외는AUTH_TOKEN_INVALID(→ 재로그인)로 구분UserIdArgumentResolver를 인증 주체(필터가 실은 요청 속성)에서 읽도록 교체.@UserId Long?(선택) 동작 유지, 컨트롤러 61곳 무수정UserIdHeaderCustomizer삭제) →bearerAuth보안 스킴으로 교체.Swagger Authorize 버튼에 accessToken을 넣으면 인증 API 테스트 가능
AUTH_TOKEN_INVALID·AUTH_TOKEN_EXPIRED(기존 이름 변경 없음)tmt.auth.token.*— 서명 키는 envJWT_SECRET(32바이트 이상), 운영은 SSM/tmt-prod/auth/*예정.prod는 기본값 없음(누락 시 기동 실패), local은 개발 기본값
X-User-Id 사용 81곳을 요청 속성 방식으로 일괄 마이그레이션
의존성 추가 (production): jjwt 0.12.6 (api / impl / jackson). JWT 파싱·검증의 보안 엣지 케이스를
직접 구현하지 않기 위해 추가했다. jjwt-jackson이 Jackson 2를 전이로 끌고 오지만 레포의
Jackson 3(tools.jackson)과 패키지가 달라 충돌 없이 공존한다.
How
use클레임으로 access·refresh 교차 사용 차단클라이언트 삭제)라는 트레이드오프가 있고, 강제 무효화가 필요해지면 그때 저장소를 붙인다
@UserId Long?계약을 지킨다./v1/auth하위(로그인·재발급)는 검증하지 않는다RequestIdFilter(HIGHEST_PRECEDENCE) →AuthTokenFilter— 필터가 직접 쓰는401 본문(ProblemDetail 동일 포맷)에 requestId가 실리도록
AddressIdTokenCodec(TMT-191)과 같은 정책Prompt Log
refresh 저장 방식(stateless vs DB 저장) → jjwt + stateless 선택, 만료 정책은 기본값(1h/30d) 수용
ktlint 파싱 실패(KDoc 안
/v1/auth/**의 중첩 주석) 등 빌드 이슈 수정 후 전체 그린 확인