Bundle of small audit findings, all doc-only or dead-code: - F-5: refresh stale escalation-contact comment in models.User — Settings UI dropdown shipped 2026-04-29 (t-paliad-066). - F-10: add "OBSOLETED by migration 018" note to migrations 004/005/006 so readers stop hunting for the live shape in obsolete files. - F-11: document the data-loss semantics of dropping paliad.partner_unit_events on the 027 down — audit rows are append-only telemetry, accepted loss on rollback. - F-15: drop the patholo_session / patholo_refresh cookie fallback added during the 2026-04-16 rebrand. Active users have long since been re-authed through the upgrade path; inactive users hit the normal /login flow. - F-16: refresh stale /api/departments comment in team_pages.go to /api/partner-units (renamed in t-paliad-070). - F-17: move internal/db/migrations/_dev/mock_supabase_auth.sql to internal/db/devtools/ so a future loosening of the //go:embed pattern can't accidentally ship the dev-only fixture. - F-18: update docs/project-status.md "Audit polish-2" entry — the batch shipped via t-paliad-067 / 068 / 073, follow-ups are now tracked under the 2026-04-30 re-audit + t-paliad-074. go build / vet / test clean.
112 lines
3.1 KiB
Go
112 lines
3.1 KiB
Go
package auth
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
)
|
|
|
|
// testSecret mirrors the format of a Supabase JWT signing key.
|
|
var testSecret = []byte("test-secret-for-hs256-verification-123")
|
|
|
|
func sign(t *testing.T, secret []byte, claims jwt.MapClaims) string {
|
|
t.Helper()
|
|
tok := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
|
s, err := tok.SignedString(secret)
|
|
if err != nil {
|
|
t.Fatalf("sign: %v", err)
|
|
}
|
|
return s
|
|
}
|
|
|
|
func TestVerifyToken_Valid(t *testing.T) {
|
|
c := &Client{JWTSecret: testSecret}
|
|
token := sign(t, testSecret, jwt.MapClaims{
|
|
"sub": "11111111-1111-1111-1111-111111111111",
|
|
"exp": time.Now().Add(time.Hour).Unix(),
|
|
})
|
|
got, err := c.VerifyToken(token)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if got.Sub != "11111111-1111-1111-1111-111111111111" {
|
|
t.Errorf("sub: got %q", got.Sub)
|
|
}
|
|
}
|
|
|
|
func TestVerifyToken_WrongSecret(t *testing.T) {
|
|
c := &Client{JWTSecret: testSecret}
|
|
token := sign(t, []byte("attacker-guessed-wrong"), jwt.MapClaims{
|
|
"sub": "11111111-1111-1111-1111-111111111111",
|
|
"exp": time.Now().Add(time.Hour).Unix(),
|
|
})
|
|
if _, err := c.VerifyToken(token); err == nil {
|
|
t.Fatal("expected error for wrong-signature token, got nil (auth bypass)")
|
|
}
|
|
}
|
|
|
|
func TestVerifyToken_Expired(t *testing.T) {
|
|
c := &Client{JWTSecret: testSecret}
|
|
token := sign(t, testSecret, jwt.MapClaims{
|
|
"sub": "11111111-1111-1111-1111-111111111111",
|
|
"exp": time.Now().Add(-time.Hour).Unix(),
|
|
})
|
|
_, err := c.VerifyToken(token)
|
|
if err == nil {
|
|
t.Fatal("expected error for expired token")
|
|
}
|
|
if !errors.Is(err, jwt.ErrTokenExpired) {
|
|
t.Errorf("expected ErrTokenExpired, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestVerifyToken_AlgNone(t *testing.T) {
|
|
c := &Client{JWTSecret: testSecret}
|
|
// An attacker might try alg=none to bypass signature checks.
|
|
tok := jwt.NewWithClaims(jwt.SigningMethodNone, jwt.MapClaims{
|
|
"sub": "22222222-2222-2222-2222-222222222222",
|
|
"exp": time.Now().Add(time.Hour).Unix(),
|
|
})
|
|
token, err := tok.SignedString(jwt.UnsafeAllowNoneSignatureType)
|
|
if err != nil {
|
|
t.Fatalf("sign none: %v", err)
|
|
}
|
|
if _, err := c.VerifyToken(token); err == nil {
|
|
t.Fatal("expected error for alg=none, got nil (critical bypass)")
|
|
}
|
|
}
|
|
|
|
func TestVerifyToken_MissingSub(t *testing.T) {
|
|
c := &Client{JWTSecret: testSecret}
|
|
token := sign(t, testSecret, jwt.MapClaims{
|
|
"exp": time.Now().Add(time.Hour).Unix(),
|
|
})
|
|
if _, err := c.VerifyToken(token); err == nil {
|
|
t.Fatal("expected error for missing sub claim")
|
|
}
|
|
}
|
|
|
|
func TestVerifyToken_Garbage(t *testing.T) {
|
|
c := &Client{JWTSecret: testSecret}
|
|
if _, err := c.VerifyToken("not.a.jwt"); err == nil {
|
|
t.Fatal("expected error for garbage token")
|
|
}
|
|
}
|
|
|
|
func TestMiddleware_NoCookieRejected(t *testing.T) {
|
|
c := &Client{JWTSecret: testSecret}
|
|
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
t.Fatal("next should not run without auth")
|
|
})
|
|
req := httptest.NewRequest("GET", "/api/anything", nil)
|
|
rec := httptest.NewRecorder()
|
|
c.Middleware(next).ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusUnauthorized {
|
|
t.Errorf("expected 401 on API without cookies, got %d", rec.Code)
|
|
}
|
|
}
|