從 edge-ai-platform POC 轉為正式產品的雲端後端,含以下整合階段:
- Phase 0:雛形骨架 — `cmd/api-server` (REST :3721) + `cmd/remote-proxy`
(tunnel :3800 / internal :3801) 雙 binary 共用 internal/,沿用 POC 的
WebSocket+yamux tunnel 協定但解耦 relay 與 API
- Phase 0.6:OIDC BFF 接 Innovedus Member Center
- internal/oidc package(coreos/go-oidc + PKCE S256 + state + nonce)
- internal/usersession package(HMAC-SHA256 cookie + RotateSessionID
防 session fixation, OWASP ASVS V3.2.1)
- 4 個 OIDC handler(/api/auth/login|callback|me|logout)+ AuthMiddleware
- 完全拔除 StaticAuthProvider,OIDC 是唯一認證路徑
- 9 個 ADR(含 ADR-010 BFF / ADR-011 取代 static auth /
ADR-012 pending session shared cookie / ADR-013 PKCE-only public client)
- Phase 0.7:A1 改造 + security audit 修復
- OIDC ClientSecret 變選填,支援 stage MC 的 public PKCE-only client
(AuthStyleInParams 強制 token endpoint 不送 client_secret)
- 預留 ServiceClient* 欄位給未來 client_credentials grant
- 移除 13+ 處 resolveUserID(uc, StaticUserID) fallback 改 strict mode
(Audit C1:multi-tenant 隔離破口)
- Pairing exchange MarkUsed 失敗 abort + revoke session token(Audit M3)
- 新增 all_endpoints_require_auth_test 整合測試(51 endpoint × 401)
驗證:go test -race -count=3 ./... 17 packages 全綠 / go vet 0 warning
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
74 lines
2.3 KiB
Go
74 lines
2.3 KiB
Go
package auth
|
||
|
||
import (
|
||
"testing"
|
||
|
||
"github.com/stretchr/testify/assert"
|
||
"github.com/stretchr/testify/require"
|
||
)
|
||
|
||
func TestGeneratePairingToken_Format(t *testing.T) {
|
||
tok, err := GeneratePairingToken()
|
||
require.NoError(t, err)
|
||
|
||
assert.True(t, IsValidPairingToken(tok), "產生的 token 應符合 pairing 正則:got %q", tok)
|
||
assert.Len(t, tok, len(PairingTokenPrefix)+PairingTokenHexLen)
|
||
}
|
||
|
||
func TestGenerateSessionToken_Format(t *testing.T) {
|
||
tok, err := GenerateSessionToken()
|
||
require.NoError(t, err)
|
||
|
||
assert.True(t, IsValidSessionToken(tok), "產生的 token 應符合 session 正則:got %q", tok)
|
||
assert.Len(t, tok, len(SessionTokenPrefix)+SessionTokenHexLen)
|
||
}
|
||
|
||
func TestGeneratePairingToken_Unique(t *testing.T) {
|
||
// 產生 100 次應不會碰撞(熵極低)。
|
||
seen := make(map[string]struct{}, 100)
|
||
for i := 0; i < 100; i++ {
|
||
tok, err := GeneratePairingToken()
|
||
require.NoError(t, err)
|
||
_, dup := seen[tok]
|
||
require.False(t, dup, "pairing token 不應重複產生:%s", tok)
|
||
seen[tok] = struct{}{}
|
||
}
|
||
}
|
||
|
||
func TestIsValidPairingToken(t *testing.T) {
|
||
cases := []struct {
|
||
name string
|
||
token string
|
||
valid bool
|
||
}{
|
||
{"正確格式", "vAc_0123456789abcdef0123456789abcdef", true},
|
||
{"大寫 hex 不允許", "vAc_0123456789ABCDEF0123456789ABCDEF", false},
|
||
{"前綴錯誤", "vAs_0123456789abcdef0123456789abcdef", false},
|
||
{"長度太短", "vAc_0123456789abcdef", false},
|
||
{"長度太長", "vAc_0123456789abcdef0123456789abcdef00", false},
|
||
{"含非 hex 字元", "vAc_0123456789abcdef0123456789abcdeZ0", false},
|
||
{"空字串", "", false},
|
||
{"僅前綴", "vAc_", false},
|
||
}
|
||
for _, tc := range cases {
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
assert.Equal(t, tc.valid, IsValidPairingToken(tc.token))
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestIsValidSessionToken(t *testing.T) {
|
||
goodSession := "vAs_" + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
||
assert.True(t, IsValidSessionToken(goodSession))
|
||
assert.False(t, IsValidSessionToken("vAs_short"))
|
||
assert.False(t, IsValidSessionToken("vAc_0123456789abcdef0123456789abcdef"))
|
||
}
|
||
|
||
func TestHashToken_Deterministic(t *testing.T) {
|
||
h1 := HashToken("vAc_abc")
|
||
h2 := HashToken("vAc_abc")
|
||
assert.Equal(t, h1, h2, "同樣輸入應得同樣 hash")
|
||
assert.NotEqual(t, HashToken("vAc_abc"), HashToken("vAc_def"))
|
||
assert.Len(t, h1, 64, "sha256 hex 長度應為 64")
|
||
}
|