修复多租户身份配置将所有人类登录都强制解释为租户上下文的问题。Web 现在提供平台与租户两个受控入口,API 严格绑定 context_type、tid、issuer、application 和 subject,并为平台用户建立独立本地投影与可刷新会话。\n\n风险:新增会话身份列保持旧会话可读,新会话一律使用严格约束;未改变租户数据隔离和 API Key 行为。\n\n验证:Go 全量测试与 go vet 通过;PostgreSQL 平台投影、会话和安全事件集成测试通过;前端 lint、141 项测试与生产 build 通过;OpenAPI 生成和迁移安全测试通过。
86 lines
2.8 KiB
Go
86 lines
2.8 KiB
Go
package oidcsession
|
|
|
|
import (
|
|
"bytes"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestLoginTransactionIsEncryptedAndBounded(t *testing.T) {
|
|
now := time.Date(2026, 7, 13, 12, 0, 0, 0, time.UTC)
|
|
cipher, _ := NewCipher(bytes.Repeat([]byte{9}, 32))
|
|
transaction, err := NewLoginTransaction("/workspace?tab=wallet", now)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if transaction.State == transaction.Nonce || len(transaction.PKCEVerifier) != 43 || transaction.State == transaction.PKCEVerifier {
|
|
t.Fatal("state, nonce and PKCE verifier are not independent security material")
|
|
}
|
|
encoded, err := cipher.EncodeLoginTransaction(transaction)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(encoded, transaction.State) || strings.Contains(encoded, transaction.PKCEVerifier) {
|
|
t.Fatal("login transaction cookie contains plaintext security material")
|
|
}
|
|
decoded, err := cipher.DecodeLoginTransaction(encoded, now.Add(9*time.Minute))
|
|
if err != nil || decoded.ReturnTo != transaction.ReturnTo {
|
|
t.Fatalf("decode transaction=%#v err=%v", decoded, err)
|
|
}
|
|
if _, err := cipher.DecodeLoginTransaction(encoded, now.Add(10*time.Minute)); err == nil {
|
|
t.Fatal("10-minute login transaction was accepted")
|
|
}
|
|
}
|
|
|
|
func TestLoginTransactionEncryptsTenantHint(t *testing.T) {
|
|
now := time.Date(2026, 7, 28, 12, 0, 0, 0, time.UTC)
|
|
cipher, _ := NewCipher(bytes.Repeat([]byte{9}, 32))
|
|
tenantHint := "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"
|
|
transaction, err := NewLoginTransactionWithContext("/", "tenant", tenantHint, now)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
encoded, err := cipher.EncodeLoginTransaction(transaction)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(encoded, tenantHint) {
|
|
t.Fatal("login transaction cookie contains plaintext tenant hint")
|
|
}
|
|
decoded, err := cipher.DecodeLoginTransaction(encoded, now)
|
|
if err != nil || decoded.ContextType != "tenant" || decoded.TenantHint != tenantHint {
|
|
t.Fatalf("decoded transaction=%+v err=%v", decoded, err)
|
|
}
|
|
}
|
|
|
|
func TestLoginTransactionRejectsInvalidContextBinding(t *testing.T) {
|
|
now := time.Date(2026, 7, 28, 12, 0, 0, 0, time.UTC)
|
|
tenantHint := "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"
|
|
for _, test := range []struct {
|
|
contextType string
|
|
tenantHint string
|
|
}{
|
|
{contextType: ""},
|
|
{contextType: "account"},
|
|
{contextType: "platform", tenantHint: tenantHint},
|
|
} {
|
|
if _, err := NewLoginTransactionWithContext(
|
|
"/", test.contextType, test.tenantHint, now,
|
|
); err == nil {
|
|
t.Fatalf("unsafe context binding accepted: %#v", test)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestValidReturnToRejectsOpenRedirects(t *testing.T) {
|
|
for _, value := range []string{"https://evil.example", "//evil.example", "/\\evil", "", "workspace"} {
|
|
if ValidReturnTo(value) {
|
|
t.Fatalf("unsafe returnTo accepted: %q", value)
|
|
}
|
|
}
|
|
if !ValidReturnTo("/workspace/tasks?from=login#latest") {
|
|
t.Fatal("safe relative returnTo was rejected")
|
|
}
|
|
}
|