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
+84 -61
View File
@@ -1,89 +1,112 @@
# Runtime Flow — Dashboard Persistent Authentication
# Runtime Flow — Dashboard 관리자 인증
## 목적과 범위
Dashboard startup, login token 발급, 새 browser session의 인증 복원, 실패 차단과 logout cookie 삭제 순서를 정의한다.
ASGI startup부터 login, token restoration, invalid-cookie cleanup, explicit logout까지의 실행 순서와 상태 계약을 정의한다.
- 포함: ASGI startup, config, login/logout POST, signed cookie, Streamlit gate, DB 이전 차단
- 제외: MCP 요청, token registry, OAuth/OIDC, external auth middleware
- 포함 범위: config, HTTPS routes, cookie verification, Streamlit gate, cleanup/deletion
- 제외 범위: MCP request authentication, DB session/token registry, per-token revocation
- 시작 조건: trusted ASGI process receives request
- 성공 종료 상태: authenticated Dashboard page executes
- 실패 종료 상태: matching cookie deletion and unauthenticated login page
## 정상 흐름
## 흐름
### START — ASGI server 시작
### startup — ASGI startup
| 단계 | 주체 | 처리 | 다음 |
|---|---|---|---|
| `START-01` | Uvicorn | `dashboard.server:app` 로드 | `START-02` |
| `START-02` | Streamlit `App` | custom auth routes와 `dashboard/app.py` runtime 조립 | `UNAUTHENTICATED` |
- 시작 이벤트: process startup
- 선행조건: deployment injects configuration without recording values
- 실행 방식: synchronous startup/request handling
- 트랜잭션·영속성 경계: no auth persistence; existing financial SQLite is untouched
운영 command는 `uvicorn dashboard.server:app --host 0.0.0.0 --port 8501`이다.
| 단계 ID | 실행 주체 | 시작 조건·입력 | 처리 | 읽기·쓰기 상태 | 외부 호출 | 성공 시 다음 단계 | 실패 시 전이 |
|---|---|---|---|---|---|---|---|
| `startup-01` | Uvicorn | process command | load `dashboard.server:app` | process memory only | none | `startup-02` | `startup-fail` |
| `startup-02` | ASGI app | app loaded | mount auth routes and Streamlit runtime | route table only | none | `gate-01` | `startup-fail` |
### LOGIN — Credential 제출과 token 발급
### login — Credential login
| 단계 | 주체 | 입력/처리 | 상태·외부 효과 | 다음 |
|---|---|---|---|---|
| `LOGIN-01` | Browser | Streamlit이 렌더링한 HTML form을 same-origin HTTPS POST | credential은 request body에만 존재 | `LOGIN-02` |
| `LOGIN-02` | `auth_server` | Origin, content type, body size와 field cardinality 검증 | 금융 상태 없음 | `LOGIN-03` 또는 `LOGIN-REJECT` |
| `LOGIN-03` | `authentication` | 세 environment setting을 fail-closed로 읽음 | process memory | `LOGIN-04` 또는 `CONFIG-FAIL` |
| `LOGIN-04` | `authentication` | ID와 password 모두 constant-time 비교 | 실패 시 남은 최소 1초 sleep | `LOGIN-05` 또는 `LOGIN-FAIL` |
| `LOGIN-05` | `authentication` | `v/sub/iat/exp/jti` payload와 derived key로 token 서명 | DB 상태 없음 | `LOGIN-06` |
| `LOGIN-06` | `auth_server` | 303 response에 hardened cookie 설정 | browser cookie 생성 | `RESTORE-01` |
- 시작 이벤트: same-origin HTTPS form POST
- 선행조건: valid trusted scheme, origin and bounded body
- 실행 방식: synchronous request
- 트랜잭션·영속성 경계: no DB transaction; cookie is browser state
기본 `exp-iat`는 86400초, remember-me는 2592000초다. 이 값은 발급 시 고정된다.
| 단계 ID | 실행 주체 | 시작 조건·입력 | 처리 | 읽기·쓰기 상태 | 외부 호출 | 성공 시 다음 단계 | 실패 시 전이 |
|---|---|---|---|---|---|---|---|
| `login-01` | auth server | POST `/api/auth/login` | validate scheme/origin/body | request only | none | `login-02` | `login-reject` |
| `login-02` | authentication | validated form | load three settings, fail closed | server memory | secret provider/environment | `login-03` | `config-fail` |
| `login-03` | authentication | configured settings | constant-time compare ID/password | no persistent write | none | `login-04` | `login-fail` |
| `login-04` | authentication | valid credential | issue signed claims with `sub=dashboard-admin` | server memory | none | `login-05` | `login-fail` |
| `login-05` | auth server | signed token | set hardened cookie and redirect | browser cookie write | response | `gate-01` | `login-fail` |
### RESTORE — 새 session·재접속 인증 복원
### restore — Existing cookie restoration
| 단계 | 주체 | 입력/처리 | 상태 | 다음 |
|---|---|---|---|---|
| `RESTORE-01` | Browser | Streamlit initial request/WebSocket handshake에 host cookie 전송 | HttpOnly token | `GATE-01` |
| `GATE-01` | `dashboard.app` | page config 직후 authentication gate 호출 | DB 미접근 | `GATE-02` |
| `GATE-02` | `authentication` | 현재 ID/password/signing secret 로드 | secret은 session에 저장하지 않음 | `GATE-03` 또는 `CONFIG-FAIL` |
| `GATE-03` | `authentication` | signature 검증 후 exact claim schema, subject, TTL, time 검증 | token 재발급 없음 | `DASHBOARD-01` 또는 `TOKEN-FAIL` |
| `DASHBOARD-01` | `dashboard.app` | DB path 해석과 기존 page dispatch | 기존 금융 계약 | `AUTHENTICATED` |
- 시작 이벤트: new request/session with browser cookie
- 선행조건: cookie present or absent; current configuration available
- 실행 방식: synchronous gate
- 트랜잭션·영속성 경계: no server-side session; financial DB begins only after gate success
각 rerun은 cookie를 현재 설정으로 다시 검증한다. 일반 요청은 `iat`, `exp`, cookie `Max-Age`를 연장하지 않는다.
| 단계 ID | 실행 주체 | 시작 조건·입력 | 처리 | 읽기·쓰기 상태 | 외부 호출 | 성공 시 다음 단계 | 실패 시 전이 |
|---|---|---|---|---|---|---|---|
| `gate-01` | dashboard app | page startup | call authentication before DB path/page dispatch | no DB access | none | `gate-02` | `unauthenticated` |
| `gate-02` | authentication | current config and cookie | verify signature, exact claims, fixed role subject, TTL and rotation | no session write | none | `dashboard-01` | `cleanup-01` |
| `dashboard-01` | dashboard app | accepted token | resolve DB path and dispatch requested page | existing SQLite reads/writes | gateway/page code | `authenticated` | `dashboard-fail` |
### LOGOUT — Browser cookie 삭제
### cleanup — Invalid cookie cleanup
| 단계 | 주체 | 처리 | 다음 |
|---|---|---|---|
| `LOGOUT-01` | Browser | same-origin HTTPS POST `/api/auth/logout` | `LOGOUT-02` |
| `LOGOUT-02` | `auth_server` | matching name/path/security 속성과 `Max-Age=0` response | `LOGOUT-03` |
| `LOGOUT-03` | Browser | cookie 삭제 후 `/` 재접속 | `UNAUTHENTICATED` |
- 시작 이벤트: gate rejects malformed, expired, tampered, claim-invalid, or config-failure cookie
- 선행조건: same-origin HTTPS cleanup request
- 실행 방식: synchronous request
- 트랜잭션·영속성 경계: no DB or token registry write
Logout은 DB를 변경하지 않는다. 복사된 stateless token의 개별 server-side revocation은 없으며 password/signing-secret rotation이 전체 기존 token을 무효화한다.
| 단계 ID | 실행 주체 | 시작 조건·입력 | 처리 | 읽기·쓰기 상태 | 외부 호출 | 성공 시 다음 단계 | 실패 시 전이 |
|---|---|---|---|---|---|---|---|
| `cleanup-01` | browser/auth gate | invalid cookie | navigate to `GET /api/auth/cleanup` | request only | auth route | `cleanup-02` | `cleanup-reject` |
| `cleanup-02` | auth server | same-origin HTTPS `Referer` | emit matching deletion (`Max-Age=0`) and root redirect | browser cookie deletion | response | `unauthenticated` | `cleanup-reject` |
### logout — Explicit logout
- 시작 이벤트: user submits logout form
- 선행조건: same-origin HTTPS POST
- 실행 방식: synchronous request
- 트랜잭션·영속성 경계: no financial DB change and no per-token revocation
| 단계 ID | 실행 주체 | 시작 조건·입력 | 처리 | 읽기·쓰기 상태 | 외부 호출 | 성공 시 다음 단계 | 실패 시 전이 |
|---|---|---|---|---|---|---|---|
| `logout-01` | browser | POST `/api/auth/logout` | validate same-origin HTTPS | request only | none | `logout-02` | `logout-reject` |
| `logout-02` | auth server | validated request | matching deletion and root redirect | browser cookie deletion | response | `unauthenticated` | `logout-reject` |
## 실패와 복구
| 실패 상태 | 원인 | 차단 범위 | 복구 |
|---|---|---|---|
| `CONFIG-FAIL` | ID/password/signing secret 누락·invalid·read error | DB/gateway/page 전체 | 올바른 secret 주입 후 rerun |
| `LOGIN-REJECT` | cross-site, malformed, oversized POST | cookie 발급 | same-origin 정상 form 재시도 |
| `LOGIN-FAIL` | ID 또는 password 불일치 | cookie 발급·DB 접근 | 최소 1초 후 일반 오류, 재시도 |
| `TOKEN-FAIL` | absent/malformed/tampered/expired/wrong claims/rotation | DB/gateway/page 전체 | 재로그인 |
| `DASHBOARD-FAIL` | 인증 이후 기존 page/DB 오류 | 기존 page 계약 | 기존 운영 복구 |
| 대상 단계 | 실패 조건 | 추가 쓰기 중단 | 재시도 계약 | 중복 방지 | 보상·수동 복구 | 최종 상태 |
|---|---|---|---|---|---|---|
| `login-02` | missing/empty/read-error/short secret | yes | fix deployment config | no cookie | operator injects secret | `config-fail` |
| `login-03` | wrong ID/password | yes | retry after generic error and 1s delay | no lock | external rate limit | `login-fail` |
| `gate-02` | invalid/expired/tampered/claim/rotation failure | yes | cleanup then login | matching deletion | user logs in again | `unauthenticated` |
| `cleanup-02` | non-HTTPS/cross-origin | yes | retry same origin | deletion is idempotent | browser clears manually if needed | `cleanup-reject` |
| `logout-02` | non-HTTPS/cross-origin | yes | retry same origin | deletion is idempotent | browser clears manually if needed | `logout-reject` |
Invalid token의 세부 원인을 UI에서 구분하지 않는다. Credential, secret, token, cookie header 또는 exception 원문을 log에 넣지 않는다.
### 동시성과 순서
## 동시성·순서
- 서로 다른 browser session은 같은 signed token을 보낼 수 있다.
- Token verification은 stateless이고 shared mutable authentication state를 만들지 않는다.
- 설정 및 cookie 검증은 항상 DB path/gateway/page보다 먼저다.
- Password/signing-secret rotation 직후 다음 rerun부터 기존 token이 실패한다.
- 실패당 1초 delay는 병렬 요청을 직렬화하지 않으므로 TLS ingress/reverse-proxy 경계에서 rate limiting이 필요하다.
- 동시 실행 허용 여부: independent requests allowed; no application-wide auth lock
- 잠금·직렬화 단위: none for authentication; existing SQLite rules remain for financial writes
- 순서 보장: config/cookie gate precedes DB path, gateway and page dispatch
- 충돌 검출·처리: token is stateless; password/signing-secret/admin-ID rotation invalidates old claims
## 관찰 가능성
허용 event: `configuration_error`, `authentication_required`, `token_accepted`, `login_succeeded`, `login_failed`, `login_origin_rejected`, `login_request_rejected`, `logout`, `logout_origin_rejected`.
각 log는 event와 비민감 correlation ID만 포함한다. Token, credential, origin 세부값, request body와 cookie header는 기록하지 않는다.
| 신호 종류 | 이름 | 발생 단계·조건 | 상관 식별자 | 진단 속성 | 민감정보 처리 |
|---|---|---|---|---|---|
| event log | `authentication_required` | `gate-01` rejects | non-sensitive correlation ID | event class only | no cookie/token/reason |
| event log | `login_succeeded` | `login-05` | non-sensitive correlation ID | TTL class only | no credential/token |
| event log | `login_failed` | `login-03` | non-sensitive correlation ID | generic failure class | no ID/password |
| event log | `cleanup` | `cleanup-02` | non-sensitive correlation ID | generic cleanup class | no cookie header |
| event log | `logout` | `logout-02` | non-sensitive correlation ID | operation result | no token/cookie value |
## 검증
- Normal/remember TTL 및 non-sliding: token unit tests
- Tamper/expiry/rotation/schema rejection: token unit tests
- HTTP response cookie와 logout deletion: auth-server tests
- Fail-closed DB-before-gate ordering: AppTest
- 실제 loopback HTTPS login/reconnect/logout와 cookie 속성: HTTP evidence script
- MCP behavior와 DB schema가 변경되지 않았음을 full regression과 diff로 확인
- 정상 흐름: auth server tests plus loopback HTTPS login and fresh session restoration
- 실패 흐름: configuration, wrong credential, malformed/tampered/expired cookie and origin rejection tests
- 경계 흐름: TTL boundary, rotation, oversized token, same-host different-port origin
- 동시성 흐름: independent failed submissions and no application lock; ingress rate-limit configuration review
- 운영 진입점과 기대 결과: `uvicorn dashboard.server:app --host 0.0.0.0 --port 8501`; valid HTTPS reaches login or authenticated page, invalid cookie receives matching deletion before unauthenticated page