mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 16:59:40 +02:00
Critical security and correctness fixes for the sync server: Security: - Fix WebSocket JWT validation — was completely broken (hardcoded "pending-auth"). Now validates JWT via JWKS, rejects invalid tokens, enforces 10-second auth deadline, sends auth-ok confirmation. - Add 10 MB request body size limit (prevents OOM attacks) - Validate op field (must be insert/update/delete) - Validate table and id fields (must be non-empty) - Abort sync on RecordChange failure (was silently continuing) Correctness: - Fix silent JSON unmarshal errors in store (now returns error) - Copy client set before iterating in NotifyUser (prevents race) - Add write timeout on WebSocket notifications Testing (19 tests, 0 -> 100% for unit-testable code): - auth: token extraction, validator init, missing auth handling - config: defaults, env override, invalid port - sync: op validation, changeset validation, response format, field change round-trip, body size constant Documentation: - Add CLAUDE.md with architecture, sync protocol, LWW explanation, API endpoints, configuration, security notes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
82 lines
2 KiB
Go
82 lines
2 KiB
Go
package auth
|
|
|
|
import (
|
|
"net/http"
|
|
"testing"
|
|
)
|
|
|
|
func TestExtractToken(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
header string
|
|
wantToken string
|
|
}{
|
|
{"valid bearer", "Bearer eyJhbGciOiJFZERTQSJ9.test.sig", "eyJhbGciOiJFZERTQSJ9.test.sig"},
|
|
{"missing bearer prefix", "eyJhbGciOiJFZERTQSJ9.test.sig", ""},
|
|
{"empty header", "", ""},
|
|
{"lowercase bearer", "bearer token123", ""},
|
|
{"only bearer", "Bearer ", ""},
|
|
{"bearer with space", "Bearer token123", " token123"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
r, _ := http.NewRequest("GET", "/", nil)
|
|
if tt.header != "" {
|
|
r.Header.Set("Authorization", tt.header)
|
|
}
|
|
|
|
got := ExtractToken(r)
|
|
if got != tt.wantToken {
|
|
t.Errorf("ExtractToken() = %q, want %q", got, tt.wantToken)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewValidator(t *testing.T) {
|
|
v := NewValidator("http://localhost:3001/api/auth/jwks")
|
|
|
|
if v.jwksURL != "http://localhost:3001/api/auth/jwks" {
|
|
t.Errorf("jwksURL = %q, want 'http://localhost:3001/api/auth/jwks'", v.jwksURL)
|
|
}
|
|
|
|
if len(v.keys) != 0 {
|
|
t.Errorf("expected empty keys map, got %d keys", len(v.keys))
|
|
}
|
|
|
|
if v.fetchEvery.Minutes() != 5 {
|
|
t.Errorf("fetchEvery = %v, want 5m", v.fetchEvery)
|
|
}
|
|
}
|
|
|
|
func TestValidateTokenNoKeys(t *testing.T) {
|
|
// Validator with unreachable JWKS endpoint
|
|
v := NewValidator("http://localhost:99999/jwks")
|
|
|
|
_, err := v.ValidateToken("some.invalid.token")
|
|
if err == nil {
|
|
t.Error("expected error for token with no keys, got nil")
|
|
}
|
|
}
|
|
|
|
func TestUserIDFromRequestNoAuth(t *testing.T) {
|
|
v := NewValidator("http://localhost:99999/jwks")
|
|
|
|
r, _ := http.NewRequest("GET", "/", nil)
|
|
_, err := v.UserIDFromRequest(r)
|
|
if err == nil {
|
|
t.Error("expected error for request without auth header")
|
|
}
|
|
}
|
|
|
|
func TestUserIDFromRequestEmptyBearer(t *testing.T) {
|
|
v := NewValidator("http://localhost:99999/jwks")
|
|
|
|
r, _ := http.NewRequest("GET", "/", nil)
|
|
r.Header.Set("Authorization", "Bearer ")
|
|
_, err := v.UserIDFromRequest(r)
|
|
if err == nil {
|
|
t.Error("expected error for empty bearer token")
|
|
}
|
|
}
|