從 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>
107 lines
3.2 KiB
Go
107 lines
3.2 KiB
Go
package oidc
|
||
|
||
import (
|
||
"crypto/sha256"
|
||
"encoding/base64"
|
||
"regexp"
|
||
"testing"
|
||
|
||
"github.com/stretchr/testify/assert"
|
||
"github.com/stretchr/testify/require"
|
||
)
|
||
|
||
// base64url(無 padding)允許的字元集,對齊 RFC 4648 §5。
|
||
// 順帶涵蓋 RFC 7636 §4.1 對 code_verifier 的字元集要求子集。
|
||
var base64URLPattern = regexp.MustCompile(`^[A-Za-z0-9_-]+$`)
|
||
|
||
func TestGenerateCodeVerifier_LengthAndCharset(t *testing.T) {
|
||
v, err := GenerateCodeVerifier()
|
||
require.NoError(t, err)
|
||
|
||
// 32 bytes → base64url 後 43 字元(無 padding)。
|
||
// RFC 7636 規定範圍 43-128 字元;43 字元剛好符合最小邊界。
|
||
assert.Len(t, v, 43, "verifier 應為 43 字元(32 bytes base64url)")
|
||
assert.GreaterOrEqual(t, len(v), 43, "RFC 7636 最小 43 字元")
|
||
assert.LessOrEqual(t, len(v), 128, "RFC 7636 最大 128 字元")
|
||
|
||
assert.Regexp(t, base64URLPattern, v, "verifier 應只含 base64url 字元")
|
||
}
|
||
|
||
func TestGenerateCodeVerifier_Randomness(t *testing.T) {
|
||
const n = 50
|
||
seen := make(map[string]struct{}, n)
|
||
for i := 0; i < n; i++ {
|
||
v, err := GenerateCodeVerifier()
|
||
require.NoError(t, err)
|
||
_, dup := seen[v]
|
||
assert.Falsef(t, dup, "第 %d 次產生與先前重複,亂數源異常", i)
|
||
seen[v] = struct{}{}
|
||
}
|
||
}
|
||
|
||
func TestCodeChallenge_KnownVector(t *testing.T) {
|
||
// RFC 7636 Appendix B 提供的 known answer test:
|
||
// verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
|
||
// challenge = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
|
||
const verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
|
||
const want = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
|
||
|
||
got := CodeChallenge(verifier)
|
||
assert.Equal(t, want, got, "challenge 與 RFC 7636 Appendix B test vector 不符")
|
||
}
|
||
|
||
func TestCodeChallenge_MatchesSHA256(t *testing.T) {
|
||
v, err := GenerateCodeVerifier()
|
||
require.NoError(t, err)
|
||
|
||
want := base64.RawURLEncoding.EncodeToString(sha256Sum([]byte(v)))
|
||
got := CodeChallenge(v)
|
||
assert.Equal(t, want, got, "challenge 應為 base64url(SHA256(verifier))")
|
||
}
|
||
|
||
func TestGenerateState_Format(t *testing.T) {
|
||
s, err := GenerateState()
|
||
require.NoError(t, err)
|
||
// 32 bytes → 43 字元 base64url(無 padding)
|
||
assert.Len(t, s, 43)
|
||
assert.Regexp(t, base64URLPattern, s)
|
||
}
|
||
|
||
func TestGenerateNonce_Format(t *testing.T) {
|
||
n, err := GenerateNonce()
|
||
require.NoError(t, err)
|
||
assert.Len(t, n, 43)
|
||
assert.Regexp(t, base64URLPattern, n)
|
||
}
|
||
|
||
func TestStateAndNonce_Independent(t *testing.T) {
|
||
// state 和 nonce 雖然產生方式相同,但兩次連續呼叫不應產生相同值。
|
||
s1, err := GenerateState()
|
||
require.NoError(t, err)
|
||
s2, err := GenerateState()
|
||
require.NoError(t, err)
|
||
assert.NotEqual(t, s1, s2, "兩次 GenerateState 不應重複")
|
||
|
||
n1, err := GenerateNonce()
|
||
require.NoError(t, err)
|
||
n2, err := GenerateNonce()
|
||
require.NoError(t, err)
|
||
assert.NotEqual(t, n1, n2, "兩次 GenerateNonce 不應重複")
|
||
|
||
assert.NotEqual(t, s1, n1, "state 與 nonce 應為獨立隨機值")
|
||
}
|
||
|
||
func TestRandomBase64URL_RejectNonPositive(t *testing.T) {
|
||
_, err := randomBase64URL(0)
|
||
assert.Error(t, err, "n=0 應拒絕")
|
||
|
||
_, err = randomBase64URL(-1)
|
||
assert.Error(t, err, "n<0 應拒絕")
|
||
}
|
||
|
||
// sha256Sum 是 test helper,避免在測試中每次都寫 [:]。
|
||
func sha256Sum(b []byte) []byte {
|
||
s := sha256.Sum256(b)
|
||
return s[:]
|
||
}
|