Files
sub2api-cn-relay-manager/internal/app/portal_auth_test.go
phamnazage-jpg 4e2ee087fd feat(vNext.4): implement trusted-subject security chain for portal user key self-service
- Add portal_auth.go: Portal user session auth with HMAC-signed cookies
- Add /api/portal/session/{login,logout,state} endpoints
- Update nginx config template: cookie-to-header trusted proxy pattern
- Update frontend: sync CRM session on login/logout
- Add TRUSTED_SUBJECT_DEPLOY_GUIDE.md with remote43 deployment steps
- Update EXECUTION_BOARD.md: mark trusted-subject blocking issue as resolved

This implements the secure chain:
  Browser → Portal → nginx (cookie→header) → CRM (verify proxy secret)

Required remote43 actions:
1. Generate 64-char hex secret
2. Update .env.crm with TRUSTED_* config
3. Update nginx with cookie map and header injection
4. Restart services

Fixes EXECUTION_BOARD.md 2026-06-08 blocking issue
2026-06-09 07:48:03 +08:00

136 lines
3.6 KiB
Go

package app
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
func TestPortalSessionLoginSetsCookieAndReturnsSubject(t *testing.T) {
cfg := PortalAuthConfig{
SessionSecret: "test-secret-32-bytes-long-for-hmac",
Now: func() time.Time { return time.Unix(1_717_000_000, 0) },
}
req := httptest.NewRequest(http.MethodPost, "/api/portal/session/login", strings.NewReader(`{"email":"user@example.com"}`))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
handlePortalSessionLogin(rec, req, cfg)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
}
// 检查响应体包含 subject
body := rec.Body.String()
if !strings.Contains(body, `"subject_id":"portal-email:user@example.com"`) {
t.Fatalf("response body missing subject_id: %s", body)
}
// 检查设置了 cookie
cookies := rec.Result().Cookies()
if len(cookies) < 2 {
t.Fatalf("expected at least 2 cookies (session + subject), got %d", len(cookies))
}
// 检查 session cookie 是 httpOnly
var foundSessionCookie bool
for _, c := range cookies {
if c.Name == portalSessionCookieName {
foundSessionCookie = true
if !c.HttpOnly {
t.Fatal("session cookie should be HttpOnly")
}
}
}
if !foundSessionCookie {
t.Fatalf("session cookie %s not found", portalSessionCookieName)
}
}
func TestPortalSessionLoginRejectsMissingEmail(t *testing.T) {
cfg := PortalAuthConfig{
SessionSecret: "test-secret",
}
req := httptest.NewRequest(http.MethodPost, "/api/portal/session/login", strings.NewReader(`{}`))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
handlePortalSessionLogin(rec, req, cfg)
if rec.Code != http.StatusBadRequest {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusBadRequest)
}
}
func TestPortalSessionLoginRequiresSecret(t *testing.T) {
cfg := PortalAuthConfig{
SessionSecret: "", // 未配置
}
req := httptest.NewRequest(http.MethodPost, "/api/portal/session/login", strings.NewReader(`{"email":"user@example.com"}`))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
handlePortalSessionLogin(rec, req, cfg)
if rec.Code != http.StatusServiceUnavailable {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusServiceUnavailable)
}
}
func TestPortalSessionLogoutClearsCookies(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/api/portal/session/logout", nil)
rec := httptest.NewRecorder()
handlePortalSessionLogout(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
}
// 检查清除了 cookie
cookies := rec.Result().Cookies()
var clearedSession bool
var clearedSubject bool
for _, c := range cookies {
if c.Name == portalSessionCookieName && c.MaxAge == -1 {
clearedSession = true
}
if c.Name == portalSubjectCookieName && c.MaxAge == -1 {
clearedSubject = true
}
}
if !clearedSession {
t.Fatal("session cookie should be cleared")
}
if !clearedSubject {
t.Fatal("subject cookie should be cleared")
}
}
func TestPortalSessionStateUnauthenticatedWhenNoCookie(t *testing.T) {
cfg := PortalAuthConfig{
SessionSecret: "test-secret",
Now: func() time.Time { return time.Unix(1_717_000_000, 0) },
}
req := httptest.NewRequest(http.MethodGet, "/api/portal/session", nil)
rec := httptest.NewRecorder()
handlePortalSessionState(rec, req, cfg)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
}
body := rec.Body.String()
if !strings.Contains(body, `"authenticated":false`) {
t.Fatalf("expected unauthenticated, got: %s", body)
}
}