Files
Pulse/internal/api/auth_additional_coverage_test.go
rcourtman f60050a801 fix(security): restrict query-string token auth to WebSocket upgrades only
API tokens passed via ?token= query parameter were accepted on all HTTP
requests. This is a security concern because tokens in URLs can leak via
server logs, browser history, referrer headers, and proxy logs.

The query-string token path exists solely for WebSocket connections which
cannot set custom headers during the upgrade handshake. This change adds
an isWebSocketUpgrade check to all three query-string extraction sites
in CheckAuth and extractAndStoreAuthContext, rejecting ?token= on regular
HTTP requests while preserving WebSocket functionality.

No frontend impact — the kiosk flow stores the token in sessionStorage
then uses X-API-Token headers for all API calls.
2026-02-04 09:52:32 +00:00

114 lines
3.5 KiB
Go

package api
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/rcourtman/pulse-go-rewrite/internal/config"
)
func TestCheckAuth_APIOnlyModeRequiresToken(t *testing.T) {
record, err := config.NewAPITokenRecord("token-required-123.12345678", "api", []string{config.ScopeMonitoringRead})
if err != nil {
t.Fatalf("NewAPITokenRecord: %v", err)
}
cfg := &config.Config{
APITokens: []config.APITokenRecord{*record},
}
req := httptest.NewRequest(http.MethodGet, "/api/test", nil)
rr := httptest.NewRecorder()
if CheckAuth(cfg, rr, req) {
t.Fatalf("expected CheckAuth to fail without token in API-only mode")
}
if rr.Code != http.StatusUnauthorized {
t.Fatalf("expected 401, got %d", rr.Code)
}
if !strings.Contains(rr.Body.String(), "API token required") {
t.Fatalf("expected API token required message, got %q", rr.Body.String())
}
if rr.Header().Get("WWW-Authenticate") == "" {
t.Fatalf("expected WWW-Authenticate header to be set")
}
}
func TestCheckAuth_APIOnlyModeRejectsInvalidToken(t *testing.T) {
record, err := config.NewAPITokenRecord("token-valid-123.12345678", "api", []string{config.ScopeMonitoringRead})
if err != nil {
t.Fatalf("NewAPITokenRecord: %v", err)
}
cfg := &config.Config{
APITokens: []config.APITokenRecord{*record},
}
req := httptest.NewRequest(http.MethodGet, "/api/test", nil)
req.Header.Set("X-API-Token", "token-invalid-123.12345678")
rr := httptest.NewRecorder()
if CheckAuth(cfg, rr, req) {
t.Fatalf("expected CheckAuth to fail with invalid token")
}
if rr.Code != http.StatusUnauthorized {
t.Fatalf("expected 401, got %d", rr.Code)
}
}
func TestCheckAuth_APIOnlyModeAcceptsQueryToken(t *testing.T) {
rawToken := "token-query-123.12345678"
record, err := config.NewAPITokenRecord(rawToken, "api", []string{config.ScopeMonitoringRead})
if err != nil {
t.Fatalf("NewAPITokenRecord: %v", err)
}
cfg := &config.Config{
APITokens: []config.APITokenRecord{*record},
}
// Query-string tokens are rejected on regular HTTP to prevent URL-based leakage.
req := httptest.NewRequest(http.MethodGet, "/api/test?token="+rawToken, nil)
rr := httptest.NewRecorder()
if CheckAuth(cfg, rr, req) {
t.Fatalf("expected CheckAuth to reject query token on regular HTTP request")
}
// Query-string tokens are accepted on WebSocket upgrade requests.
wsReq := httptest.NewRequest(http.MethodGet, "/api/test?token="+rawToken, nil)
wsReq.Header.Set("Upgrade", "websocket")
wsReq.Header.Set("Connection", "Upgrade")
wsRR := httptest.NewRecorder()
if !CheckAuth(cfg, wsRR, wsReq) {
t.Fatalf("expected CheckAuth to succeed with query token on WebSocket upgrade")
}
if wsRR.Header().Get("X-Auth-Method") != "api_token" {
t.Fatalf("expected X-Auth-Method api_token, got %q", wsRR.Header().Get("X-Auth-Method"))
}
}
func TestCheckAuth_AcceptsBearerToken(t *testing.T) {
rawToken := "token-bearer-123.12345678"
record, err := config.NewAPITokenRecord(rawToken, "api", []string{config.ScopeMonitoringRead})
if err != nil {
t.Fatalf("NewAPITokenRecord: %v", err)
}
cfg := &config.Config{
APITokens: []config.APITokenRecord{*record},
AuthUser: "admin",
AuthPass: "$2a$10$dummy",
}
req := httptest.NewRequest(http.MethodGet, "/api/test", nil)
req.Header.Set("Authorization", "Bearer "+rawToken)
rr := httptest.NewRecorder()
if !CheckAuth(cfg, rr, req) {
t.Fatalf("expected CheckAuth to succeed with bearer token")
}
if rr.Header().Get("X-Auth-Method") != "api_token" {
t.Fatalf("expected X-Auth-Method api_token, got %q", rr.Header().Get("X-Auth-Method"))
}
}