Skip to content

Commit 9355663

Browse files
SOIVsisyphus-dev-ai
andcommitted
docs: 인증 정책 문서 보안 흐름 정리
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
1 parent 6d89701 commit 9355663

1 file changed

Lines changed: 50 additions & 37 deletions

File tree

docs/v2_FINANCIAL-LEDGER/technical/02-authentication.md

Lines changed: 50 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,21 @@
2626
> Synology Account와 같은 외부 계정 복구는 Fieldstack 기본 범위에서 제외합니다.
2727
> Fieldstack는 로컬 인스턴스 기준 복구(웹/관리자/CLI)만 제공합니다.
2828
29-
### 1) ID/이메일 + 비밀번호 (기본)
29+
### 1) 이메일 + 비밀번호 (기본)
3030

3131
**일반 로그인 기본값:**
32-
- 사용자 식별자는 ID/이메일 사용
32+
- 사용자 식별자는 이메일 사용
3333
- 비밀번호는 Argon2id 해시로 저장
3434
- Whitelist 기반 접근 제어
3535

36-
### 2) TOTP 2차 인증 (Google Authenticator 등)
36+
### 2) TOTP 2차 인증 (Google Authenticator 등)
3737

38-
**적용 방식:**
39-
- 로그인 1차 성공 후 OTP 6자리 입력 (Step-up)
40-
- 2FA 등록은 계정 설정에서 ON/OFF
41-
- 관리자 계정은 2FA 필수 권장
38+
**적용 방식:**
39+
- 로그인 1차 성공 후 OTP 6자리 입력 (Step-up)
40+
- 2FA 등록은 계정 설정에서 ON/OFF
41+
- 관리자 계정은 2FA 필수
42+
43+
> 💡 관리자 계정은 **로그인 시 TOTP 2FA 필수**이며, **관리자 대시보드(설정) 접근은 관리자 PIN Step-up**으로 분리합니다.
4244
4345
**MFA(Multi-Factor Authentication):**
4446
- TOTP 2차 인증
@@ -73,18 +75,20 @@
7375
> 💡 **왜 PIN을 선택했나요?**
7476
> `architecture/01-decisions.md § 결정 #2` - 설계 근거 참고
7577
76-
### 개념
77-
78-
**이중 인증 구조:**
79-
```
80-
일반 사용:
81-
1차 로그인(이메일+비밀번호 또는 Passkey/OAuth) → 앱 접근
82-
83-
관리자 설정 접근:
84-
1차 로그인(이메일+비밀번호 또는 Passkey/OAuth) → 앱 접근
85-
+
86-
4~6자리 PIN → 관리자 설정 접근
87-
```
78+
### 개념
79+
80+
**이중 인증 구조:**
81+
```
82+
일반 사용:
83+
1차 로그인(이메일+비밀번호 또는 Passkey/OAuth) → 앱 접근
84+
85+
관리자 설정 접근:
86+
1차 로그인(이메일+비밀번호 또는 Passkey/OAuth) → 앱 접근
87+
+
88+
4~6자리 PIN → 관리자 설정 접근
89+
```
90+
91+
> 💡 관리자 계정은 로그인 단계에서 TOTP 2FA를 필수로 거칩니다. 관리자 PIN은 **대시보드 접근 시점의 추가 확인**으로, 로그인 이후 관리자 영역에 진입할 때만 요구됩니다.
8892
8993
### 용도
9094

@@ -211,15 +215,24 @@ admin_sessions 테이블은 관리자 PIN 인증 후 생성되는 임시 세션
211215

212216
**JWT 기반:**
213217

214-
JWT 토큰의 내부 구조(Payload)는 다음과 같습니다. 사용자의 고유 ID, 이메일, 역할을 포함하고, 발급 시간(iat)과 만료 시간(exp)도 함께 저장됩니다. 만료 시간은 발급 시간 기준 7일 후입니다.
218+
JWT 토큰의 내부 구조(Payload)는 다음과 같습니다. 사용자의 고유 ID, 이메일, 역할을 포함하고, 발급 시간(iat)과 만료 시간(exp)도 함께 저장됩니다. 만료 시간은 발급 시간 기준 1일 후입니다.
215219

216220
**저장 위치:**
217221
- `httpOnly` Cookie (추천)
218222
- 또는 LocalStorage (보안 주의)
219223

220-
**만료 시간:**
221-
- Access Token: 7일
222-
- Refresh Token: 30일 (선택)
224+
**만료 시간:**
225+
- Access Token: 1일(24h)
226+
- Refresh Token: 7~14일 (선택, Rotation 권장)
227+
228+
> 💡 Refresh Token을 사용하는 경우, Refresh Token은 사용 시마다 새 토큰으로 교체(Rotation)하고 이전 토큰은 즉시 무효화하는 방식을 권장합니다.
229+
230+
**쿠키 권장 설정(프로덕션):**
231+
- Access Token 쿠키(`auth_token`): `HttpOnly`, `Secure`, `SameSite=Lax`
232+
- 관리자 PIN 세션 쿠키(`admin_session`): `HttpOnly`, `Secure`, `SameSite=Strict`
233+
234+
> ⚠️ `Secure` 쿠키는 HTTPS에서만 동작합니다. 로컬 개발 환경(HTTP)에서는 `Secure` 옵션을 끄고 테스트합니다.
235+
> 쿠키 기반 인증을 사용하는 경우, 상태 변경 요청(POST/PUT/PATCH/DELETE)은 CSRF 방어(예: CSRF 토큰) 정책을 함께 적용하는 것을 권장합니다.
223236
224237
### 3. 비밀번호 분실 복구 프로세스
225238

@@ -287,9 +300,9 @@ verifySession 메서드는 세션 ID로 세션을 조회한 후, 존재하지
287300

288301
**PIN 설정 엔드포인트(POST /setup-pin):** 먼저 요청자가 관리자 역할인지 확인합니다. PIN이 4~6자리 숫자인지 검증하고, 연속된 숫자(예: 1234)나 반복된 숫자(예: 1111)인지도 체크합니다. 검증을 통과하면 PIN을 해싱하여 해시값과 Salt를 콜론(:)으로 구분하여 데이터베이스에 저장합니다.
289302

290-
**PIN 인증 엔드포인트(POST /verify-pin):** 관리자 권한을 확인한 후, 저장된 PIN 해시를 조회합니다. 입력된 PIN과 저장된 해시를 비교하여 검증합니다. 실패하면 감사 로그를 남기고 401 에러를 반환합니다. 성공하면 30분 유효한 세션을 생성하여 세션 ID를 반환합니다.
291-
292-
**관리자 설정 접근 미들웨어(requireAdminPin):** 기본 인증과 관리자 권한을 먼저 확인합니다. 요청 헤더에서 관리자 세션 ID를 추출하고, 없으면 PIN 입력이 필요하다는 응답을 반환합니다. 세션 ID가 있으면 유효성을 검증하고, 만료된 경우에도 PIN 입력을 다시 요구합니다. 검증이 통과하면 다음 단계로 넘깁니다.
303+
**PIN 인증 엔드포인트(POST /verify-pin):** 관리자 권한을 확인한 후, 저장된 PIN 해시를 조회합니다. 입력된 PIN과 저장된 해시를 비교하여 검증합니다. 실패하면 감사 로그를 남기고 401 에러를 반환합니다. 성공하면 30분 유효한 세션을 생성하고, `Set-Cookie`로 관리자 세션 쿠키(`admin_session`, `HttpOnly`)를 설정합니다. 프로덕션 환경에서는 `Secure``SameSite=Strict`를 활성화합니다.
304+
305+
**관리자 설정 접근 미들웨어(requireAdminPin):** 기본 인증과 관리자 권한을 먼저 확인합니다. `httpOnly` 쿠키(`admin_session`)에서 관리자 세션 ID를 추출하고, 없으면 PIN 입력이 필요하다는 응답을 반환합니다. 세션 ID가 있으면 유효성을 검증하고, 만료된 경우에도 PIN 입력을 다시 요구합니다. 검증이 통과하면 다음 단계로 넘깁니다.
293306

294307
---
295308

@@ -304,17 +317,17 @@ PinInput 컴포넌트는 사용자가 PIN을 입력하는 화면을 제공합니
304317

305318
각 자리마다 개별 입력란을 생성합니다. 숫자가 아닌 값은 입력할 수 없습니다. 숫자를 입력하면 자동으로 다음 입력란으로 포커스가 이동하고, Backspace를 누르면 이전 입력란으로 돌아갑니다. PIN의 모든 자리가 채워지면 onComplete 콜백을 실행하여 완성된 PIN을 전달합니다. 컴포넌트가 열리면 첫 번째 입력란에 자동으로 포커스됩니다. 에러 메시지가 있으면 입력란 아래에 빨간색으로 표시됩니다.
306319

307-
### PIN 인증 모달
320+
### PIN 인증 모달
308321

309-
AdminPinModal 컴포넌트는 관리자 PIN 입력을 위한 팝업입니다. PinInput 컴포넌트를 내부에 포함시킵니다.
322+
AdminPinModal 컴포넌트는 관리자 PIN 입력을 위한 팝업입니다. PinInput 컴포넌트를 내부에 포함시킵니다.
310323

311-
PIN 입력이 완료되면 백엔드의 /api/admin/verify-pin에 요청을 보냅니다. 응답이 성공하면 반환된 세션 ID를 sessionStorage에 저장하고 onSuccess 콜백을 실행합니다. 실패하면 'PIN이 올바르지 않습니다' 에러 메시지를 표시합니다. 요청 중에는 '인증 중...' 로딩 메시지가 표시됩니다. 모달 하단에는 이 인증은 30분간 유효하다는 안내가 표시됩니다.
324+
PIN 입력이 완료되면 백엔드의 /api/admin/verify-pin에 요청을 보냅니다. 응답이 성공하면 서버가 `Set-Cookie`로 관리자 세션 쿠키(`admin_session`, `httpOnly`)를 설정하고, 프론트는 onSuccess 콜백을 실행합니다. 실패하면 'PIN이 올바르지 않습니다' 에러 메시지를 표시합니다. 요청 중에는 '인증 중...' 로딩 메시지가 표시됩니다. 모달 하단에는 이 인증은 30분간 유효하다는 안내가 표시됩니다.
312325

313326
### Protected Admin Route
314327

315-
ProtectedAdminRoute 컴포넌트는 관리자 전용 페이지를 보호하는 라우트 래퍼입니다.
316-
317-
컴포넌트가 마운트되면 먼저 기존 관리자 세션이 있는지 확인합니다. sessionStorage에 저장된 세션 ID가 없으면 바로 PIN 모달을 표시합니다. 세션 ID가 있으면 백엔드에 세션 유효성 검증을 요청합니다. 유효하면 자식 컴포넌트를 렌더링하고, 유효하지 않으면 sessionStorage의 세션을 삭제하고 PIN 모달을 다시 표시합니다. PIN 인증이 성공하면 verified 상태를 켜고 모달을 닫습니다. 관리자가 아닌 사용자는 홈 페이지로 리다이렉트됩니다.
328+
ProtectedAdminRoute 컴포넌트는 관리자 전용 페이지를 보호하는 라우트 래퍼입니다.
329+
330+
컴포넌트가 마운트되면 먼저 백엔드에 관리자 세션 유효성 검증을 요청합니다(쿠키 기반). 유효하면 자식 컴포넌트를 렌더링하고, 유효하지 않으면 PIN 모달을 표시합니다. PIN 인증이 성공하면 verified 상태를 켜고 모달을 닫습니다. 관리자가 아닌 사용자는 홈 페이지로 리다이렉트됩니다.
318331

319332
---
320333

@@ -345,15 +358,15 @@ logFailedAttempt 함수는 PIN 인증 실패 시 사용자 ID, 실패 액션, IP
345358

346359
GET /auth/google 엔드포인트는 사용자를 Google의 OAuth 인증 페이지로 리다이렉트합니다. 프로필과 이메일 정보에 대한 권한을 요청합니다.
347360

348-
GET /auth/callback 엔드포인트는 Google에서 돌아온 콜백을 처리합니다. 총 7단계로 진행됩니다. 첫째로 Google에서 반환된 Authorization Code를 Access Token으로 교환합니다. 둘째로 Access Token으로 사용자의 프로필 정보를 조회합니다. 셋째로 조회된 이메일이 Whitelist에 있는지 확인하고, 없으면 접근 거부합니다. 넷째로 사용자가 이미 존재하면 마지막 로그인 시간과 프로필 사진을 업데이트하고, 없으면 새로 생성합니다. 다섯째로 사용자 ID, 이메일, 역할로 JWT 토큰을 생성합니다. 여섯째로 생성된 토큰을 httpOnly Cookie로 저장하며, 프로덕션 환경에서는 secure 플래그도 활성화하고, 만료 시간은 7일로 설정합니다. 일곱째로 메인 화면으로 리다이렉트합니다.
361+
GET /auth/callback 엔드포인트는 Google에서 돌아온 콜백을 처리합니다. 총 7단계로 진행됩니다. 첫째로 Google에서 반환된 Authorization Code를 Access Token으로 교환합니다. 둘째로 Access Token으로 사용자의 프로필 정보를 조회합니다. 셋째로 조회된 이메일이 Whitelist에 있는지 확인하고, 없으면 접근 거부합니다. 넷째로 사용자가 이미 존재하면 마지막 로그인 시간과 프로필 사진을 업데이트하고, 없으면 새로 생성합니다. 다섯째로 사용자 ID, 이메일, 역할로 JWT 토큰을 생성합니다. 여섯째로 생성된 토큰을 httpOnly Cookie로 저장하며, 프로덕션 환경에서는 secure 플래그도 활성화하고, 만료 시간은 Access Token 기본값(1일)로 설정합니다. 일곱째로 메인 화면으로 리다이렉트합니다.
349362

350363
---
351364

352-
## 로그인
353-
354-
**Backend:** POST /auth/logout 엔드포인트는 관리자인 경우 해당 사용자의 모든 관리자 세션을 삭제한 후, auth_token 쿠키를 삭제합니다.
365+
## 로그아웃
355366

356-
**Frontend:** 로그아웃 버튼을 클릭하면 백엔드의 로그아웃 엔드포인트에 요청을 보냅니다. 완료되면 sessionStorage의 관리자 세션도 삭제하고, 로그인 페이지로 이동합니다.
367+
**Backend:** POST /auth/logout 엔드포인트는 관리자인 경우 해당 사용자의 모든 관리자 세션을 삭제한 후, auth_token 쿠키와 admin_session 쿠키를 삭제합니다.
368+
369+
**Frontend:** 로그아웃 버튼을 클릭하면 백엔드의 로그아웃 엔드포인트에 요청을 보냅니다. 완료되면 로그인 페이지로 이동합니다.
357370

358371
---
359372

0 commit comments

Comments
 (0)