visionA/docs/autoflow/04-architecture/adr/adr-003-pairing-token.md
jim800121chen fb7da5d180 chore(autoflow): migrate .autoflow/ 共享層文件至 docs/autoflow/
依 autoflow-agent workspace v2 設計把 PRD / 設計 / 架構 / 交付類
共享文件從個人層 .autoflow/(ignored)搬到 docs/autoflow/(進 git),
讓團隊可共享產品與架構文件,個人層只留 progress / review / testing 等
per-branch 筆記。

- 02-prd/        21 個檔(PRD、features、market-analysis 等)
- 03-design/     18 個檔(design-spec、wireframes、flows 等)
- 04-architecture/ 31 個檔(TDD、design-doc、ADR×14、API 規格等)
- 07-delivery/   3 個檔(project-summary、phase-0.6-handover、stage-deployment-setup)

合計 73 檔。原檔已從 .autoflow/ 移除(migration 工具執行 git mv,
但因 .autoflow/ 在 .gitignore 中、git 將此操作視為新增、無 rename history)。
2026-05-04 16:55:55 +08:00

192 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ADR-003以 Pairing Token 取代 POC 的 SHA256(MAC) Token
## 狀態
Accepted — 2026-04-212026-04-22 修訂:明確兩階段 Token 流程 + 固定格式)
## 背景 (Context)
POC 的 tunnel 認證機制非常簡單:
- local agent 啟動時,計算 `SHA256(MAC address)[:16]`,得到 16 字元 hex 當 token
-`?token=xxx``X-Relay-Token` header 連線 relay
- relay 以 `map[token]*yamux.Session` 記憶體儲存,**無任何驗證**——任何人拿到 token 就能連上並註冊 session
這個設計在 POC 階段可接受,但不適合正式產品:
| POC 現況 | 問題 |
|---------|------|
| Token = MAC 衍生值,**永不變** | 洩漏後無法撤銷;換電腦 token 也跟著換(身分無法追蹤) |
| 無 expiry | 一次洩漏,永久曝險 |
| 無 ownership | Token 沒綁到「使用者」概念,無法實現「我的裝置列表」 |
| 無 revoke | 沒有「登出所有裝置」能力 |
| 無 rotation | 無法定期輪換降低風險 |
| Token == Identity | Token 本身即是身分,無法與 User Account 解耦 |
正式產品的雲端版需要:
- 綁定到「某個使用者帳號」
- 可撤銷
- 有 expiry過期要 re-pair
- 可同一使用者有多台裝置(每台一個獨立 token
- 可由使用者介面 list / revoke
## 決策 (Decision)
引入 **兩階段 Token** 機制:**Pairing Token短期一次性** → 換取 **Session Token長期可撤銷**。其生命週期如下:
### Token 格式統一規範2026-04-22 修訂 M-1
```
Pairing TokenvAc_ + 32 字元 hex (總長 36 字元Admin-Credential
Session TokenvAs_ + 64 字元 hex (總長 68 字元Agent-Session
```
- 字元集:`[0-9a-f]` 小寫 hex前綴 `vAc_` / `vAs_`
- 產生方式:`crypto/rand.Read(16 bytes)``hex.EncodeToString(...)`pairing`crypto/rand.Read(32 bytes)`session
- **API 回傳值永遠是純字串,無空格、無分隔符**
- UI 顯示層可視情況每 8 字元插入空格提升可讀性(例:`vAc_a1b2c3d4 e5f6g7h8 ...`
- 使用者貼上時前端正規化 `.replace(/\s/g, '')`
- 前綴讓 log / debug 一眼看出類型;正則驗證:`^vAc_[0-9a-f]{32}$` / `^vAs_[0-9a-f]{64}$`
### 兩階段流程Phase 1 完整;雛形簡化)
#### Stage 1 — Pairing Token短期一次性15 分鐘 TTL
1. 使用者在雲端 Web 登入(`visionA-frontend`
2. 進入「Devices → Pair New Device」→ 前端呼叫 `POST /api/pairing/token`
3. `api-server` 產生 pairing token`vAc_` + 32 hex
- 寫入 DB`token_hash = sha256(plain)`, `kind='pairing'`, `expires_at = now + 15min`, `used_at = NULL`
- 回傳 token 明文(**只在此次回應中出現一次**
4. 使用者把 token 貼到 local agent 的設定 UI / config / CLI flag
5. local agent 持有此 **Pairing Token**,準備建立第一次連線
#### Stage 2 — Session Token長期90 天 TTL可撤銷
6. local agent 首次用 Pairing Token 連 `WS /tunnel/connect?token=vAc_xxx`
7. `remote-proxy`(透過 `api-server` 內部呼叫)處理:
- 查 DBtoken 是否 `kind='pairing'` 且未撤銷、未過期、未使用
- 有效 → **原子升級**
- 產生 Session Token`vAs_` + 64 hex
- 寫入新 DB row`kind='session'`, `parent_token = <pairing token_hash>`, `device_id = <new or resolved>`, `expires_at = now + 90d`
- 將 Pairing Token 標記 `used_at = now()`(作廢;無法再次升級)
- **在 WS upgrade response header** 或 **首個 yamux control frame** 回傳 Session Token 明文
- 建立 yamux session`SessionStore.Register(session_token_hash, handle)`
- 無效 → 拒絕並回 HTTP 401
8. local agent 收到 Session Token 後**持久化儲存**(取代 Pairing Token之後重連一律用 Session Token
9. Session Token 過期、被撤銷 → local agent 連線被拒 → 使用者需在 Web UI 重新發 Pairing Token 走一次 Stage 1
#### 狀態轉換時序Phase 1
```
使用者 Web UI api-server local agent remote-proxy / DB
│ │ │ │
│─ Pair New Device ────►│ │ │
│ │─ INSERT pairing ──►│ │
│ │ kind='pairing' │ │
│ │ TTL 15 min │ │
│◄── vAc_xxx (once) ────│ │ │
│ │
│─ 複製貼到 agent ────────────────────────────►│ │
│ │─ WS /tunnel/connect ─►│
│ │ ?token=vAc_xxx │
│ │ │─ Validate(pairing)
│ │ │─ 產生 vAs_yyy
│ │ │─ INSERT session
│ │ │ kind='session'
│ │ │ TTL 90 day
│ │ │─ mark pairing used
│ │◄─ upgrade + vAs_yyy ──│
│ │─ 持久化 vAs_yyy │
│ │ │
│ │─ 日後重連always ──►│
│ │ ?token=vAs_yyy │
│ │◄── 直接接受 ──────────│
│ │ │
│─ Revoke device ──────►│─ UPDATE session ──────────────────────────►│
│ │ revoked_at = now │
│ │◄── 下次連線 401 ──────│
```
10. 使用者可在「Devices」頁面 list / revoke 任一 device 的 Session Token`UPDATE revoked_at`
### 雛形版(當前要做的)
雛形**不接 DB / 不接 Auth**,但**要把介面切好**,未來替換實作即可:
```go
// internal/auth/pairing.go
type PairingStore interface {
Validate(ctx, token) (*PairingInfo, error)
MarkUsed(ctx, token, deviceID) error
Create(ctx, userID) (string, *PairingInfo, error)
Revoke(ctx, token) error
List(ctx, userID) ([]*PairingInfo, error)
}
type PairingInfo struct {
TokenHash string
UserID string
DeviceID string
CreatedAt time.Time
ExpiresAt *time.Time
UsedAt *time.Time
RevokedAt *time.Time
}
```
**雛形實作 `StaticPairingStore`**
- 從 env var 讀一組寫死的 token + 一個假 user_id`VISIONA_PAIRING_TOKEN=abc123...``VISIONA_PAIRING_USER_ID=demo-user`
- `Validate` 簡單比對;`Create/Revoke/List``ErrNotImplemented`
- local agent 啟動 config 寫這個 token或 CLI flag `--pairing-token=xxx`
未來替換為 `DBPairingStore`其他程式碼relay session、API handler完全不改。
### 雛形 vs Phase 1 的單/雙階段處理2026-04-22 修訂 M-2
| 階段 | 做法 |
|------|------|
| **雛形Phase 0** | 為簡化開發,**以單一 Pairing Token 形式比對**,不做升級步驟。`StaticPairingStore` 讀取 `VISIONA_PAIRING_TOKEN` env格式仍為 `vAc_` + 32 hexagent 直接用它連 tunnel。**無 TTL、無升級、無撤銷**。這是刻意妥協以加速驗證。 |
| **Phase 1** | **完整兩階段**見「兩階段流程」Pairing Token 15 min TTL + Session Token 90 d TTL + 撤銷機制 |
**雛形限制須明確告知使用者**:雛形的單一 token 等同「永不過期的 pairing + session 合併體」,僅適合 dev 環境。Phase 1 前**必須**切到兩階段。
## 考慮過的替代方案
| 方案 | 優點 | 缺點 | 排除原因 |
|------|------|------|---------|
| **沿用 SHA256(MAC)** | POC 已驗證 | 不安全、不可撤銷、無 ownership | 無法做成產品 |
| **mTLS 憑證client certificate** | 業界標準、極安全 | 憑證管理複雜、使用者端安裝門檻高、Web 產生憑證 UX 差 | 使用者體驗門檻過高 |
| **OAuth Device Flow** | 標準化、適合 headless | 需外部 IdPAuth0 / Cognito、實作複雜 | Phase 2 可以評估,雛形過度複雜 |
| **短期 JWT + refresh token** | 標準做法 | JWT 無法撤銷除非建黑名單refresh token 又變另一個長期 token | 仍需處理 revoke不如直接用 opaque token |
| **使用者密碼 / API key** | 簡單 | 無法限定 scope、難以細粒度撤銷 | 粗糙 |
## 後果 (Consequences)
### 正面影響
- **抽象清晰**`PairingStore` interface 把 token 驗證與 tunnel session 管理解耦
- **雛形到 Phase 1 零程式碼修改**:只換 store 實作
- **未來支援多種 token 形態**:無論是 JWT、opaque token、還是 DB-backed session token都實作同一個 interface
### 負面影響(接受的取捨)
- **雛形安全性差**env 寫死 token任何能讀 env 的人都能冒充。可以接受,因為雛形只會在開發環境跑
- **沒有 rate limit**雛形不做Phase 1 要做 `/tunnel/connect` 的 IP / token rate limit
- **無 logging of failed attempts**:雛形只記 error logPhase 1 要記到 audit log
### 風險
- **Token 儲存明文 vs hash**:產品版要把 `token_hash = sha256(token)` 存 DB不存明文。雛形 env 暫用明文
- **Token 透過 URL query 傳遞會進 access log**POC 已這樣做雛形保留Phase 1 評估改用 `Authorization: Bearer xxx` headerWebSocket 自訂 header 瀏覽器有限制,但 local agent 可以)
- **如果使用者把 token 貼到 public repo**:產品版需支援「自動撤銷洩漏 token」secret scanning雛形不處理
## 合規性
- [x] Architect 確認
- [ ] Security ReviewPhase 1 前必做
- [ ] 加入 `.gitignore` 保證 token 不進 Git工程師實作時落實
## 相關文件
- Design Doc §6安全架構
- TDD §5Pairing Token 協定)
- TDD `security.md`
- 相關 ADRADR-002Tunnel Protocol、ADR-005雛形不接 DB/Auth