Skip to content

[Auth & Security] JWT 인증·사업장 권한·멀티테넌시 구현 #4

Description

@hywznn

한 줄 목표

사업장 사용자가 안전하게 로그인하고, 자신의 사업장 데이터만 역할에 맞게 사용하도록 JWT 인증·권한·Refresh Token 수명주기를 구현합니다.

현재 진행 상태

초보자 설명: Access Token은 API를 잠깐 사용하는 출입증이고, Refresh Token은 출입증을 다시 발급받는 수단입니다. 서버는 Refresh Token을 매번 새 값으로 교체하며, 이미 쓴 값이 다시 오면 해당 로그인 묶음을 모두 폐기합니다.

공식 API

/api/v1을 생략한 /auth/** 별칭은 만들지 않습니다.

API 인증 하는 일 주요 응답
POST /api/v1/auth/login Public, JSON email/password 로그인하고 Access Token과 HttpOnly Refresh 쿠키 발급 200, 실패 401 INVALID_CREDENTIALS
POST /api/v1/auth/refresh Refresh 쿠키만 기존 쿠키를 한 번 사용하고 Access/Refresh Token 회전 200, 실패 401 INVALID_REFRESH_TOKEN
POST /api/v1/auth/logout 선택적 Refresh 쿠키 현재 로그인 family 폐기와 쿠키 삭제 항상 204; DB 장애는 성공으로 숨기지 않음
GET /api/v1/auth/me Bearer Access Token 현재 user_id, company_id, roles 확인 200, 실패 401

구현 체크리스트

  • Company, UserAccount, RefreshToken 모델과 V2 Flyway migration
  • 비밀번호 BCrypt 단방향 hash 저장
  • 로그인과 JWT Access Token 발급·검증
  • Refresh Token 원문 비저장, SHA-256 hash 저장
  • Refresh Token rotation과 이전 토큰 사용 처리
  • 재사용 탐지 시 token family 전체 폐기
  • 멱등 로그아웃과 HttpOnly 쿠키 삭제
  • JWT에서 user_id, company_id, roles를 ActorContext로 구성
  • ADMIN·HR·VIEWER 역할 검사
  • user_id + company_id 조회와 타 사업장 리소스 404 은닉
  • privacy-safe 인증 감사 이벤트와 AuthAuditPort 정의
  • 기본 비활성·Secret 필수인 데모 Company·ADMIN Seed 제공
  • OpenAPI·README·ADR-0002·환경변수 예시 갱신

감사 이벤트 경계

#35는 로그인 성공·실패, 재발급, 재사용 탐지, token family 폐기, 로그아웃 action을 원본 토큰·email·비밀번호 없이 안전한 ID로 정의합니다. 현재 adapter는 request_id가 포함된 진단 로그입니다. append-only 영구 감사 테이블과 검색 API는 소유 Issue #11에서 이 port에 연결합니다.

검증 체크리스트

  • 정상 로그인·JWT claims·/auth/me
  • 잘못된 email과 비밀번호가 동일한 외부 오류
  • VIEWER 쓰기 차단, HR 쓰기 허용
  • 다른 사업장 리소스 404 은닉
  • 정상 Refresh와 교체 chain·family 검증
  • 누락·변조·미존재·폐기·재사용 토큰의 동일한 401
  • 재사용 뒤 새 교체 토큰까지 폐기
  • 로그아웃 반복 호출과 서로 다른 로그인 family 격리
  • 계정·사업장 정지 시 Refresh 차단과 family 폐기
  • 데모 Seed 멱등성·BCrypt 저장·Secret 비노출
  • 로컬 ./gradlew clean test./gradlew build
  • feat: Refresh Token 회전·로그아웃과 안전한 데모 인증 기반 구현 #35 GitHub Actions의 실제 PostgreSQL 동시성 테스트
  • 팀원 리뷰와 main 병합

보안 규칙

  • Refresh Token은 JSON·일반 로그·감사 이벤트에 넣지 않고 HttpOnly 쿠키로만 전달합니다.
  • 쿠키는 운영에서 Secure이며, CSRF/Origin 보호 전에는 SameSite=None을 금지하고 Strict/Lax만 허용합니다.
  • 같은 쿠키의 동시 Refresh는 보수적으로 재사용으로 판단할 수 있으므로 Client는 single-flight로 한 번만 호출합니다.
  • 로그아웃 뒤 이미 발급된 stateless Access Token은 기본 TTL 기준 최대 15분 유효할 수 있어 Client가 즉시 삭제합니다.
  • Seed는 기본 비활성이며 DEMO_SEED_ADMIN_PASSWORD를 실행 환경 Secret으로 줄 때만 동작합니다.
  • JWT Secret, 초기 비밀번호, 실제 토큰을 GitHub·Notion·로그에 올리지 않습니다.

이번 Issue에서 하지 않는 것

선행·후속 관계

용어 도움말

  • JWT Access Token: API 요청자가 누구인지 짧은 시간 동안 증명하는 문자열
  • Refresh Token rotation: 재발급할 때 기존 Refresh Token을 폐기하고 새 값으로 교체하는 방식
  • Token family: 한 번의 로그인에서 이어진 Refresh Token 교체 묶음
  • Single-flight: 동시에 여러 재발급을 보내지 않고 한 요청의 결과를 함께 기다리는 Client 방식
  • 멀티테넌시: 여러 사업장이 한 서버를 쓰되 서로의 데이터를 볼 수 없게 분리하는 구조

Metadata

Metadata

Assignees

Labels

area:serverSpring Boot API·도메인·DB·tenant·Task Workflow 영역; Prompt·모델·Provider 구현 제외priority:P0MVP 진행을 막는 최우선 핵심 작업security:privacy개인정보·접근권한·토큰·보안 영향이 있는 작업type:feature사용자 또는 Agent가 사용하는 기능 개발

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions