fix(dashboard): address persistent auth review

This commit is contained in:
2026-10-02 03:02:23 +00:00
parent 91e106d75c
commit 7bb959859a
11 changed files with 885 additions and 347 deletions
@@ -15,145 +15,432 @@
## 1. 목적
Streamlit Dashboard의 모든 금융 데이터 조회·수정·import 기능 앞에 단일 관리자 인증 경계를 두고, 인증되지 않았거나 인증 설정이 유효하지 않은 요청을 fail-closed한다. 인증 성공 후에는 server-signed `Secure`/`HttpOnly` cookie로 같은 browser profile의 새 연결과 새 session에서 인증을 정해진 절대 만료까지 복원한다.
Dashboard의 금융 기능 앞에 단일 fail-closed 관리자 인증 경계를 두고, 유효한 인증 cookie가 같은 browser profile의 새 연결에서 복원되도록 한다.
## 2. 배경
Revision 1.0은 Issue #1에서 단일 관리자 credential과 browser-session-only 인증 경계를 도입했다. Issue #6은 sidebar query-link 이동과 browser 재접속에서 반복 로그인이 발생하는 문제를 해결하기 위해 동일한 인증 기능의 session·logout·배포 계약을 persistent cookie 방식으로 확장한다. 새 기능 ID가 아니라 본 canonical 요구사항의 revision으로 관리한다.
Issue #1의 관리자 인증을 Issue #6에서 persistent signed cookie와 명시적 logout으로 확장한다. 기존 요구사항의 정체성, 승인 범위, Issue #1과 구현 PR #3의 추적성을 유지하며 revision 2.0으로 갱신한다.
## 3. 범위
### 포함
- 모든 Dashboard 페이지 앞의 공통 관리자 인증과 인증 전 DB 차단
- 환경변수 기반 관리자 ID/password 및 독립적인 signing secret
- Dashboard 전 기능 앞의 인증 gate와 인증 전 DB 차단
- `JARCA_DASHBOARD_ADMIN_ID`, `JARCA_DASHBOARD_ADMIN_PASSWORD`, `JARCA_DASHBOARD_SIGNING_SECRET` 기반 server-only 검증
- 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와 운영 제약 문서화
- same-origin HTTPS login, cleanup, logout route
- `Secure`/`HttpOnly`/`SameSite=Lax`/`Path=/` signed cookie와 24시간·30일 absolute TTL
- 고정된 비민감 role subject와 설정 credential에 묶인 서명 검증
- invalid, expired, tampered, malformed, configuration-failure cookie의 cleanup 및 matching deletion
- password, signing-secret, admin-ID 변경에 따른 기존 token 거부
- HTTPS, proxy, Kubernetes/Compose 운영 제약과 민감정보 비노출
### 제외
- 다중 사용자, 회원가입, 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와 데이터 모델 변경
- 다중 사용자, RBAC, OAuth/OIDC, SSO, MFA, CAPTCHA, 계정 잠금
- 외부 인증 proxy로 책임 이전
- sliding expiration, DB-backed session/token registry, per-token revocation
- MCP endpoint 인증 변경, SQLite schema/data migration
- 다른 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`
- **role subject**: token의 `sub`에 고정 기록하는 비민감 값 `dashboard-admin`; 관리자 ID나 credential이 아니다.
- **server-only binding**: signing secret과 현재 설정 credential을 서버에서만 읽어 key를 파생하며 browser에 값을 보내지 않는 방식.
- **cleanup**: 인증 실패 cookie를 같은 origin의 cleanup route에서 matching name/path/security 속성으로 삭제하는 절차.
- **canonical 근거**: Issue #1, Issue #6, 구현 PR #3 및 #7.
## 5. 요구사항
### REQ-001.01 — 공통 인증 경계
Dashboard는 DB path 취득, page dispatch, gateway 호출, upload, rollback 또는 DB-backed 렌더링보다 먼저 현재 설정과 signed cookie를 검증해야 한다. 기존 session boolean만으로 만료·rotation 검증을 우회해서는 안 된다. 실패 시 로그인 화면만 렌더링한다.
모든 DB path, page dispatch, gateway, upload, import보다 먼저 설정과 cookie를 검증하고, 실패하면 로그인 화면만 표시한다.
### 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을 사용하지 않는다.
- 인터페이스: `dashboard-auth-gate`
- 영속성: 없음
- 후속 사용 주체: Dashboard page와 gateway
- 마이그레이션: 없음
- 호환성: 인증 후 기존 page/query 동작 유지
- 배포 전제 조건: 유효한 세 환경변수
### REQ-001.03 — Credential 검증과 비노출
### REQ-001.02 — 설정과 server-only binding
제출 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에 둘 수 있다.
세 환경변수만 사용하고 누락·빈 값·read error·짧은 signing secret이면 fail-closed한다. signing key는 서버에서 configured credential과 secret으로만 파생한다.
### REQ-001.04 — 실패 로그인 응답
**영향 범위**
잘못된 ID 또는 password의 각 제출은 처리 시작부터 응답까지 최소 1초가 걸려야 한다. 두 실패는 동일한 오류를 표시하고 cookie·DB·gateway·금융 상태를 변경하지 않는다. 성공에는 실패 지연을 적용하지 않으며 application lock은 두지 않는다.
- 인터페이스: `dashboard-auth-config`, `dashboard-auth-cookie`
- 영속성: 없음
- 후속 사용 주체: 인증 verifier
- 마이그레이션: 없음
- 호환성: Streamlit secrets/fallback 미사용
- 배포 전제 조건: secret manager 주입
### REQ-001.05 — Persistent session과 절대 만료
### REQ-001.03 — Credential과 민감정보 비노출
정상 로그인도 24시간, 기본 미선택 `로그인 유지`를 선택한 로그인은 30일의 절대 만료를 사용한다. 발급 시 `iat`와 `exp`를 고정하고 page 이동, rerun, 활동, 새 tab, browser 재시작 또는 인증 복원으로 token이나 만료를 갱신하지 않는다. 같은 browser profile의 유효 cookie는 새 Streamlit session에서 인증을 복원한다.
ID와 password는 constant-time 비교한다. credential, secret, signing material, token, raw cookie와 Set-Cookie 값은 UI, URL, log, 문서, session state에 기록하지 않는다.
### 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과 같다.
- 인터페이스: login form, event log
- 영속성: 없음
- 후속 사용 주체: 운영자와 감사 도구
- 마이그레이션: 없음
- 호환성: 비민감 correlation ID만 유지
- 배포 전제 조건: redacted logging
### REQ-001.07 — Token 서명·claim·rotation
### REQ-001.04 — Login 실패 계약
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을 거부해야 한다.
ID 또는 password가 틀리면 동일한 일반 오류와 최소 1초 지연을 적용하고 상태를 변경하지 않는다. 성공에는 지연을 적용하지 않는다.
### 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으로 수행한다.
- 인터페이스: `POST /api/auth/login`
- 영속성: 없음
- 후속 사용 주체: browser
- 마이그레이션: 없음
- 호환성: 기존 login form 유지
- 배포 전제 조건: 외부 rate limiting
### REQ-001.09 — 보호 범위와 기존 기능 호환성
### REQ-001.05 — Persistent token
인증 후 기존 page 목록, `page` query parameter, sidebar 이동, gateway와 MCP 동작은 유지한다. 인증은 Dashboard만 보호하고 MCP endpoint 인증을 의미하지 않는다. 금융 SQLite schema/migration은 변경하지 않는다.
성공 login은 기본 24시간, remember-me 선택 시 30일의 absolute TTL token을 발급한다. `iat`와 `exp`는 고정하며 활동·rerun·복원으로 연장하지 않는다. `sub`는 항상 `dashboard-admin`이다.
### 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 규칙을 사용해야 한다.
- 인터페이스: `dashboard-auth-cookie`
- 영속성: browser cookie만
- 후속 사용 주체: 새 Streamlit session
- 마이그레이션: 기존 session boolean에서 cookie로 전환
- 호환성: 같은 browser profile 복원
- 배포 전제 조건: HTTPS
### REQ-001.06 — Cookie 보안 속성
Cookie 이름은 `__Host-jarca_dashboard_auth`이며 `Secure`, `HttpOnly`, `SameSite=Lax`, `Path=/`, no `Domain`을 사용한다. 일반 요청은 재발급하지 않는다.
**영향 범위**
- 인터페이스: HTTP response와 browser
- 영속성: browser cookie
- 후속 사용 주체: browser/Streamlit handshake
- 마이그레이션: 없음
- 호환성: host-only cookie
- 배포 전제 조건: 공개 origin HTTPS
### REQ-001.07 — Invalid cookie cleanup
없음·malformed·oversized·tampered·expired·claim-invalid·rotation-invalid 또는 configuration-failure cookie는 보호 자원에 접근하기 전에 거부한다. 같은 origin cleanup route를 통해 동일 name/path/security 속성과 `Max-Age=0` deletion을 반환한 뒤 unauthenticated page를 표시한다.
**영향 범위**
- 인터페이스: `GET /api/auth/cleanup`, `dashboard-auth-gate`
- 영속성: 서버 session/registry 없음
- 후속 사용 주체: browser
- 마이그레이션: 없음
- 호환성: stale cookie가 남지 않음
- 배포 전제 조건: same-origin HTTPS
### REQ-001.08 — Login, cleanup, logout
Login과 logout은 trusted same-origin HTTPS POST만 허용하고 cleanup GET은 same-origin HTTPS `Referer`를 검증한다. logout은 명시적으로 matching deletion을 반환한다. cleanup과 logout은 금융 DB를 변경하지 않는다.
**영향 범위**
- 인터페이스: `POST /api/auth/login`, `GET /api/auth/cleanup`, `POST /api/auth/logout`
- 영속성: 없음
- 후속 사용 주체: browser
- 마이그레이션: 없음
- 호환성: root redirect 유지
- 배포 전제 조건: proxy가 Host/Origin/Proto 보존
### REQ-001.09 — Rotation과 무상태성
Token claim은 `v`, 고정 `sub`, `iat`, `exp`, `jti`만 허용한다. password, signing secret 또는 admin ID 변경은 기존 token을 거부한다. per-token revocation이나 DB session은 만들지 않는다.
**영향 범위**
- 인터페이스: token verifier
- 영속성: revocation/session 저장소 없음
- 후속 사용 주체: 운영 rotation 절차
- 마이그레이션: 없음
- 호환성: 전체 무효화는 rotation으로 수행
- 배포 전제 조건: 모든 replica 동일 설정
### REQ-001.10 — 보호 범위와 운영
인증 후 기존 page, query parameter, gateway와 MCP 동작을 유지한다. Compose/Kubernetes는 실제 secret을 저장하지 않으며 HTTPS, forwarded-header allowlist와 rate limit을 적용한다.
**영향 범위**
- 인터페이스: Dashboard 운영 진입점
- 영속성: 기존 SQLite만
- 후속 사용 주체: 운영 배포
- 마이그레이션: DB migration 없음
- 호환성: MCP endpoint는 별도 계약
- 배포 전제 조건: TLS와 제한된 proxy CIDR
### REQ-001.11 — 관찰 가능성
Event log는 event name과 비민감 correlation ID만 기록한다. 허용 event에는 설정 오류, 인증 필요, token 수락, login 성공/실패/reject, logout이 포함된다. ID/password/secret/token/cookie/request body와 구체적인 검증 실패 원인은 기록하지 않는다.
설정 오류, 인증 필요, token 수락, login 성공/실패/reject, cleanup, logout event와 비민감 correlation ID만 기록한다. 민감 값과 구체적 검증 원인은 기록하지 않는다.
## 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`
- 인터페이스: event log
- 영속성: 로그에 비민감 메타데이터만
- 후속 사용 주체: 운영 관찰 도구
- 마이그레이션: 없음
- 호환성: 일반 오류 메시지 유지
- 배포 전제 조건: redaction 정책
## 7. 오류 계약
## 6. 제약과 불변조건
| 조건 | 사용자 결과 | 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 | 금융 상태 무변경 |
- `sub`는 비민감 고정 role 값이며 credential에서 파생하거나 관리자 ID를 담지 않는다.
- credential, secret, token, cookie 값은 저장·출력·문서화하지 않는다.
- 인증 성공 전 DB/gateway/page dispatch는 실행하지 않는다.
- 만료는 absolute이며 활동으로 연장하지 않는다.
- invalid cookie cleanup과 explicit logout의 deletion은 발급 cookie와 matching scope를 사용한다.
- 서버는 per-token revocation과 DB-backed session을 유지하지 않는다.
## 8. 수용 기준
## 7. 인수 기준
- [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 제약이 운영 문서에 기록된다.
### REQ-001.01-AC01 — 인증 전 차단
## 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`
모든 인증 거부 경로에서 DB, gateway, page dispatch가 실행되지 않는다.
## 10. 추적성
**검증**
- 최초 요구 Issue: `jsfamily/jarca-ledger-mcp#1`
- 지속 로그인 변경 Issue: `jsfamily/jarca-ledger-mcp#6`
- 최초 구현 PR: `#3`
- Issue #6 구현 PR: `#7`
- Issue #6 기준 revision: `7bea8b368e02e98afd4b2dc463fb270b0e6eb297`
- Issue #6 승인 snapshot body SHA-256: `4f734169ec896747fe39fd7f3ab75d1fc0d8cdd4903045e665beba40d80eb1fe`
AppTest와 gate ordering 테스트에서 database access sentinel이 호출되지 않음을 확인한다.
**검증 분류**
- 수준: integration
- 백엔드: Streamlit AppTest
- 케이스: missing config, no cookie, invalid cookie
**상태 불변조건**
거부 시 금융 상태와 DB는 변경되지 않는다.
### REQ-001.02-AC01 — 설정 fail-closed
**판정 기준**
세 설정 중 하나라도 유효하지 않으면 일반 설정 오류로 인증을 거부한다.
**검증**
설정별 누락·빈 값·짧은 secret 테스트를 실행한다.
**검증 분류**
- 수준: unit/integration
- 백엔드: pytest
- 케이스: 각 환경변수 누락, 빈 값, short secret
**상태 불변조건**
cookie 발급과 보호 자원 접근이 없다.
### REQ-001.03-AC01 — 민감정보 비노출
**판정 기준**
credential, secret, token, cookie가 UI·URL·log·session state에 나타나지 않는다.
**검증**
로그·응답·session state와 문서를 redaction 검색한다.
**검증 분류**
- 수준: static/integration
- 백엔드: pytest, ruff, repository search
- 케이스: 성공·실패 login과 invalid cookie
**상태 불변조건**
비민감 correlation ID 외 인증 비밀 상태가 없다.
### REQ-001.04-AC01 — 실패 지연과 일반 오류
**판정 기준**
잘못된 ID와 잘못된 password가 같은 일반 오류를 반환하고 각각 최소 1초 지연된다.
**검증**
두 실패 경로의 elapsed time과 response를 측정한다.
**검증 분류**
- 수준: integration
- 백엔드: pytest/HTTP client
- 케이스: wrong ID, wrong password
**상태 불변조건**
cookie·DB·gateway 상태가 변하지 않는다.
### REQ-001.05-AC01 — Absolute TTL과 고정 subject
**판정 기준**
기본 24시간과 remember-me 30일 token의 `iat`, `exp`, `sub=dashboard-admin`이 고정된다.
**검증**
token claim, TTL, rerun/non-sliding 테스트를 실행한다.
**검증 분류**
- 수준: unit/integration
- 백엔드: pytest
- 케이스: normal, remember-me, activity, reconnect
**상태 불변조건**
복원은 token을 갱신하지 않는다.
### REQ-001.06-AC01 — Hardened cookie
**판정 기준**
발급 cookie가 이름·Secure·HttpOnly·SameSite·Path·Domain 계약을 모두 만족한다.
**검증**
실제 HTTP `Set-Cookie`를 파싱하고 속성을 검사한다.
**검증 분류**
- 수준: integration
- 백엔드: Starlette TestClient와 loopback HTTPS
- 케이스: login normal/remember
**상태 불변조건**
Domain attribute가 없고 평문 전송에서 발급되지 않는다.
### REQ-001.07-AC01 — Invalid cookie cleanup
**판정 기준**
invalid/expired/tampered/config-failure cookie는 보호 자원 전에 거부되고 same-origin cleanup route의 matching deletion 후 unauthenticated page가 표시된다.
**검증**
실제 browser/HTTP 흐름에서 각 invalid cookie와 cleanup response를 확인한다.
**검증 분류**
- 수준: integration/browser
- 백엔드: HTTPS probe와 Streamlit browser evidence
- 케이스: malformed, expired, tampered, wrong claims, config failure
**상태 불변조건**
cleanup은 DB를 변경하지 않으며 stale cookie가 남지 않는다.
### REQ-001.08-AC01 — Explicit logout
**판정 기준**
logout POST가 matching `Max-Age=0` deletion을 반환하고 다음 접근이 unauthenticated다.
**검증**
login 후 logout response와 fresh request를 확인한다.
**검증 분류**
- 수준: integration/browser
- 백엔드: HTTPS probe
- 케이스: same-origin logout, cross-origin rejection
**상태 불변조건**
금융 DB와 token registry는 변경되지 않는다.
### REQ-001.09-AC01 — Rotation과 무상태성
**판정 기준**
configured credential 변경은 기존 token을 거부하며 per-token revocation/DB session 구현이 없다.
**검증**
rotation 테스트와 source/schema/diff 검색을 실행한다.
**검증 분류**
- 수준: unit/static
- 백엔드: pytest와 repository search
- 케이스: password, signing secret, admin ID rotation
**상태 불변조건**
검증은 현재 설정과 서명만 사용한다.
### REQ-001.10-AC01 — 운영 제약
**판정 기준**
README와 Kubernetes 문서에 HTTPS, proxy allowlist, rate limit, secret 주입, rotation과 stateless logout이 기록된다.
**검증**
문서 검색과 deployment manifest 검토를 실행한다.
**검증 분류**
- 수준: documentation/static
- 백엔드: repository search
- 케이스: README, deploy/k8s/README.md
**상태 불변조건**
실제 credential·secret 값이 문서와 manifest에 없다.
### REQ-001.11-AC01 — 안전한 관찰성
**판정 기준**
허용 event와 correlation ID만 기록되고 민감 값·구체적 검증 원인은 기록되지 않는다.
**검증**
로그 테스트와 redaction 검색을 실행한다.
**검증 분류**
- 수준: unit/static
- 백엔드: pytest와 repository search
- 케이스: config, login, cleanup, logout success/failure
**상태 불변조건**
로그에서 credential·secret·token·cookie가 복원되지 않는다.
## 8. 오류 및 충돌 계약
동일 요청에서 설정 오류가 credential 오류보다 우선하며, 모든 인증 실패는 보호 자원 전에 종료된다. invalid cookie는 cleanup deletion을 시도하고, cleanup route가 거부되거나 실패해도 보호 자원은 열지 않는다. cross-origin/plain HTTP 요청은 403, malformed request는 400이다. 동시 login은 application lock 없이 독립 처리한다.
## 9. 데이터 및 인터페이스 계약
Token은 `v`, `sub=dashboard-admin`, `iat`, `exp`, `jti`와 서명만 가지며 browser cookie에만 저장된다. 세 환경변수는 server process에서만 읽는다. 상세 HTTP, cookie, cleanup, logout 계약은 `docs/interface-contract.md`에 정의한다.
## 10. 하위 호환성
Issue #1의 Dashboard-only 범위, 기존 page/query/gateway/MCP 동작과 구현 PR #3의 추적성을 유지한다. session-only 인증은 persistent cookie로 대체하며 SQLite schema와 MCP transport는 변경하지 않는다.
## 11. 추적성
- 근거 이슈: `#1`, `#6`
- 구현 PR: `#3`, `#7`
- 관련 설계 문서: `docs/interface-contract.md`, `docs/project-structure.md`, `docs/runtime-flow.md`, `README.md`, `deploy/k8s/README.md`
- 관련 요구사항: 없음