From a11ae8682f7a4917ffc8e0eb56820003a4de1e05 Mon Sep 17 00:00:00 2001 From: a123 Date: Mon, 16 Mar 2026 20:13:45 +0900 Subject: [PATCH 1/5] docs(bo-auth): add renewal auth and authorization specification --- 2026-renewal/feature/01-bo-auth.md | 390 +++++++++++++++++++++++++++++ 1 file changed, 390 insertions(+) diff --git a/2026-renewal/feature/01-bo-auth.md b/2026-renewal/feature/01-bo-auth.md index e69de29..f4e5239 100644 --- a/2026-renewal/feature/01-bo-auth.md +++ b/2026-renewal/feature/01-bo-auth.md @@ -0,0 +1,390 @@ +# 01. BO Auth 고도화 명세서 + +## 0) 문서 목적 + +본 문서는 `02-feature-roadmap.md`의 1번 기능(백오피스 인증/권한 체계 고도화)에 대한 구현 기준 문서다. +핵심 목표는 **Google OAuth 단일 로그인**, **사전 등록 계정만 접근 허용**, **라벨 기반 권한 분리**, **슈퍼어드민 전용 사용자 관리**를 통해 운영 보안과 유지보수성을 동시에 높이는 것이다. + +이 문서는 실제 코드베이스를 직접 확인한 뒤 작성했다. + +- backoffice 현재 인증: `passport-local + 세션` 단일 비밀번호 방식 +- main 현재 인증: 관리자 인증 체계 없음(공개 서비스 API 중심) + +--- + +## 1) 현재 구현 진단 (As-Is) + +## 1.1 Backoffice + +- 인증 방식 + - 비밀번호 입력 후 `POST /bo/auth/login` + - 서버는 `process.env.PASSWORD`(bcrypt hash)와 비교 + - 성공 시 세션에 `admin` 사용자 직렬화 +- 접근 제어 + - `isLoggedIn` 미들웨어로 로그인 여부만 확인 + - 권한 레벨(읽기/쓰기/기능별) 없음 +- 문제점 + - 계정 단위 추적 불가(누가 로그인했는지 구분 안 됨) + - 권한 분리 불가(모든 로그인 사용자가 동일 권한) + - 감사 로그/관리자 변경 이력 부재 + - 확장성 부족(운영 인원 증가 시 보안 리스크 확대) + +## 1.2 Main + +- 현재 main은 공개 사용자를 위한 서비스이며 관리자 인증 체계가 없다. +- 모집 ON/OFF 같은 운영 제어는 feature API로 조회하는 구조가 있으나, 관리자 권한 모델과 연결되어 있지 않다. +- 따라서 운영 정책의 단일 진실원(Single Source of Truth)을 backoffice 인증/권한 시스템으로 세우는 것이 필요하다. + +--- + +## 2) 목표 상태 (To-Be) + +## 2.1 인증 정책 + +- 로그인 방식은 **Google OAuth 2.0 단일화** +- 비밀번호 로그인/로컬 전략 완전 제거 +- 사전 등록된 Google 계정만 접근 허용 (`allowlist`) + +## 2.2 권한 정책 + +권한 라벨(최소 단위): + +- `read/all` +- `write/all` +- `write/activity` +- `write/recruit-form` + +권한 해석 규칙: + +- `write/all`은 모든 write 라벨을 포함하는 상위 권한 +- 조회 권한은 기본적으로 `read/all` 필요 + +## 2.3 슈퍼어드민 정책 + +- 슈퍼어드민 Google 계정은 고정값(환경변수)으로 관리 +- 슈퍼어드민만 사용자 등록/권한 부여/권한 변경 가능 +- 마지막 슈퍼어드민 제거 금지 + +--- + +## 3) 사용자 시나리오 (UX 중심) + +## 3.1 로그인 UX + +- 로그인 화면에서 비밀번호 입력 제거 +- `Google로 로그인` 버튼 1개 제공 +- 인증 실패 시 사유를 사용자 친화 문구로 안내 + - 허용되지 않은 계정 + - 계정 비활성화 + - 권한 없음 + +## 3.2 권한 기반 UX + +- 읽기 전용 계정은 조회만 가능(수정 버튼 비활성 + 이유 툴팁) +- 기능별 write 권한이 없으면 해당 액션 CTA 숨김 또는 비활성화 +- API 403 발생 시 공통 에러 처리(권한 부족 메시지 + 관리자 문의 안내) + +## 3.3 관리자 UX + +- 슈퍼어드민 전용 사용자 관리 화면 제공 + - 사용자 검색/필터(활성, 비활성, 권한별) + - 권한 체크박스 기반 부여/회수 + - 변경 내역 감사 로그 조회 +- 실수 방지 UX + - 파괴적 작업(비활성화/권한 회수) 2차 확인 모달 + - 자기 자신의 super-admin 해제 불가 + +--- + +## 4) 데이터 모델 설계 (DX 중심) + +## 4.1 Sequelize 모델 + +### `users` + +- `id` (PK) +- `google_sub` (string, unique, not null) +- `email` (string, unique, not null) +- `name` (string, nullable) +- `picture_url` (string, nullable) +- `is_active` (boolean, default true) +- `is_super_admin` (boolean, default false) +- `last_login_at` (date, nullable) +- timestamps + +인덱스: + +- unique(`google_sub`) +- unique(`email`) +- index(`is_active`) + +### `permission_labels` + +- `id` (PK) +- `code` (string, unique) + 허용값: `read/all`, `write/all`, `write/activity`, `write/recruit-form` +- `description` (string) +- timestamps + +### `user_permissions` + +- `id` (PK) +- `user_id` (FK -> users.id) +- `permission_label_id` (FK -> permission_labels.id) +- timestamps + +인덱스: + +- unique(`user_id`, `permission_label_id`) +- index(`permission_label_id`) + +### `admin_audit_logs` (권장) + +- `id` (PK) +- `actor_user_id` (FK -> users.id) +- `target_user_id` (FK -> users.id, nullable) +- `action` (string) +- `before_json` (JSON) +- `after_json` (JSON) +- `ip` (string) +- `user_agent` (string) +- timestamps + +--- + +## 5) 인증/인가 아키텍처 + +## 5.1 인증 흐름 + +1. 프론트: `GET /bo/auth/google` 호출 +2. 백엔드: Google OAuth consent 페이지로 리다이렉트 +3. Google callback: `GET /bo/auth/google/callback` +4. 서버: `profile.sub`, `email` 추출 +5. DB `users` 조회 +6. 미등록/비활성 계정이면 403 + 로그인 실패 페이지로 리다이렉트 +7. 등록 계정이면 `req.login()` 후 세션 발급 +8. 프론트에서 `/bo/auth/me`로 사용자/권한 정보 로드 + +## 5.2 Passport 구성 + +- `passport-google-oauth20` 사용 +- `serializeUser`: 최소 식별자(`user.id`)만 저장 +- `deserializeUser`: `users + permissions` 로딩 + +## 5.3 세션 저장 + +기본 권장: Redis 기반 세션 스토어 + +- 이유 + - 멀티 인스턴스/스케일아웃 대응 + - 메모리 스토어 대비 안정성 우수 + - 세션 만료/강제 로그아웃 관리 용이 + +쿠키 정책: + +- `httpOnly: true` +- `secure: true` (prod) +- `sameSite: "None"` (cross-site 운영 시) +- `maxAge`: 2시간(요구 운영 정책에 따라 조정) + +--- + +## 6) 백엔드 API 명세 (Auth/Admin) + +## 6.1 Auth API + +### `GET /bo/auth/google` + +- 설명: Google OAuth 시작 +- 응답: Google로 리다이렉트 + +### `GET /bo/auth/google/callback` + +- 설명: OAuth 콜백 처리 +- 성공: FE 성공 URL로 리다이렉트 +- 실패: FE 로그인 URL로 리다이렉트(`?reason=unauthorized`) + +### `GET /bo/auth/me` + +- 설명: 현재 로그인 사용자 정보 +- 응답 예: + +```json +{ + "id": 12, + "email": "user@uos.ac.kr", + "name": "홍길동", + "is_super_admin": false, + "permissions": ["read/all", "write/activity"] +} +``` + +### `POST /bo/auth/logout` + +- 설명: 세션 종료 +- 응답: 200 + +## 6.2 Super Admin 전용 User Management API + +prefix: `/bo/admin/users` + +### `GET /bo/admin/users` + +- 사용자 목록 + 권한 + 활성 상태 조회 + +### `POST /bo/admin/users` + +- 사용자 등록 +- body: + - `email` (required) + - `name` (optional) + - `permissions` (array) + - `is_active` (optional, default true) + +### `PATCH /bo/admin/users/:id` + +- 사용자 기본 정보/활성 상태/권한 일괄 수정 + +### `POST /bo/admin/users/:id/permissions` + +- 권한 추가 +- body: `code` + +### `DELETE /bo/admin/users/:id/permissions/:code` + +- 권한 제거 + +### `GET /bo/admin/audit-logs` + +- 감사 로그 조회(페이징/필터) + +--- + +## 7) 권한 체크 미들웨어 설계 + +## 7.1 미들웨어 목록 + +- `requireAuth` + - 세션 존재 + 사용자 활성 상태 확인 +- `requireAnyPermission([...codes])` + - 권한 교집합 확인 +- `requireSuperAdmin` + - `is_super_admin === true` + 고정 슈퍼어드민 이메일 검증 + +## 7.2 권한 판정 규칙 + +- `write/all` 보유 시: + - `write/activity`, `write/recruit-form` 자동 허용 +- 읽기 API: + - `read/all` 또는 `write/all` 허용(운영 정책 선택) + +--- + +## 8) 기존 API 권한 매핑 + +- `GET /bo/member` -> `read/all` +- `GET /bo/member/pdf/:filename` -> `read/all` +- `POST /bo/semina` -> `write/activity` 또는 `write/all` +- `GET /bo/feature/recruit` -> `read/all` +- `PATCH /bo/feature/recruit` -> `write/recruit-form` 또는 `write/all` + +추가 제안: + +- 권한 실패 응답 포맷 통일 + +```json +{ + "code": "FORBIDDEN", + "message": "해당 작업 권한이 없습니다.", + "required_permissions": ["write/recruit-form", "write/all"] +} +``` + +--- + +## 9) 프론트 명세 (Backoffice) + +## 9.1 로그인 페이지 + +- 기존 비밀번호 인풋 제거 +- 버튼: `Google로 로그인` +- 실패 사유별 UI: + - `unauthorized_account` + - `inactive_account` + - `no_permission` + +## 9.2 앱 부팅 흐름 + +1. 앱 시작 시 `/bo/auth/me` 호출 +2. 성공 시 사용자 상태 전역 저장 +3. 실패 시 로그인 페이지 유지 +4. 라우트 가드에서 권한 확인 + +## 9.3 권한 기반 컴포넌트 제어 + +- `Can` 컴포넌트(또는 hook) 도입 + - `can("write/recruit-form")` 형태로 사용 + - 버튼 숨김/비활성화 정책 일관화 + +--- + +## 10) 보안 요구사항 + +- OAuth state 검증 필수(CSRF 방지) +- 세션 고정 공격 방지(`session.regenerate`) +- CORS는 정확한 Origin 화이트리스트만 허용 +- 허용 계정 검증은 반드시 서버 DB 기준 +- 에러 메시지에 내부 정책 과다 노출 금지 +- 사용자/권한 변경 API는 모두 감사 로그 적재 + +--- + +## 11) DX(확장성/운영성) 설계 포인트 + +- 권한 문자열 상수화(`PermissionCodes`)로 오타 방지 +- 라우트-권한 매핑 중앙 관리(예: `authorizationMap.ts`) +- 테스트 우선순위: + - 인증 성공/실패 + - 미등록 계정 차단 + - 권한별 200/403 + - super-admin 전용 API 보호 +- 운영 편의: + - 관리자 변경 로그 검색 + - 계정 비활성화 즉시 반영 + - 세션 만료/강제 로그아웃 정책 문서화 + +--- + +## 12) 단계별 구현 계획 (Small Steps) + +1. 모델/마이그레이션 추가 (`users`, `permission_labels`, `user_permissions`, `admin_audit_logs`) +2. permission label 시드 데이터 삽입 +3. Google OAuth 전략 추가 + local 로그인 제거 +4. `/bo/auth/google`, `/callback`, `/me`, `/logout` 구현 +5. 세션 스토어 Redis 적용 +6. `requireAuth`, `requireAnyPermission`, `requireSuperAdmin` 구현 +7. 기존 업무 API에 권한 매핑 적용 +8. super-admin 사용자 관리 API 구현 +9. 프론트 로그인 UI/라우트가드/권한 가드 반영 +10. 통합 테스트 + 운영 점검 + 배포 + +--- + +## 13) 수용 기준 (Acceptance Criteria) + +- 비밀번호 로그인 경로 제거 및 사용 불가 +- 등록되지 않은 Google 계정 로그인 차단 +- 권한 없는 사용자의 쓰기 API 호출 시 403 +- 슈퍼어드민이 아닌 사용자는 계정/권한 관리 API 접근 불가 +- 슈퍼어드민은 사용자 등록 및 권한 변경 가능 +- 로그인 후 `/bo/auth/me`에서 권한 라벨 확인 가능 +- 감사 로그에 사용자/권한 변경 이력이 남음 + +--- + +## 14) 현재 코드 대비 변경 영향 요약 + +- Backoffice는 인증 코어가 전면 교체된다(`local -> google oauth`). +- 라우트 보호 기준이 `로그인 여부`에서 `로그인 + 권한`으로 강화된다. +- 운영팀의 사용자 관리 기능이 코드/DB/UX 전반에 새로 추가된다. +- Main은 직접 인증 기능 추가 대상은 아니지만, 운영 제어 기능의 신뢰 소스가 Backoffice 권한 모델로 정리된다. From ce9daf9e14bdc654302e7b79f695720bf47cde0c Mon Sep 17 00:00:00 2001 From: a123 Date: Wed, 18 Mar 2026 00:37:15 +0900 Subject: [PATCH 2/5] docs(bo-auth): add security release gate checklist --- 2026-renewal/feature/01-bo-auth.md | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/2026-renewal/feature/01-bo-auth.md b/2026-renewal/feature/01-bo-auth.md index f4e5239..4a461d6 100644 --- a/2026-renewal/feature/01-bo-auth.md +++ b/2026-renewal/feature/01-bo-auth.md @@ -91,8 +91,8 @@ - 권한 체크박스 기반 부여/회수 - 변경 내역 감사 로그 조회 - 실수 방지 UX - - 파괴적 작업(비활성화/권한 회수) 2차 확인 모달 - - 자기 자신의 super-admin 해제 불가 + - 위험한 작업(예 : 계정 비활성화, 권한 제거 등) 2차 확인창 실행. + - 자기 자신의 super-admin 해제 불가. super admin이 자기 계정에서 is_super_admin 권한을 스스로 끌 수 없게 막는 것. 마지막 관리자 권한을 실수로 없애서 아무도 사용자 / 권한 관리를 못 하게 되는 사고 방지 --- @@ -337,6 +337,26 @@ prefix: `/bo/admin/users` - 에러 메시지에 내부 정책 과다 노출 금지 - 사용자/권한 변경 API는 모두 감사 로그 적재 +## 10.1) 보안 필수 체크리스트 (출시 게이트) + +아래 항목은 운영 배포 전 `모두 충족`되어야 한다. + +- OAuth `state` 검증을 서버에서 강제한다. +- Google 사용자 식별은 `email` 단독이 아닌 `google_sub`를 기준으로 처리한다. +- 세션 쿠키는 `HttpOnly=true`, `Secure=true(prod)`, `SameSite` 정책을 명시한다. +- 로그인 성공 시 `req.session.regenerate()`로 세션 고정 공격을 방지한다. +- 모든 쓰기 API는 서버 측 권한 검사(`requireAnyPermission`)를 통과해야 한다. +- 슈퍼어드민 API는 서버 측에서 `requireSuperAdmin`으로만 접근 허용한다. +- 계정/권한 변경 이벤트는 `admin_audit_logs`에 actor/target/ip/user-agent를 기록한다. +- CSRF 보호를 적용한다(특히 쿠키 기반 세션 사용 시 필수). +- CORS 허용 Origin은 정확한 도메인만 화이트리스트로 제한한다. +- Rate limiting을 적용한다(로그인 시도, 권한 실패 반복 요청 포함). +- 권한 오류/로그인 실패/관리자 작업 로그를 모니터링 대시보드에서 확인 가능해야 한다. +- 프론트는 권한 기반 UI 제어를 하되, 권한 통제의 진실원은 서버임을 유지한다. +- XSS 방지를 위해 입력 검증/출력 인코딩/CSP 정책을 함께 적용한다. +- 퇴부원 또는 운영 제외 계정은 즉시 `is_active=false` 처리 후 접근 차단한다. +- 마지막 슈퍼어드민 제거/비활성화는 서버에서 금지한다. + --- ## 11) DX(확장성/운영성) 설계 포인트 From 0d234ba12dffcbf876d55a13968af61424439cc7 Mon Sep 17 00:00:00 2001 From: a123 Date: Mon, 23 Mar 2026 00:13:29 +0900 Subject: [PATCH 3/5] docs: sync bo auth spec and appendix from backoffice --- 2026-renewal/feature/01-bo-auth-appendix.md | 549 ++++++++++++++++++++ 2026-renewal/feature/01-bo-auth.md | 545 ++++++------------- 2 files changed, 704 insertions(+), 390 deletions(-) create mode 100644 2026-renewal/feature/01-bo-auth-appendix.md diff --git a/2026-renewal/feature/01-bo-auth-appendix.md b/2026-renewal/feature/01-bo-auth-appendix.md new file mode 100644 index 0000000..b1f86f7 --- /dev/null +++ b/2026-renewal/feature/01-bo-auth-appendix.md @@ -0,0 +1,549 @@ +# 01. BO Auth 기술 상세 Appendix + +## 문서 관계 / 소스 오브 트루스 + +- 이 Appendix는 기술 구현 기준 문서다. +- 기존 [01-bo-auth.md](./01-bo-auth.md)의 기술 상세(데이터 모델, 인증/토큰, 권한 검사, 인덱스, 에러 코드)는 이 Appendix 기준으로 대체한다. +- PM 의사결정/일정/리스크는 [01-bo-auth-plan.md](./01-bo-auth.md)를 기준으로 본다. + +## A) 목표 아키텍처 요약 + +- 인증: Google OAuth +- API 인증 유지: Authorization Bearer Token +- DB: MongoDB +- 권한: `perm` 비트마스크 +- 계정 온보딩: 초대 링크 기반 + +## B) 데이터 모델 + +- IP 개인정보 최소화 정책은 `admin_audit_logs`, `refresh_tokens` 모두에 동일 적용한다(원문 IP 미저장, `ipHash` 사용). + +## B.1 `admin_users` + +- `email` (unique, required) +- `googleSub` (unique, nullable) +- `name` (nullable) +- `pictureUrl` (nullable) +- `perm` (number, default `READ`) +- `isSuperAdmin` (boolean, default false) +- `isActive` (boolean, default true) +- `lastLoginAt` +- `createdAt`, `updatedAt` + +## B.2 `admin_invites` + +- `email` (required) +- `perm` (number, default `READ`) +- `tokenHash` (unique, required) +- `expiresAt` (required) +- `status` (`pending`, `used`, `expired`, `revoked`) +- `invitedBy` (required) +- `usedByUserId` (nullable) +- `createdAt`, `updatedAt` + +## B.3 `admin_audit_logs` + +- `actorUserId` +- `targetUserId` +- `action` +- `before`, `after` +- `ipHash` (원문 IP 저장 금지, `HMAC-SHA256(ip, SERVER_SECRET_SALT)` 적용) +- `userAgent` +- `createdAt` + +## B.4 `refresh_tokens` + +- `userId` (required) +- `tokenHash` (unique, required) +- `expiresAt` (required) +- `revoked` (boolean, default false) +- `revokedAt` (nullable) +- `deviceInfo` + - `userAgent` (raw string) + - `os` (parsed) + - `browser` (parsed) + - `ipHash` (선택, 원문 IP 저장 금지 권장) + - 알고리즘: `HMAC-SHA256(ip, SERVER_SECRET_SALT)` + - 운영 정책: `SERVER_SECRET_SALT`는 운영 중 교체 금지 + - 주의: secret 변경 시 기존 `ipHash` 비교/조회가 불가능해짐 + - 조회/비교: 세션 목록/필터링은 원문 IP가 아닌 `ipHash` 기준으로 처리 + - 비상 교체 절차: + - 트리거: secret 유출이 확인/강하게 의심되는 경우 + - 조치: `refresh_tokens` 전체 revoke + 전체 재로그인 유도 공지 + - 영향: 기존 `ipHash` 기반 조회/필터링 일시 불가 + - 기록: 교체 시각/사유/조치 내역을 운영 로그 및 감사 로그에 기록 + - `lastSeenAt` (선택) +- `createdAt`, `updatedAt` + +## C) 권한 모델 + +```ts +enum Permission { + READ = 1 << 0, + WRITE_ACTIVITY = 1 << 1, + WRITE_RECRUIT_FORM = 1 << 2, + WRITE_CLUB_INFO = 1 << 3, +} +``` + +권한 체크: + +- `(perm & WRITE_ACTIVITY) !== 0` +- `(perm & WRITE_RECRUIT_FORM) !== 0` +- `(perm & WRITE_CLUB_INFO) !== 0` +- `(perm & REQUIRED_MASK) === REQUIRED_MASK` + +운영 UI: + +- 내부 저장은 비트마스크 +- 화면은 라벨(`read/all`, `write/activity`, `write/recruit-form`, `write/club-info`, `write/all`)로 표시 +- `write/all`은 별도 비트를 두지 않고 `WRITE_ACTIVITY | WRITE_RECRUIT_FORM | WRITE_CLUB_INFO` OR 합산값으로 해석한다. +- UI 역변환 규칙: `(perm & (WRITE_ACTIVITY | WRITE_RECRUIT_FORM | WRITE_CLUB_INFO)) === (WRITE_ACTIVITY | WRITE_RECRUIT_FORM | WRITE_CLUB_INFO)`이면 `write/all`로 표시하고, 아니면 보유한 개별 write 라벨만 표시한다. + +비트 관리 규칙: + +- 신규 권한은 `2^n` 값만 사용 +- 기존 비트 값 변경 금지 +- 중앙 enum 파일에서만 권한 정의 +- 32bit 초과 시 64bit/bigint 확장 정책 적용 + +## D) 인증/토큰 정책 + +## D.1 Access Token + +- 만료: 10~15분(최대 15분) +- 저장: 메모리 저장(브라우저 새로고침 시 초기화되는 런타임 메모리; 예: 앱 전역 state/in-memory store) +- `localStorage/sessionStorage` 저장 금지 +- `jti` claim 포함(필요 시 replay 탐지 확장) + +## D.2 Refresh Token + +- 만료: 14일 +- 저장: `httpOnly + Secure + SameSite` cookie 권장 +- rotation + revoke 적용 +- 다중 디바이스 분리 관리 +- FE는 refresh token 기반 silent refresh로 페이지 reload 시 세션을 복구한다. +- 도메인 정책(확정): + - Option A 채택: 리버스 프록시로 FE/BE 요청 경로를 same-site로 정렬 + - FE는 동일 사이트 경로(예: `/api`)로 호출하고 프록시가 BE로 전달 + - refresh cookie는 same-site 기준으로 처리해 Safari ITP 영향 구간을 최소화 + - `SameSite=Lax` 이상을 기본으로 하고 운영 환경은 `Secure` 필수 적용 +- 잔여 리스크 및 대응: + - 프록시 misconfiguration으로 cross-site 전송이 남는 경우 Safari에서 silent refresh 실패 가능 + - Phase 1 QA에서 Safari refresh 실패가 확인되면 프록시 규칙 우선 수정, 인프라 제약으로 불가 시 BFF 패턴 전환 + +rotation atomic 처리: + +- 동시 refresh 요청 중복 방지: + - FE 1차 방어(필수): 탭 내부 싱글턴 Promise lock으로 refresh 중복 호출을 차단한다. + - FE 2차 방어(권장): `BroadcastChannel('auth')`로 탭 간 신규 access token을 공유한다. + - BE 안전망(권장): `revokedAt` 기준 grace window(5~10초) 정책을 적용한다. + - grace window 이내 재사용: 경쟁 조건으로 판단(`REFRESH_TOKEN_REVOKED`) 후 재로그인 강제 없이 복구 경로 안내 + - grace window 초과 재사용: 탈취 의심으로 판단(`REFRESH_TOKEN_REUSE_DETECTED`) 후 전체 refresh token revoke + +1. `revoked=false` 조건으로 토큰 조회/전환 +2. 기존 토큰 revoke +3. 새 refresh token 저장 +4. 새 access token 발급 +5. 2~4 중 하나라도 실패 시 전체 롤백(트랜잭션 정책은 `N) 트랜잭션 정책` 참조) + +reuse detection: + +- revoke된 refresh token 재사용 시 `REFRESH_TOKEN_REUSE_DETECTED` +- 보안 이벤트 기록 +- 해당 사용자 전체 refresh token 강제 revoke + +## D.3 CSRF 경계 + +- 일반 Bearer API는 CSRF 영향 낮음 +- refresh 엔드포인트는 cookie 사용으로 CSRF 방어 필수 +- refresh 엔드포인트 CSRF 방어: + - 요청 시 커스텀 헤더(`X-Requested-With: XMLHttpRequest`) 필수 포함 + - 서버는 해당 헤더 부재 시 요청 거부 (`401`) + - CORS preflight 의존 방식이므로 `Access-Control-Allow-Origin` 설정 엄격 유지 필수 + - 허용 도메인은 운영 allowlist로 관리하며(예: `BO_ALLOWED_ORIGINS`), 와일드카드(`*`) 사용 금지 + - 추가 검증: `Origin`(필수) 및 `Referer`(보조) 값을 allowlist와 대조하고 불일치 시 거부 +- 한계 및 보완: + - `X-Requested-With` 단독 방식은 same-origin/프록시 환경에서 우회 가능성이 있으므로 단독 방어로 간주하지 않는다. + - 따라서 custom header + origin/referer 검증을 기본 세트로 적용한다. +- refresh 요청 통과 조건(체크리스트): + - `X-Requested-With: XMLHttpRequest` 헤더 존재 + - `Origin`이 `BO_ALLOWED_ORIGINS` allowlist와 일치 + - `Referer`(존재 시)가 `BO_ALLOWED_ORIGINS` allowlist와 일치 + - 위 조건을 모두 만족할 때만 통과, 하나라도 불일치하면 `401` 거부 + +## E) Google OAuth 보안 + +- `state` 검증 +- `email_verified` 검증 +- ID Token `issuer` 검증 +- ID Token `audience(client_id)` 검증 +- 필요 시 `hd`/도메인 제한 +- 이메일 비교는 `trim + lowercase` 정규화 후 수행 + +## F) 초대 플로우 + +1. 슈퍼어드민 이메일 입력 +2. 이메일 전용 초대 링크 생성(만료 기본 2일) +3. 사용자 링크 접속 후 Google 로그인 +4. 정규화된 이메일 일치 시 승인 +5. `googleSub` 바인딩 +6. 초대 상태 `used` 처리 + +보안: + +- 토큰 엔트로피 최소 128bit +- 토큰 원문 저장 금지, `tokenHash`만 저장 +- 1회 사용 후 즉시 폐기 +- 재발급 시 기존 활성 토큰 revoke +- 초대 검증 API rate limit 적용 +- 만료 정책: + - 기본 만료는 2일 + - 허용 범위: 최소 1시간(3600초) ~ 최대 7일(604800초) + - 하드코딩 금지, 운영 설정값으로 변경 가능 + - 슈퍼어드민은 허용 범위 내에서 초대 생성 시 만료값을 조정할 수 있다. + - 허용 범위를 벗어난 요청은 `400`으로 거부한다. +- 권한 초기값/지정: + - 초대 생성 시 기본 권한은 `READ`를 부여 + - 슈퍼어드민은 초대 생성 시 추가 write 권한(`WRITE_ACTIVITY`, `WRITE_RECRUIT_FORM`, `WRITE_CLUB_INFO`)을 사전 지정할 수 있다. + +초대 생성 API 스펙(`POST /bo/admin/invites`): + +- body: + - `email` (required, 1개) + - `perm` (optional, 기본값 `READ`) + - `expiresInSec` (optional, 기본값 172800, 허용 범위 3600~604800) +- 검증: + - `perm`에 `READ` 미포함 요청 시 서버에서 `READ`를 강제 포함 + - 범위 외 `expiresInSec` 요청은 `400`으로 거부 + +동시성: + +- `status=pending` 조건에서만 수락 허용 +- 트랜잭션 내부에서 `pending -> used` 전환 후 바인딩 +- 동시 요청은 1건만 성공 + +충돌 처리: + +- `googleSub`가 이미 다른 계정에 바인딩된 경우 `GOOGLE_SUB_CONFLICT` 반환 + +## G) 슈퍼어드민 정책 + +- 런타임 판단 기준: DB `isSuperAdmin` 단일 기준 +- 서버 시작 시 `SUPER_ADMIN_EMAIL` bootstrap 보정 +- 운영 중 env 자동 덮어쓰기 금지 +- 최소 1개 이상 비상 super admin 계정 유지(활성 상태) +- Google OAuth 장애 시 임시 로컬 인증 fallback 경로를 통해 비상 계정만 로그인 허용 +- 로컬 인증 fallback 경로는 상시 개방하지 않고 장애 대응 시에만 운영 플래그로 활성화 +- 예외 전환 규칙: Phase 1 전환 기간에는 기존 로그인 fallback 경로를 임시 활성화로 유지하고, Phase 3 완료 후에는 비상 플래그 기반 fallback만 허용한다. +- env/DB mismatch: + - 기본: 경고 + 운영 알림 + - strict mode: fail-fast + +## H) DB 재검증/권한 검사 + +라우트 레벨: + +- 모든 보호 API 1차 권한 검사 미들웨어 적용 + +서비스 레벨: + +- 민감 작업 2차 DB 재검증 + +필수 DB 재검증 대상: + +- 권한 변경 API +- 초대 생성/검증/수락 API +- 계정 활성/비활성 API +- 모든 write API +- `/bo/auth/me` (화면 기준 정보 최신화) + +성능 기준: + +- `userId` 단건 조회 중심 +- write/admin API low QPS 가정 +- 필요 시 Redis 캐시 확장 +- `/bo/auth/me`는 매 요청 DB 재검증을 기본 정책으로 한다. +- 근거: `/bo/auth/me`는 관리자 화면 권한/상태 렌더링의 기준 엔드포인트이므로 stale 권한 노출 방지를 위해 성능보다 최신 권한 일관성을 우선한다. + +## I) Rate Limiting + +대상: + +- OAuth 시작 +- OAuth callback +- 초대 검증 전 단계 +- 초대 검증 API +- refresh API + +초기값 및 조정 범위: + +| 대상 | 초기값 | 조정 범위 | 기준 | +| --- | --- | --- | --- | +| OAuth callback | IP당 10 req/min | 5~20 | 로그인 실패율 모니터링 후 | +| refresh API | 사용자당 30 req/min | 20~60 | 다중 디바이스 환경 고려 | +| invite 검증 API | IP당 10 req/min | 5~20 | 초대 남용 시 강화 | + +초기값은 백오피스 내부 사용자 규모 기준으로 설정한다. 운영 중 rate limit 초과 알림이 반복되거나 브루트포스 징후 발생 시 하향 조정하고, 정상 사용자 차단 이슈 발생 시 상향 조정한다. 조정 이력은 감사 로그에 준하여 기록한다. +구현 전제: 분산 환경 일관성을 위해 Redis 기반 rate limiter를 기본으로 사용한다(단일 인스턴스 개발 환경은 in-memory fallback 허용). + +## J) 감사 로그/모니터링 + +감사 로그: + +- append-only +- 수정/삭제 API 제공 금지 +- 필요 시 해시 체인/외부 저장 이중 적재 확장 +- retention: 180일 + +모니터링: + +- 권한 변경 +- super admin 이벤트 +- rate limit 반복 초과 +- 로그인 실패 반복 + +## K) 에러 코드 카탈로그 + +공통: + +- `UNAUTHORIZED` (`401`) +- `FORBIDDEN` (`403`) +- `TOO_MANY_REQUESTS` (`429`) +- `INTERNAL_ERROR` (`500`) + +초대/인증: + +- `INVITE_EXPIRED` (`410`) +- `INVITE_ALREADY_USED` (`409`) +- `INVITE_REVOKED` (`410`) +- `INVITE_EMAIL_MISMATCH` (`422`) +- `GOOGLE_SUB_CONFLICT` (`409`) +- `OAUTH_STATE_INVALID` (`400`) +- `EMAIL_NOT_VERIFIED` (`403`) + +토큰: + +- `REFRESH_TOKEN_INVALID` (`401`) +- `REFRESH_TOKEN_REVOKED` (`401`): 이미 revoke된 토큰 사용(grace window 이내 경쟁 조건 포함) +- `REFRESH_TOKEN_EXPIRED` (`401`) +- `REFRESH_TOKEN_REUSE_DETECTED` (`401`): revoke된 토큰의 grace window 초과 재사용(탈취 의심, 전체 토큰 강제 revoke) + +권한/계정: + +- `ACCOUNT_INACTIVE` (`403`) +- `PERMISSION_DENIED` (`403`) +- `SUPER_ADMIN_REQUIRED` (`403`) + +## L) 필수 인덱스 + +`admin_users`: + +- `email` unique +- `googleSub` unique + +`admin_invites`: + +- `tokenHash` unique +- `email + status` 복합 인덱스 +- `expiresAt` TTL 인덱스(정책 적용 시) + +`admin_audit_logs`: + +- `actorUserId` +- `createdAt` +- 필요 시 `action + createdAt` 복합 인덱스 + +`refresh_tokens`: + +- `tokenHash` unique +- `userId + revoked + revokedAt` 복합 인덱스 (grace window 판정 최적화) +- `expiresAt` TTL 인덱스(정책 적용 시) + +## M) 구현 순서 (개발자 기준) + +1. [Phase 1] MongoDB 컬렉션/인덱스 생성 (`admin_users`, `admin_invites`, `admin_audit_logs`, `refresh_tokens`) +2. [Phase 1] Google OAuth 검증 로직 구현(`state`, `email_verified`, `issuer`, `audience`) +3. [Phase 1] 토큰 발급/검증 로직 구현(access/refresh, rotation, revoke) +4. [Phase 1] 기존 `passport-local` 및 세션 직렬화/역직렬화 전략 제거 +5. [Phase 1] refresh reuse detection 구현 및 보안 이벤트 연계(배치 근거: rotation/revoke와 분리 불가한 동일 인증 경로) +6. [Phase 2] 권한 enum/비트마스크 유틸 구현(라벨 <-> 비트 변환 레이어 포함, `write/all` OR 합산/역변환 규칙 포함) +7. [Phase 2] 초대 생성/검증/수락/재발급/폐기 구현 +8. [Phase 2] `googleSub` 바인딩 충돌 정책 구현(`GOOGLE_SUB_CONFLICT`) +9. [Phase 2] 라우트 미들웨어 + 서비스 DB 재검증 적용 +10. [Phase 2] `/bo/auth/me` 최신 상태 정책 및 권한 반영 검증 +11. [Phase 2] FE 라우트 가드/`useCan()` 훅/silent refresh 실패 처리 구현(`R) FE 구현 기준` 반영) +12. [Phase 3] 감사 로그 적재/조회 및 append-only 정책 적용 +13. [Phase 3] rate limiting 및 에러 코드 매핑 적용 +14. [Phase 3] 통합 테스트 및 운영 점검 + +## N) 트랜잭션 정책 (MongoDB) + +아래 시나리오는 반드시 트랜잭션으로 처리한다. + +- 초대 수락: `invite(pending->used)` + `googleSub 바인딩` + `admin_users.perm 반영` +- 권한 변경: `admin_users.perm 변경` + `admin_audit_logs 기록` +- super-admin 변경: `admin_users.isSuperAdmin 변경` + `admin_audit_logs 기록` +- 계정 비활성화: `isActive=false` + `refresh_tokens revoke` + `audit 기록` +- refresh rotation: `old refresh revoke` + `new refresh 발급` (atomic 전환) + +트랜잭션 원칙: + +- 실패 시 전체 롤백 +- 감사 로그 쓰기 실패도 롤백 조건 +- 동시성 경합 지점은 조건부 갱신(`revoked=false`, `status=pending`)으로 원자성 보장 +- 운영 영향: 감사 로그 저장소 장애 시 권한 변경/계정 상태 변경 API가 일시 중단될 수 있음(보안 무결성 우선 정책) +- 운영 대응: 장애 알림 즉시 전파, 복구 전까지 읽기 중심 운영 모드 유지, 복구 후 변경 작업 재개 + +## O) 기존 데이터 마이그레이션 전략 + +목표: + +- 기존 관리자 계정을 신규 인증/권한 체계로 안전하게 전환한다. + +적용 시점: + +- Phase 1 완료 후, Phase 2 시작 직전 실행 +- 실제 운영 관리자 계정 수는 Phase 1 완료 시점에 확정해 본 섹션에 기록한다. +- 예상 소요: + - 관리자 계정 30개 기준 반나절~1일 + - 관리자 계정 100개 기준 1~2일(초대 수락 속도에 따라 변동) + +기존 계정 처리 원칙: + +- `googleSub`가 없는 기존 계정은 초대 플로우 재온보딩을 기본 경로로 사용 +- 운영상 즉시 전환이 필요한 계정은 제한적으로 마이그레이션 스크립트 사용 가능(슈퍼어드민 승인 필수) +- 비밀번호 로그인 데이터는 신규 인증 기준에서 참조하지 않는다. + +실행 절차: + +1. 마이그레이션 대상 계정 목록 확정(활성 계정 기준) +2. 기존 권한 -> 신규 `perm` 매핑 테이블 확정 +3. 계정별 초대 발송 또는 승인된 스크립트 전환 수행 +4. 전환 완료 계정의 `googleSub` 바인딩 및 로그인 검증 +5. 권한 대조 리포트 생성(기존 권한 vs 신규 `perm`) + +검증 기준: + +- 마이그레이션 전/후 활성 관리자 계정 수 일치 +- 권한 매핑 불일치 0건 +- 전환 대상 계정의 Google 로그인 성공 확인 + +롤백 기준 및 절차: + +- 롤백 임계치: + - 권한 매핑 불일치 1건 이상 발생 시 즉시 중단 + - 전환 대상 계정 로그인 실패율 5% 초과 시 즉시 중단 +- 임계치 초과 시 신규 적용 중단 +- 영향 계정에 대해 기존 운영 경로(임시 fallback)로 즉시 복귀 +- 원인 수정 후 재실행 + +## P) 테스트 전략 + +단위 테스트: + +- [Phase 1] OAuth 검증: `state` 불일치, `email_verified=false`, `issuer/audience` 실패 +- [Phase 1] 토큰: 발급/검증/만료/revoke/rotation/reuse detection +- [Phase 2] 권한: `perm` 비트 연산, 복합 마스크 검사, 경계값 검증 + +통합 테스트: + +- [Phase 1] refresh rotation atomic 보장(동시 요청 포함) +- [Phase 1] 탭 내부 동시 401 상황에서 refresh API 1회만 호출되는지 검증(싱글턴 Promise lock) +- [Phase 3] 감사 로그 append-only(수정/삭제 거부) +- [Phase 2] 권한 변경 후 `/bo/auth/me` 최신 상태 반영 + +E2E 테스트: + +- [Phase 2] 초대 생성 -> 링크 접속 -> Google 로그인 -> 권한 부여 +- [Phase 2] 예외 시나리오: 초대 만료/중복 수락/이메일 불일치/`GOOGLE_SUB_CONFLICT` + +Safari silent refresh QA(Phase 1 필수): + +- 환경: + - 스테이징 FE/BE를 운영과 동일한 cross-domain으로 배포해 검증 + - 브라우저/디바이스: macOS Safari, iOS Safari +- 시나리오: + - [Phase 1] access token 만료 후 자동 refresh 성공 및 원 요청 재시도 성공 + - [Phase 1] 페이지 새로고침 후 `/auth/refresh` 기반 세션 복구 성공 + - [Phase 1] Safari cross-site tracking 활성화 상태에서 refresh cookie 전달 여부 확인 + - [Phase 1] 다중 탭 동시 refresh 시 reuse detection 오탐 여부 확인 +- 판정/조치: + - 시나리오 1,2 통과 시 Phase 1 인증 기준 충족 + - 아래 조건 중 하나라도 충족하면 Safari cross-origin refresh 실패로 판정하고 BFF 패턴 전환 착수: + - 시나리오 1 실패(access token 만료 후 자동 refresh 미동작 또는 재시도 API 실패) + - 시나리오 2 실패(새로고침 후 세션 복구 실패) + - 시나리오 3에서 refresh 요청에 cookie 미첨부 확인 + - reuse detection 오탐 발생 시 동시 요청 제어(debounce/lock) 적용 후 재검증 + - grace window(5~10초) 내 재사용은 `REVOKED`로 처리되고, window 초과 재사용만 `REUSE_DETECTED`로 처리되는지 검증 + +완료 기준: + +- [Phase 1] 인증/OAuth/토큰 테스트 통과 후 Phase 2 진입 +- [Phase 2] 권한/초대 E2E 테스트 통과 후 Phase 3 진입 +- [Phase 3] 감사로그/rate limit/에러코드 회귀 테스트 통과 후 릴리스 + +## Q) 기존 API 권한 매핑 (비트마스크 기준) + +라벨-비트 기준: + +- `read/all` -> `READ` +- `write/activity` -> `WRITE_ACTIVITY` +- `write/recruit-form` -> `WRITE_RECRUIT_FORM` +- `write/club-info` -> `WRITE_CLUB_INFO` +- `write/all` -> `WRITE_ACTIVITY | WRITE_RECRUIT_FORM | WRITE_CLUB_INFO` + +라우트 매핑: + +| API | 필요 권한(라벨) | 비트마스크 조건 | +| --- | --- | --- | +| `GET /bo/member` | `read/all` | `(perm & READ) !== 0` | +| `GET /bo/member/pdf/:filename` | `read/all` | `(perm & READ) !== 0` | +| `POST /bo/semina` | `write/activity` 또는 `write/all` | `(perm & WRITE_ACTIVITY) !== 0` | +| `GET /bo/feature/recruit` | `read/all` | `(perm & READ) !== 0` | +| `PATCH /bo/feature/recruit` | `write/recruit-form` 또는 `write/all` | `(perm & WRITE_RECRUIT_FORM) !== 0` | +| `GET /bo/feature/club-info` | `read/all` | `(perm & READ) !== 0` (`신규 구현 필요`) | +| `PATCH /bo/feature/club-info` | `write/club-info` 또는 `write/all` | `(perm & WRITE_CLUB_INFO) !== 0` (`신규 구현 필요`) | +| `GET /bo/admin/users` | super-admin only | `isSuperAdmin === true` (`신규 구현 필요`) | +| `POST /bo/admin/invites` | super-admin only | `isSuperAdmin === true` (`신규 구현 필요`) | +| `PATCH /bo/admin/users/:id/perm` | super-admin only | `isSuperAdmin === true` (`신규 구현 필요`) | +| `PATCH /bo/admin/users/:id/super-admin` | super-admin only | `isSuperAdmin === true` (`신규 구현 필요`, 감사 로그 필수) | +| `PATCH /bo/admin/users/:id/active` | super-admin only | `isSuperAdmin === true` (`신규 구현 필요`) | + +## R) FE 구현 기준 + +- 앱 부팅: + - 앱 시작 시 `GET /bo/auth/me`를 호출해 사용자/권한 정보를 전역 상태(store)로 초기화한다. + - 실패(`401/403`) 시 인증 상태를 비로그인으로 전환하고 로그인 화면으로 라우팅한다. +- 라우트 가드: + - 보호 라우트 진입 전 `isAuthenticated`와 `perm/isSuperAdmin`을 검사한다. + - 가드 실패 시 권한 안내 페이지 또는 로그인 페이지로 리다이렉트한다. +- 권한 기반 UI 제어: + - `useCan()` 훅 또는 `Can` 컴포넌트로 버튼/CTA 렌더링을 제어한다. + - `write/all` 표시는 C 섹션의 UI 역변환 규칙을 동일하게 사용한다. +- silent refresh 실패 처리: + - `/auth/refresh` 실패 시 access token/사용자 상태를 즉시 초기화한다. + - 현재 페이지에서 재시도 루프 없이 로그인 페이지로 단일 리다이렉트한다. + - 필요 시 `reason=session_expired` 쿼리로 사용자 안내 문구를 노출한다. + +## S) API 응답 포맷 (`GET /bo/auth/me`) + +- 응답 원칙: + - `perm`(number)을 권한 판정의 기준값으로 사용한다. + - `permLabels`는 UI 편의를 위한 파생 필드로 제공한다. + - `permLabels`는 서버가 C 섹션의 UI 역변환 규칙(`write/all` 합산 규칙 포함)을 적용해 생성하며, FE는 이를 그대로 표시한다. + - `isSuperAdmin`는 super-admin 전용 UI/기능 노출 제어에 사용한다. + +응답 예시: + +```json +{ + "id": "1234567890abcde", + "email": "admin@uos.ac.kr", + "name": "홍길동", + "isSuperAdmin": false, + "isActive": true, + "perm": 3, + "permLabels": ["read/all", "write/activity"] +} +``` diff --git a/2026-renewal/feature/01-bo-auth.md b/2026-renewal/feature/01-bo-auth.md index 4a461d6..bf04843 100644 --- a/2026-renewal/feature/01-bo-auth.md +++ b/2026-renewal/feature/01-bo-auth.md @@ -1,410 +1,175 @@ -# 01. BO Auth 고도화 명세서 +# 01. BO Auth 실행 명세서 (PM용) -## 0) 문서 목적 - -본 문서는 `02-feature-roadmap.md`의 1번 기능(백오피스 인증/권한 체계 고도화)에 대한 구현 기준 문서다. -핵심 목표는 **Google OAuth 단일 로그인**, **사전 등록 계정만 접근 허용**, **라벨 기반 권한 분리**, **슈퍼어드민 전용 사용자 관리**를 통해 운영 보안과 유지보수성을 동시에 높이는 것이다. - -이 문서는 실제 코드베이스를 직접 확인한 뒤 작성했다. - -- backoffice 현재 인증: `passport-local + 세션` 단일 비밀번호 방식 -- main 현재 인증: 관리자 인증 체계 없음(공개 서비스 API 중심) - ---- - -## 1) 현재 구현 진단 (As-Is) - -## 1.1 Backoffice - -- 인증 방식 - - 비밀번호 입력 후 `POST /bo/auth/login` - - 서버는 `process.env.PASSWORD`(bcrypt hash)와 비교 - - 성공 시 세션에 `admin` 사용자 직렬화 -- 접근 제어 - - `isLoggedIn` 미들웨어로 로그인 여부만 확인 - - 권한 레벨(읽기/쓰기/기능별) 없음 -- 문제점 - - 계정 단위 추적 불가(누가 로그인했는지 구분 안 됨) - - 권한 분리 불가(모든 로그인 사용자가 동일 권한) - - 감사 로그/관리자 변경 이력 부재 - - 확장성 부족(운영 인원 증가 시 보안 리스크 확대) - -## 1.2 Main - -- 현재 main은 공개 사용자를 위한 서비스이며 관리자 인증 체계가 없다. -- 모집 ON/OFF 같은 운영 제어는 feature API로 조회하는 구조가 있으나, 관리자 권한 모델과 연결되어 있지 않다. -- 따라서 운영 정책의 단일 진실원(Single Source of Truth)을 backoffice 인증/권한 시스템으로 세우는 것이 필요하다. - ---- - -## 2) 목표 상태 (To-Be) - -## 2.1 인증 정책 - -- 로그인 방식은 **Google OAuth 2.0 단일화** -- 비밀번호 로그인/로컬 전략 완전 제거 -- 사전 등록된 Google 계정만 접근 허용 (`allowlist`) - -## 2.2 권한 정책 - -권한 라벨(최소 단위): - -- `read/all` -- `write/all` -- `write/activity` -- `write/recruit-form` - -권한 해석 규칙: - -- `write/all`은 모든 write 라벨을 포함하는 상위 권한 -- 조회 권한은 기본적으로 `read/all` 필요 - -## 2.3 슈퍼어드민 정책 - -- 슈퍼어드민 Google 계정은 고정값(환경변수)으로 관리 -- 슈퍼어드민만 사용자 등록/권한 부여/권한 변경 가능 -- 마지막 슈퍼어드민 제거 금지 - ---- - -## 3) 사용자 시나리오 (UX 중심) - -## 3.1 로그인 UX - -- 로그인 화면에서 비밀번호 입력 제거 -- `Google로 로그인` 버튼 1개 제공 -- 인증 실패 시 사유를 사용자 친화 문구로 안내 - - 허용되지 않은 계정 - - 계정 비활성화 - - 권한 없음 - -## 3.2 권한 기반 UX - -- 읽기 전용 계정은 조회만 가능(수정 버튼 비활성 + 이유 툴팁) -- 기능별 write 권한이 없으면 해당 액션 CTA 숨김 또는 비활성화 -- API 403 발생 시 공통 에러 처리(권한 부족 메시지 + 관리자 문의 안내) - -## 3.3 관리자 UX - -- 슈퍼어드민 전용 사용자 관리 화면 제공 - - 사용자 검색/필터(활성, 비활성, 권한별) - - 권한 체크박스 기반 부여/회수 - - 변경 내역 감사 로그 조회 -- 실수 방지 UX - - 위험한 작업(예 : 계정 비활성화, 권한 제거 등) 2차 확인창 실행. - - 자기 자신의 super-admin 해제 불가. super admin이 자기 계정에서 is_super_admin 권한을 스스로 끌 수 없게 막는 것. 마지막 관리자 권한을 실수로 없애서 아무도 사용자 / 권한 관리를 못 하게 되는 사고 방지 - ---- - -## 4) 데이터 모델 설계 (DX 중심) - -## 4.1 Sequelize 모델 - -### `users` - -- `id` (PK) -- `google_sub` (string, unique, not null) -- `email` (string, unique, not null) -- `name` (string, nullable) -- `picture_url` (string, nullable) -- `is_active` (boolean, default true) -- `is_super_admin` (boolean, default false) -- `last_login_at` (date, nullable) -- timestamps - -인덱스: - -- unique(`google_sub`) -- unique(`email`) -- index(`is_active`) - -### `permission_labels` - -- `id` (PK) -- `code` (string, unique) - 허용값: `read/all`, `write/all`, `write/activity`, `write/recruit-form` -- `description` (string) -- timestamps - -### `user_permissions` - -- `id` (PK) -- `user_id` (FK -> users.id) -- `permission_label_id` (FK -> permission_labels.id) -- timestamps - -인덱스: - -- unique(`user_id`, `permission_label_id`) -- index(`permission_label_id`) - -### `admin_audit_logs` (권장) - -- `id` (PK) -- `actor_user_id` (FK -> users.id) -- `target_user_id` (FK -> users.id, nullable) -- `action` (string) -- `before_json` (JSON) -- `after_json` (JSON) -- `ip` (string) -- `user_agent` (string) -- timestamps - ---- - -## 5) 인증/인가 아키텍처 +## Executive Summary -## 5.1 인증 흐름 +- Safari 인증 이슈와 권한 모델 확장성 문제를 동시에 해결한다. +- 인증은 Google OAuth + Authorization 토큰 기반으로 전환한다. +- 권한은 확장 가능한 구조로 단순화하고, 슈퍼어드민 운영 통제를 강화한다. +- 2주 내 단계적 전환과 rollback 전략으로 도입 리스크를 관리한다. -1. 프론트: `GET /bo/auth/google` 호출 -2. 백엔드: Google OAuth consent 페이지로 리다이렉트 -3. Google callback: `GET /bo/auth/google/callback` -4. 서버: `profile.sub`, `email` 추출 -5. DB `users` 조회 -6. 미등록/비활성 계정이면 403 + 로그인 실패 페이지로 리다이렉트 -7. 등록 계정이면 `req.login()` 후 세션 발급 -8. 프론트에서 `/bo/auth/me`로 사용자/권한 정보 로드 - -## 5.2 Passport 구성 - -- `passport-google-oauth20` 사용 -- `serializeUser`: 최소 식별자(`user.id`)만 저장 -- `deserializeUser`: `users + permissions` 로딩 - -## 5.3 세션 저장 - -기본 권장: Redis 기반 세션 스토어 - -- 이유 - - 멀티 인스턴스/스케일아웃 대응 - - 메모리 스토어 대비 안정성 우수 - - 세션 만료/강제 로그아웃 관리 용이 - -쿠키 정책: - -- `httpOnly: true` -- `secure: true` (prod) -- `sameSite: "None"` (cross-site 운영 시) -- `maxAge`: 2시간(요구 운영 정책에 따라 조정) - ---- - -## 6) 백엔드 API 명세 (Auth/Admin) - -## 6.1 Auth API - -### `GET /bo/auth/google` - -- 설명: Google OAuth 시작 -- 응답: Google로 리다이렉트 - -### `GET /bo/auth/google/callback` - -- 설명: OAuth 콜백 처리 -- 성공: FE 성공 URL로 리다이렉트 -- 실패: FE 로그인 URL로 리다이렉트(`?reason=unauthorized`) - -### `GET /bo/auth/me` - -- 설명: 현재 로그인 사용자 정보 -- 응답 예: - -```json -{ - "id": 12, - "email": "user@uos.ac.kr", - "name": "홍길동", - "is_super_admin": false, - "permissions": ["read/all", "write/activity"] -} -``` - -### `POST /bo/auth/logout` - -- 설명: 세션 종료 -- 응답: 200 - -## 6.2 Super Admin 전용 User Management API - -prefix: `/bo/admin/users` - -### `GET /bo/admin/users` - -- 사용자 목록 + 권한 + 활성 상태 조회 - -### `POST /bo/admin/users` - -- 사용자 등록 -- body: - - `email` (required) - - `name` (optional) - - `permissions` (array) - - `is_active` (optional, default true) - -### `PATCH /bo/admin/users/:id` - -- 사용자 기본 정보/활성 상태/권한 일괄 수정 - -### `POST /bo/admin/users/:id/permissions` - -- 권한 추가 -- body: `code` - -### `DELETE /bo/admin/users/:id/permissions/:code` - -- 권한 제거 - -### `GET /bo/admin/audit-logs` - -- 감사 로그 조회(페이징/필터) - ---- - -## 7) 권한 체크 미들웨어 설계 - -## 7.1 미들웨어 목록 - -- `requireAuth` - - 세션 존재 + 사용자 활성 상태 확인 -- `requireAnyPermission([...codes])` - - 권한 교집합 확인 -- `requireSuperAdmin` - - `is_super_admin === true` + 고정 슈퍼어드민 이메일 검증 - -## 7.2 권한 판정 규칙 - -- `write/all` 보유 시: - - `write/activity`, `write/recruit-form` 자동 허용 -- 읽기 API: - - `read/all` 또는 `write/all` 허용(운영 정책 선택) - ---- - -## 8) 기존 API 권한 매핑 - -- `GET /bo/member` -> `read/all` -- `GET /bo/member/pdf/:filename` -> `read/all` -- `POST /bo/semina` -> `write/activity` 또는 `write/all` -- `GET /bo/feature/recruit` -> `read/all` -- `PATCH /bo/feature/recruit` -> `write/recruit-form` 또는 `write/all` - -추가 제안: - -- 권한 실패 응답 포맷 통일 +## 0) 문서 목적 -```json -{ - "code": "FORBIDDEN", - "message": "해당 작업 권한이 없습니다.", - "required_permissions": ["write/recruit-form", "write/all"] -} -``` +본 문서는 백오피스 인증/권한 체계 고도화의 PM 의사결정용 실행 계획이다. +기술 구현 세부는 별도 Appendix 문서로 분리한다. ---- +- 기술 Appendix: [01-bo-auth-appendix.md](./01-bo-auth-appendix.md) -## 9) 프론트 명세 (Backoffice) +## 1) Expected Impact -## 9.1 로그인 페이지 +- Safari의 third-party cookie 제한 이슈를 제거하기 위해 cookie 기반 인증에서 Authorization header 기반 인증으로 전환, 인증 실패율 감소(특히 Safari 환경) +- 권한 관련 운영 이슈 감소(CS/운영 문의 티켓 기준) +- 관리자 계정 생성/권한 변경 작업 시간 감소 +- 계정/권한 보안 사고 리스크 감소 +- 정량 목표/측정 기준은 `9) Success Metrics`를 따른다. +- baseline은 개편 전 최근 7일 서버 로그인 API 로그를 기준으로 산정한다. -- 기존 비밀번호 인풋 제거 -- 버튼: `Google로 로그인` -- 실패 사유별 UI: - - `unauthorized_account` - - `inactive_account` - - `no_permission` +## 2) Risk if not implemented -## 9.2 앱 부팅 흐름 +- Safari 인증 이슈가 지속된다. +- 권한 확장 시 기술 부채가 누적된다. +- 계정/보안 운영 비용이 계속 증가한다. -1. 앱 시작 시 `/bo/auth/me` 호출 -2. 성공 시 사용자 상태 전역 저장 -3. 실패 시 로그인 페이지 유지 -4. 라우트 가드에서 권한 확인 +### Phase 1 (3/24~3/27, 4일): 인증 전환 -## 9.3 권한 기반 컴포넌트 제어 +- Google OAuth + Authorization 토큰 기반 로그인 전환 +- 크로스 도메인 안정화 +- 기존 로그인 fallback 유지 -- `Can` 컴포넌트(또는 hook) 도입 - - `can("write/recruit-form")` 형태로 사용 - - 버튼 숨김/비활성화 정책 일관화 +완료 기준: ---- +- Safari 포함 주요 브라우저 로그인 성공 +- 인증 성공률 99% 이상 (QA + staging 로그 기준) +- Safari 환경에서 access token 만료 후 refresh 기반 세션 복구(silent refresh) 성공 +- Safari cross-domain refresh 검증은 Appendix `P) 테스트 전략`의 QA 시나리오 기준으로 판정 -## 10) 보안 요구사항 +### Phase 2 (3/28~4/2, 5일): 권한 모델 전환 -- OAuth state 검증 필수(CSRF 방지) -- 세션 고정 공격 방지(`session.regenerate`) -- CORS는 정확한 Origin 화이트리스트만 허용 -- 허용 계정 검증은 반드시 서버 DB 기준 -- 에러 메시지에 내부 정책 과다 노출 금지 -- 사용자/권한 변경 API는 모두 감사 로그 적재 +- 권한 모델 전환(확장 가능한 구조) +- 관리자 권한 관리 UX 정리 +- `/bo/auth/me` 최신 상태 기준 확정 +- 리스크: + - 권한 모델 전환 시 기존 권한 불일치 가능성 +- 대응: + - 병행 검증 기간: Phase 2 전체 기간(3/28~4/2) + - 검증 방법: 신규 권한 모델 결과와 기존 권한 기준 결과를 관리자 계정 전체 대상 대조 + - 완료 판단: QA 환경에서 권한 오검증 0건 확인 후 Phase 3 진입 + - 불일치 발생 시: 신규 모델 적용 즉시 중단 -> 원인 분석 -> 수정 후 재검증 -## 10.1) 보안 필수 체크리스트 (출시 게이트) +완료 기준: + +- 권한 오검증 0건 +- 권한 변경 즉시 반영 정책 동작 +- 초대 플로우 E2E(초대 생성 -> 링크 접속 -> Google 로그인 -> 권한 부여) QA 통과 -아래 항목은 운영 배포 전 `모두 충족`되어야 한다. +### Phase 3 (4/3~4/5, 4일): 감사/보안 고도화 -- OAuth `state` 검증을 서버에서 강제한다. -- Google 사용자 식별은 `email` 단독이 아닌 `google_sub`를 기준으로 처리한다. -- 세션 쿠키는 `HttpOnly=true`, `Secure=true(prod)`, `SameSite` 정책을 명시한다. -- 로그인 성공 시 `req.session.regenerate()`로 세션 고정 공격을 방지한다. -- 모든 쓰기 API는 서버 측 권한 검사(`requireAnyPermission`)를 통과해야 한다. -- 슈퍼어드민 API는 서버 측에서 `requireSuperAdmin`으로만 접근 허용한다. -- 계정/권한 변경 이벤트는 `admin_audit_logs`에 actor/target/ip/user-agent를 기록한다. -- CSRF 보호를 적용한다(특히 쿠키 기반 세션 사용 시 필수). -- CORS 허용 Origin은 정확한 도메인만 화이트리스트로 제한한다. -- Rate limiting을 적용한다(로그인 시도, 권한 실패 반복 요청 포함). -- 권한 오류/로그인 실패/관리자 작업 로그를 모니터링 대시보드에서 확인 가능해야 한다. -- 프론트는 권한 기반 UI 제어를 하되, 권한 통제의 진실원은 서버임을 유지한다. -- XSS 방지를 위해 입력 검증/출력 인코딩/CSP 정책을 함께 적용한다. -- 퇴부원 또는 운영 제외 계정은 즉시 `is_active=false` 처리 후 접근 차단한다. -- 마지막 슈퍼어드민 제거/비활성화는 서버에서 금지한다. +- 감사 로그/보안 이벤트 모니터링 +- 보안 정책 마감(rate limit, 토큰 운영, 경보 연계) +- 릴리스 점검 및 운영 인수 ---- +완료 기준: -## 11) DX(확장성/운영성) 설계 포인트 - -- 권한 문자열 상수화(`PermissionCodes`)로 오타 방지 -- 라우트-권한 매핑 중앙 관리(예: `authorizationMap.ts`) -- 테스트 우선순위: - - 인증 성공/실패 - - 미등록 계정 차단 - - 권한별 200/403 - - super-admin 전용 API 보호 -- 운영 편의: - - 관리자 변경 로그 검색 - - 계정 비활성화 즉시 반영 - - 세션 만료/강제 로그아웃 정책 문서화 - ---- - -## 12) 단계별 구현 계획 (Small Steps) - -1. 모델/마이그레이션 추가 (`users`, `permission_labels`, `user_permissions`, `admin_audit_logs`) -2. permission label 시드 데이터 삽입 -3. Google OAuth 전략 추가 + local 로그인 제거 -4. `/bo/auth/google`, `/callback`, `/me`, `/logout` 구현 -5. 세션 스토어 Redis 적용 -6. `requireAuth`, `requireAnyPermission`, `requireSuperAdmin` 구현 -7. 기존 업무 API에 권한 매핑 적용 -8. super-admin 사용자 관리 API 구현 -9. 프론트 로그인 UI/라우트가드/권한 가드 반영 -10. 통합 테스트 + 운영 점검 + 배포 - ---- - -## 13) 수용 기준 (Acceptance Criteria) - -- 비밀번호 로그인 경로 제거 및 사용 불가 -- 등록되지 않은 Google 계정 로그인 차단 -- 권한 없는 사용자의 쓰기 API 호출 시 403 -- 슈퍼어드민이 아닌 사용자는 계정/권한 관리 API 접근 불가 -- 슈퍼어드민은 사용자 등록 및 권한 변경 가능 -- 로그인 후 `/bo/auth/me`에서 권한 라벨 확인 가능 -- 감사 로그에 사용자/권한 변경 이력이 남음 - ---- - -## 14) 현재 코드 대비 변경 영향 요약 - -- Backoffice는 인증 코어가 전면 교체된다(`local -> google oauth`). -- 라우트 보호 기준이 `로그인 여부`에서 `로그인 + 권한`으로 강화된다. -- 운영팀의 사용자 관리 기능이 코드/DB/UX 전반에 새로 추가된다. -- Main은 직접 인증 기능 추가 대상은 아니지만, 운영 제어 기능의 신뢰 소스가 Backoffice 권한 모델로 정리된다. +- 보안 체크리스트 충족(Appendix `D`, `I`, `J`, `K`, `P` 기준) +- 운영 대시보드/알림 체계 확인 +- 기존 세션 로그인 경로 제거 완료(rollback 비상 경로 제외) + +## 4) 설계 선택 근거 (요약) + +- 인증 방식 전환(세션 쿠키 -> Authorization header): + - FE/BE 크로스 도메인 환경에서 Safari third-party cookie 제한 이슈를 줄이기 위한 선택 + - Access Token은 header로 전달해 인증 안정성을 확보하고, refresh는 별도 보안 정책으로 관리 +- 슈퍼어드민 판단 기준 단일화: + - 서버 시작 시 `SUPER_ADMIN_EMAIL`로 DB를 bootstrap(보정)하되 + - 런타임 권한 판단은 항상 DB `isSuperAdmin`만 사용 +- 권한 모델 전환(라벨 조인 -> `perm` 비트마스크): + - 권한 라벨 증가 시 조인 복잡도를 줄이고 확장성을 확보 + - 내부 저장은 비트마스크, 운영 UI는 기존 라벨 형태를 유지해 가독성 보전 +- 데이터 저장소 전환(MySQL/Sequelize -> MongoDB): + - 메인/백오피스 DB 스택을 단일화해 운영 복잡도와 이중 관리 비용을 낮춤 + - 문서/구현 기준은 MongoDB 모델을 소스 오브 트루스로 사용 + +## 5) Prerequisites / Dependencies + +Phase 1 시작 게이팅(필수, Day 1 전 완료): + +- Google Cloud OAuth 설정(Client ID/Secret, Redirect URI) +- FE/BE 도메인/CORS/TLS 확정 +- Safari 대응 인증 경로 확정: Option A(리버스 프록시 기반 same-site 정렬) 적용 + - FE 도메인에서 `/api` 경로를 BE로 프록시해 refresh cookie를 same-site로 처리 + +준비 권장 항목(병행 진행 가능): + +- 운영 알림 채널(이메일/슬랙) 준비 +- 비상 계정 운영 정책 승인 +- IP 개인정보 최소화 정책 확정 + - 원문 IP 저장 대신 `ipHash(HMAC-SHA256 + 고정 서버 secret salt)` 사용 + - 운영 중 secret 교체 금지(교체 시 기존 비교/조회 불가) + +## 6) 운영 UX 변화 + +Before/After: + +- 로그인 + - Before: 아이디/비밀번호 입력 및 세션 쿠키 기반 로그인(브라우저/크로스도메인 이슈 존재) + - After: Google 로그인 버튼 1개 + Authorization 기반 인증 +- 계정 생성 + - Before: 수동 계정 생성/전달 중심 + - After: 슈퍼어드민이 이메일 기반 초대 링크 발급 후 사용자 온보딩(초대 시 권한 사전 지정 가능) +- 권한 변경 + - Before: 권한 기준/변경 이력 가시성이 낮음 + - After: 라벨 기반 권한 UI + 권한 변경 이력 확인 +- 초대 만료 관리 + - Before: 만료 정책 운영 기준 불명확 + - After: 기본 2일 정책을 기준으로 초대 만료값을 관리(슈퍼어드민 권한 범위 내 조정 가능) + +초대 플로우 상세: + +1. 슈퍼어드민이 이메일 1개를 입력해 해당 이메일 전용 초대 링크 1개를 생성한다. +2. 초대 시 기본 권한은 읽기 전용(`read/all`)이며, 슈퍼어드민이 필요 시 write 권한을 사전 지정할 수 있다. +3. 초대 링크는 만료 시간(기본 2일)을 포함하며, 만료/재발급 상태를 운영 화면에서 확인한다. +4. 사용자가 링크 접속 후 Google 로그인하면, 로그인 이메일과 초대 이메일 일치 여부를 검증한다. +5. 일치 시 `googleSub`를 계정에 바인딩하고 이후 로그인 식별 기준은 `googleSub`로 고정한다. + +## 7) Rollback Strategy + +- Phase 1 동안 기존 세션 로그인 경로를 임시 fallback으로 유지 +- 로그인 실패율 > 2% 또는 OAuth 오류율 급증 시 OAuth 경로 비활성화 후 기존 로그인으로 즉시 복구 +- Phase 3 완료 기준 충족 후 기존 로그인 경로 제거(rollback 비상 경로 제외) + +## 8) 비상 운영 절차 + +- 판단 주체: PM + 온콜 백엔드 리드 +- 절차: + 1. 장애 판단(로그인 실패율/에러율 기준) + 2. 공지(운영 채널/사용자 안내) + 3. fallback 적용 + 4. 원인 해결 후 OAuth 재활성화 +- 운영 리스크 공유: + - 감사 로그 저장소 장애 시 권한 변경/계정 상태 변경 API가 일시 중단될 수 있음 + - 이 경우 읽기 중심 운영 모드를 유지하고 로그 저장소 복구 후 변경 작업을 재개 +- 사후 조치: 장애 리포트 및 재발 방지 백로그 등록 + +## 9) Success Metrics + +- 인증 성공률 99.9% 이상 (서버 로그인 API 로그 기준) +- 인증 실패율 baseline 대비 50% 이상 감소 (개편 전 7일 평균 대비) +- 권한 관련 CS 티켓 0건 (Phase 3 완료 후 2주 기준) +- 초대 플로우 E2E(초대 생성 -> 링크 접속 -> Google 로그인 -> 권한 부여) QA 통과 +- Staging 환경에서 운영자 계정 1개 이상 초대 플로우 온보딩 완료 +- 슈퍼어드민 권한 변경 시 대상 계정에 즉시 반영됨을 검증 완료 + +## 10) 리소스 리스크 및 대응 + +- 리스크: 단일 개발자 의존 구조 +- 대응: + - Phase 단위 배포로 리스크 분산 + - 각 Phase 완료 시 중간 검증/승인 + - Phase 2 지연 시 관리자 권한 변경 UI를 임시 제외하고, 슈퍼어드민 API/DB 운영 절차로 권한 변경을 대체해 기능 우선 배포 + - 개발자 이탈/병가 발생 시 즉시 백엔드/프론트 대체 담당자를 지정하고, Phase 1(인증 안정화) 범위 우선으로 축소 운영 + +## 11) 수용 기준 + +- 2주 마일스톤 내 Phase 1~3 완료 +- 인증/권한/운영 절차가 문서 기준으로 인수 가능 +- 롤백/비상 대응 절차가 실제 운영 가능 상태 +- 기술 상세 항목은 Appendix 기준으로 구현/검증 완료 From 2b0d7d2f4c0c1a27b428a3112a3e3eef679a002e Mon Sep 17 00:00:00 2001 From: Menstear Date: Mon, 23 Mar 2026 00:24:30 +0900 Subject: [PATCH 4/5] Update 01-bo-auth.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 이번 명세서는 코멘트를 반영해 기존 인증/권한 설계를 재정비한 버전입니다. 핵심 변경은 운영 혼선을 줄이고, 보안/확장성을 동시에 확보하는 데 목적이 있습니다. 주요 수정 사항: 슈퍼어드민 판단 로직을 단일 기준으로 통일했습니다. 서버 시작 시 환경변수 기반 bootstrap은 초기 지정 용도로만 사용하고, 런타임 권한 판단은 DB의 isSuperAdmin만 신뢰하도록 정리했습니다. write/club-info 라벨을 추가하고 권한 모델을 개선했습니다. 내부 저장은 perm 비트마스크로 단순화하여 권한 확장 시 코드 복잡도를 낮추고, 운영 UI는 기존 라벨 형태로 유지해 가독성을 보장합니다. 인증 방식은 Authorization 기반으로 전환했습니다. 기존 세션 쿠키 중심 구조에서 벗어나 Bearer Token 중심으로 구성해 크로스도메인 환경의 안정성과 제어 가능성을 높였습니다. 데이터 저장소는 MongoDB 기준으로 명세를 재작성했습니다. 기존 Sequelize/MySQL 중심 문서에서 벗어나, admin_users, admin_invites, refresh_tokens, admin_audit_logs 컬렉션 중심으로 일관되게 정리했습니다. Backoffice 슈퍼어드민 초대 플로우 구현 계획을 구체화했습니다. 이메일 1개당 초대 링크 1개, 만료시간 포함, 초대 이메일과 Google 로그인 이메일 일치 검증, googleSub 바인딩, 이후 googleSub 기준 로그인까지 end-to-end 흐름을 명확히 정의했습니다. 보안 강화 항목: OAuth 검증(state, email_verified, issuer, audience) 및 CSRF/CORS 정책을 강화했습니다. Access/Refresh 토큰 수명, rotation, revoke, reuse detection 정책을 명확히 분리했습니다. 동시 refresh 요청 경쟁 조건을 고려해 FE lock + BE grace window 정책을 추가했습니다. 권한/계정 변경, 초대 수락 등 민감 경로는 트랜잭션 + 감사 로그를 기본으로 적용했습니다. super-admin 이벤트, rate limit 초과, 로그인 실패 반복 등 보안 이벤트 모니터링 기준을 포함했습니다. 즉, 이번 개편은 “로그인 방식 변경”에 그치지 않고, 운영자 계정 온보딩, 권한 확장, 보안 통제, 감사 가능성까지 포함한 Backoffice 인증/권한 체계의 표준화를 목표로 합니다. --- 2026-renewal/feature/01-bo-auth.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/2026-renewal/feature/01-bo-auth.md b/2026-renewal/feature/01-bo-auth.md index bf04843..7889a9c 100644 --- a/2026-renewal/feature/01-bo-auth.md +++ b/2026-renewal/feature/01-bo-auth.md @@ -1,4 +1,4 @@ -# 01. BO Auth 실행 명세서 (PM용) +# 01. BO Auth 실행 명세서 ## Executive Summary From c3078aa4e87a27fe1d28b167d5aabf8b007a5614 Mon Sep 17 00:00:00 2001 From: Menstear Date: Mon, 23 Mar 2026 00:26:11 +0900 Subject: [PATCH 5/5] Update 01-bo-auth.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 오타정정 --- 2026-renewal/feature/01-bo-auth.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/2026-renewal/feature/01-bo-auth.md b/2026-renewal/feature/01-bo-auth.md index 7889a9c..ccf381a 100644 --- a/2026-renewal/feature/01-bo-auth.md +++ b/2026-renewal/feature/01-bo-auth.md @@ -9,7 +9,7 @@ ## 0) 문서 목적 -본 문서는 백오피스 인증/권한 체계 고도화의 PM 의사결정용 실행 계획이다. +본 문서는 백오피스 인증/권한 체계 고도화의 실행 계획이다. 기술 구현 세부는 별도 Appendix 문서로 분리한다. - 기술 Appendix: [01-bo-auth-appendix.md](./01-bo-auth-appendix.md)