從 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>
133 lines
3.5 KiB
Go
133 lines
3.5 KiB
Go
package auth
|
||
|
||
import (
|
||
"context"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/stretchr/testify/assert"
|
||
"github.com/stretchr/testify/require"
|
||
)
|
||
|
||
func TestInMemoryPairingStore_CreateAndValidate(t *testing.T) {
|
||
ctx := context.Background()
|
||
s := NewInMemoryPairingStore()
|
||
|
||
plain, info, err := s.Create(ctx, "user-1", 15*time.Minute)
|
||
require.NoError(t, err)
|
||
require.NotEmpty(t, plain)
|
||
require.NotNil(t, info)
|
||
|
||
assert.True(t, IsValidPairingToken(plain))
|
||
assert.Equal(t, "user-1", info.UserID)
|
||
assert.Equal(t, KindPairing, info.Kind)
|
||
assert.NotNil(t, info.ExpiresAt)
|
||
assert.Nil(t, info.UsedAt)
|
||
|
||
got, err := s.Validate(ctx, plain)
|
||
require.NoError(t, err)
|
||
assert.Equal(t, "user-1", got.UserID)
|
||
}
|
||
|
||
func TestInMemoryPairingStore_Validate_UnknownToken(t *testing.T) {
|
||
s := NewInMemoryPairingStore()
|
||
_, err := s.Validate(context.Background(), "vAc_unknown0000000000000000000000")
|
||
assert.ErrorIs(t, err, ErrInvalidToken)
|
||
}
|
||
|
||
func TestInMemoryPairingStore_MarkUsed_IsOneTime(t *testing.T) {
|
||
ctx := context.Background()
|
||
s := NewInMemoryPairingStore()
|
||
|
||
plain, _, err := s.Create(ctx, "user-1", 15*time.Minute)
|
||
require.NoError(t, err)
|
||
|
||
require.NoError(t, s.MarkUsed(ctx, plain, "device-1"))
|
||
|
||
// Validate 必須失敗(一次性 token 已消費)。
|
||
_, err = s.Validate(ctx, plain)
|
||
assert.ErrorIs(t, err, ErrTokenUsed)
|
||
|
||
// 再次 MarkUsed 應為 no-op(冪等)。
|
||
assert.NoError(t, s.MarkUsed(ctx, plain, "another-device"))
|
||
}
|
||
|
||
func TestInMemoryPairingStore_Revoke(t *testing.T) {
|
||
ctx := context.Background()
|
||
s := NewInMemoryPairingStore()
|
||
|
||
plain, _, err := s.Create(ctx, "user-1", 15*time.Minute)
|
||
require.NoError(t, err)
|
||
|
||
require.NoError(t, s.Revoke(ctx, plain))
|
||
|
||
_, err = s.Validate(ctx, plain)
|
||
assert.ErrorIs(t, err, ErrTokenRevoked)
|
||
|
||
// 撤銷不存在的 token → ErrInvalidToken
|
||
assert.ErrorIs(t, s.Revoke(ctx, "vAc_abcdef00000000000000000000000000"), ErrInvalidToken)
|
||
}
|
||
|
||
func TestInMemoryPairingStore_CleanupExpired(t *testing.T) {
|
||
ctx := context.Background()
|
||
s := NewInMemoryPairingStore()
|
||
|
||
// 產生一個已過期的 token(ttl = 1ms)
|
||
expired, _, err := s.Create(ctx, "user-1", 1*time.Millisecond)
|
||
require.NoError(t, err)
|
||
|
||
// 另一個尚未過期
|
||
fresh, _, err := s.Create(ctx, "user-1", 1*time.Hour)
|
||
require.NoError(t, err)
|
||
|
||
// 等 10ms 確保第一個過期
|
||
time.Sleep(10 * time.Millisecond)
|
||
|
||
removed, err := s.CleanupExpired(ctx, time.Now().UTC())
|
||
require.NoError(t, err)
|
||
assert.Equal(t, 1, removed)
|
||
|
||
_, err = s.Validate(ctx, expired)
|
||
assert.ErrorIs(t, err, ErrInvalidToken, "過期的 token 應被清掉")
|
||
|
||
_, err = s.Validate(ctx, fresh)
|
||
assert.NoError(t, err, "未過期的 token 不應被清")
|
||
}
|
||
|
||
func TestInMemoryPairingStore_List_ByUser(t *testing.T) {
|
||
ctx := context.Background()
|
||
s := NewInMemoryPairingStore()
|
||
|
||
_, _, err := s.Create(ctx, "user-A", time.Hour)
|
||
require.NoError(t, err)
|
||
_, _, err = s.Create(ctx, "user-A", time.Hour)
|
||
require.NoError(t, err)
|
||
_, _, err = s.Create(ctx, "user-B", time.Hour)
|
||
require.NoError(t, err)
|
||
|
||
listA, err := s.List(ctx, "user-A")
|
||
require.NoError(t, err)
|
||
assert.Len(t, listA, 2)
|
||
|
||
listB, err := s.List(ctx, "user-B")
|
||
require.NoError(t, err)
|
||
assert.Len(t, listB, 1)
|
||
|
||
listNone, err := s.List(ctx, "user-X")
|
||
require.NoError(t, err)
|
||
assert.Empty(t, listNone)
|
||
}
|
||
|
||
func TestInMemoryPairingStore_Validate_Expired(t *testing.T) {
|
||
ctx := context.Background()
|
||
s := NewInMemoryPairingStore()
|
||
|
||
plain, _, err := s.Create(ctx, "user-1", 1*time.Millisecond)
|
||
require.NoError(t, err)
|
||
|
||
time.Sleep(5 * time.Millisecond)
|
||
|
||
_, err = s.Validate(ctx, plain)
|
||
assert.ErrorIs(t, err, ErrTokenExpired)
|
||
}
|