--- { "schema": "gitea-agent.requirement/v1", "id": "REQ-001", "title": "Dashboard 관리자 인증", "revision": "2.0", "status": "approved", "depends_on": [], "applies_to": ["dashboard"], "language": "ko" } --- # REQ-001 — Dashboard 관리자 인증 ## 1. 목적 Streamlit Dashboard의 모든 금융 데이터 조회·수정·import 기능 앞에 단일 관리자 인증 경계를 두고, 인증되지 않았거나 인증 설정이 유효하지 않은 요청을 fail-closed한다. 인증 성공 후에는 server-signed `Secure`/`HttpOnly` cookie로 같은 browser profile의 새 연결과 새 session에서 인증을 정해진 절대 만료까지 복원한다. ## 2. 배경 Revision 1.0은 Issue #1에서 단일 관리자 credential과 browser-session-only 인증 경계를 도입했다. Issue #6은 sidebar query-link 이동과 browser 재접속에서 반복 로그인이 발생하는 문제를 해결하기 위해 동일한 인증 기능의 session·logout·배포 계약을 persistent cookie 방식으로 확장한다. 새 기능 ID가 아니라 본 canonical 요구사항의 revision으로 관리한다. ## 3. 범위 ### 포함 - 모든 Dashboard 페이지 앞의 공통 관리자 인증과 인증 전 DB 차단 - 환경변수 기반 관리자 ID/password 및 독립적인 signing secret - constant-time credential 비교, 일반 오류, 실패당 최소 1초 지연 - 동일 Dashboard ASGI process의 server-side login/logout HTTP endpoint - server-signed `Secure`/`HttpOnly` cookie 발급·검증·삭제 - 기본 24시간, 선택 시 30일인 로그인 성공 시각 기준 절대 만료 - sidebar 페이지 이동, 새 탭, browser 종료 후 재접속과 새 Streamlit session 복원 - password/signing-secret/admin-ID rotation에 따른 기존 token 무효화 - credential·secret·token 비노출과 인증 event 관찰 가능성 - Compose, Kubernetes Secret, HTTPS와 운영 제약 문서화 ### 제외 - 다중 사용자, 회원가입, RBAC, 사용자별 데이터 분리 - OAuth/OIDC, SSO, 외부 IdP, MFA, CAPTCHA, 계정 잠금 - 외부 인증 proxy/gateway 또는 외부 session middleware로 인증 책임 이전 - sliding expiration 또는 복원 시 만료 재설정 - DB-backed token/session registry, 개별 원격 session 폐기 UI - MCP endpoint 인증과 MCP transport 변경 - SQLite 금융 schema와 데이터 모델 변경 - 다른 browser profile·기기 간 로그인 공유 ## 4. 용어와 기준 - **persistent auth token**: version, 관리자 subject, 발급 시각, 절대 만료, token ID만 포함하고 서버가 서명한 bearer token - **remember me**: 기본 미선택 checkbox이며 token TTL을 24시간에서 30일로 선택 - **rotation**: 관리자 password 또는 signing secret을 교체하여 기존 token 서명 검증을 실패시키는 운영 절차 - **canonical 근거**: Issue #1, Issue #6, source revisions `069d4c16fd84e871e8cd9dc8d9ae39b4fce4a6c6` 및 `7bea8b368e02e98afd4b2dc463fb270b0e6eb297` ## 5. 요구사항 ### REQ-001.01 — 공통 인증 경계 Dashboard는 DB path 취득, page dispatch, gateway 호출, upload, rollback 또는 DB-backed 렌더링보다 먼저 현재 설정과 signed cookie를 검증해야 한다. 기존 session boolean만으로 만료·rotation 검증을 우회해서는 안 된다. 실패 시 로그인 화면만 렌더링한다. ### REQ-001.02 — 설정과 fail-closed Credential source는 `JARCA_DASHBOARD_ADMIN_ID`, `JARCA_DASHBOARD_ADMIN_PASSWORD`뿐이며 signing source는 `JARCA_DASHBOARD_SIGNING_SECRET`뿐이다. 누락·빈 값·비문자열·read error 또는 UTF-8 32 byte 미만 signing secret은 일반 설정 오류로 fail-closed한다. Streamlit secrets나 fallback을 사용하지 않는다. ### REQ-001.03 — Credential 검증과 비노출 제출 ID와 password는 모두 `hmac.compare_digest`로 비교한다. Credential, signing secret, signing-key material, token, raw cookie와 `Set-Cookie` 값은 UI, traceback, URL/query, application log, 문서 또는 `st.session_state`에 저장하지 않는다. 비민감 correlation ID만 session state에 둘 수 있다. ### REQ-001.04 — 실패 로그인 응답 잘못된 ID 또는 password의 각 제출은 처리 시작부터 응답까지 최소 1초가 걸려야 한다. 두 실패는 동일한 오류를 표시하고 cookie·DB·gateway·금융 상태를 변경하지 않는다. 성공에는 실패 지연을 적용하지 않으며 application lock은 두지 않는다. ### REQ-001.05 — Persistent session과 절대 만료 정상 로그인도 24시간, 기본 미선택 `로그인 유지`를 선택한 로그인은 30일의 절대 만료를 사용한다. 발급 시 `iat`와 `exp`를 고정하고 page 이동, rerun, 활동, 새 tab, browser 재시작 또는 인증 복원으로 token이나 만료를 갱신하지 않는다. 같은 browser profile의 유효 cookie는 새 Streamlit session에서 인증을 복원한다. ### REQ-001.06 — Server-side adapter와 cookie Streamlit 1.58의 ASGI `App` custom route를 사용해 동일 Dashboard process의 실제 HTTP response에서 cookie를 발급·삭제한다. JavaScript/localStorage/client-side cookie component로 token을 저장하지 않는다. Cookie는 `__Host-` 이름, `Secure`, `HttpOnly`, `SameSite=Lax`, `Path=/`, no `Domain`이며 `Max-Age`는 token TTL과 같다. ### REQ-001.07 — Token 서명·claim·rotation Token payload는 `v`, 현재 관리자 ID `sub`, server `iat`, `exp`, 무작위 `jti`만 허용한다. Signing key는 독립 signing secret과 현재 관리자 password를 domain-separated HMAC-SHA256으로 결합해 파생하고 SHA-256 signer를 사용한다. Signature를 검증한 뒤 exact schema/type, subject, 허용 TTL, 미래 발급 시각과 server-side 만료를 검증한다. Password, signing secret 또는 관리자 ID 변경은 기존 token을 거부해야 한다. ### REQ-001.08 — Login/logout HTTP 계약 Login endpoint는 trusted ASGI request scheme과 Origin이 모두 same-origin HTTPS인 form POST만 받고 성공 시 hardened cookie와 root redirect를 반환한다. Logout endpoint도 같은 HTTPS 검증 후 동일 name/path/security 속성과 `Max-Age=0`으로 browser cookie를 삭제한다. Logout은 금융 DB를 변경하지 않는다. Stateless token 복사본은 개별 폐기하지 않으며 전체 강제 무효화는 password/signing-secret rotation으로 수행한다. ### REQ-001.09 — 보호 범위와 기존 기능 호환성 인증 후 기존 page 목록, `page` query parameter, sidebar 이동, gateway와 MCP 동작은 유지한다. 인증은 Dashboard만 보호하고 MCP endpoint 인증을 의미하지 않는다. 금융 SQLite schema/migration은 변경하지 않는다. ### REQ-001.10 — 배포·운영 제약 `.env.example`, Compose와 Kubernetes는 세 secret setting 이름만 정의하고 실제 값을 저장하지 않는다. 공개 browser origin은 HTTPS여야 한다. TLS proxy를 사용하는 배포는 실제 proxy source IP/CIDR만 Uvicorn forwarded-header allowlist에 넣고 Host/Origin을 보존하며 `X-Forwarded-Proto: https`를 설정한다. 기본 allowlist는 loopback으로 fail-closed하고 wildcard trust를 금지한다. 최소 1초 delay는 병렬 brute-force를 막지 않으므로 외부 노출 경계에 rate limiting이 필요하다. 모든 verifier replica는 동일 secret과 token 규칙을 사용해야 한다. ### REQ-001.11 — 관찰 가능성 Event log는 event name과 비민감 correlation ID만 기록한다. 허용 event에는 설정 오류, 인증 필요, token 수락, login 성공/실패/reject, logout이 포함된다. ID/password/secret/token/cookie/request body와 구체적인 검증 실패 원인은 기록하지 않는다. ## 6. 인터페이스 요약 - `POST /api/auth/login`: `admin_id`, `admin_password`, optional `remember_me=on` - `POST /api/auth/logout`: 입력 credential 없음 - Cookie: `__Host-jarca_dashboard_auth` - 설정: 세 `JARCA_DASHBOARD_*` environment variable - 상세 계약: `docs/interface-contract.md` ## 7. 오류 계약 | 조건 | 사용자 결과 | Cookie | DB/gateway/page | |---|---|---|---| | 설정 invalid | 일반 설정 오류 | 발급 없음 | 차단 | | credential invalid | 최소 1초 후 일반 인증 오류 | 발급 없음 | 차단 | | request malformed/cross-site | 400/403 | 발급 없음 | 차단 | | cookie 없음·malformed·tampered·expired | 로그인 화면 | 수락 없음 | 차단 | | subject/TTL/time/rotation 불일치 | 로그인 화면 | 수락 없음 | 차단 | | logout | root redirect | deletion | 금융 상태 무변경 | ## 8. 수용 기준 - [x] AC-001 인증 전 모든 DB/gateway/page 기능이 차단된다. - [x] AC-002 세 environment setting invalid 상태는 fail-closed한다. - [x] AC-003 ID/password를 constant-time 비교하고 일반 오류와 최소 1초 지연을 적용한다. - [x] AC-004 정상 login은 24시간 absolute token/cookie를 발급한다. - [x] AC-005 기본 미선택 remember-me login은 30일 absolute token/cookie를 발급한다. - [x] AC-006 Cookie는 `__Host-`, Secure, HttpOnly, SameSite=Lax, Path=/, no Domain이다. - [x] AC-007 일반 요청·page 이동·복원은 token을 재발급하거나 만료를 연장하지 않는다. - [x] AC-008 유효 cookie가 새 Streamlit session과 sidebar query page에서 인증을 복원한다. - [x] AC-009 malformed, oversized, tampered, expired, schema-invalid token은 DB 전에 거부된다. - [x] AC-010 password, signing secret 또는 관리자 ID rotation이 기존 token을 무효화한다. - [x] AC-011 logout은 browser cookie를 삭제하고 금융 상태를 변경하지 않는다. - [x] AC-012 credential, secret, token이 session state·URL·log·문서에 노출되지 않는다. - [x] AC-013 same-origin HTTPS POST가 아닌 login/logout 요청은 거부된다. - [x] AC-014 인증 후 기존 Dashboard page와 MCP behavior가 유지된다. - [x] AC-015 실제 loopback HTTPS와 Streamlit WebSocket에서 cookie 발급·복원·삭제를 검증한다. - [x] AC-016 금융 DB schema와 data model migration을 추가하지 않는다. - [x] AC-017 HTTPS, rate-limit, rotation과 stateless logout 제약이 운영 문서에 기록된다. ## 9. 검증 매핑 - Credential/delay/log: `tests/test_dashboard_authentication.py` - Token/TTL/tamper/rotation: `tests/test_dashboard_persistent_authentication.py` - HTTP/cookie/logout: `tests/test_dashboard_auth_server.py` - Streamlit gate/DB ordering/page 복원: `tests/test_dashboard_authentication_app.py`, `tests/test_dashboard_app.py` - 실제 HTTPS/WebSocket evidence: `scripts/persistent_auth_http_evidence.py`, `docs/evidence/issue-6-persistent-auth-https.json` - 전체 regression: `uv run --extra dashboard --extra dev pytest` - 정적 검사: `uv run --extra dashboard --extra dev ruff check dashboard tests scripts` ## 10. 추적성 - 최초 요구 Issue: `jsfamily/jarca-ledger-mcp#1` - 지속 로그인 변경 Issue: `jsfamily/jarca-ledger-mcp#6` - 최초 구현 PR: `#3` - Issue #6 구현 PR: 미정 - Issue #6 기준 revision: `7bea8b368e02e98afd4b2dc463fb270b0e6eb297` - Issue #6 승인 snapshot body SHA-256: `4f734169ec896747fe39fd7f3ab75d1fc0d8cdd4903045e665beba40d80eb1fe`