Files
node/service/auth/auth_test.go
T
Hanzo AI c3b398bc7b json: migrate every encoding/json import to json/v2 (go-json-experiment)
External (HTTP / JSON-RPC) is the only place JSON is legitimate. Every
existing encoding/json import in node/ moves to github.com/go-json-experiment/json
(v2 root, not the v1 sub-package). NewEncoder/NewDecoder rewrite to
MarshalWrite/UnmarshalRead. MarshalIndent rewrites to Marshal with
jsontext.WithIndent. json.RawMessage rewrites to jsontext.Value.
*json.SyntaxError rewrites to *jsontext.SyntacticError.

81 files migrated. LLM.md captures the rule + v1->v2 delta table.

Known v2 semantic deltas surfaced by existing tests (followups, not regressions):
- [N]byte fields with no MarshalJSON now marshal as base64 string (v1 marshalled
  as JSON array of byte numbers). Affects vms/platformvm/txs/*_test.go fixtures
  with embedded BLS proofOfPossession.
- time.Duration has no v2 default representation; configs that wire-format
  Duration as nanoseconds (vms/{xvm,platformvm}/config, config/spec) need to
  switch to string-form Duration or carry an explicit option. v2 root does not
  re-export FormatDurationAsNano.
- v2 enforces strict UTF-8 (vms/chainadapter/messaging fixture has non-UTF-8).
- json.MarshalWrite does not append a trailing '\n' (v1 NewEncoder.Encode did);
  service/auth/auth_test.go expectation updated.
- nil []byte round-trips to empty (not nil); config_test deep-equal fixtures
  surface this.

All affected sites are at the API boundary; ZAP wire envelope already covers
the internal data paths (state, P2P, consensus, MPC, threshold). Internal
JSON sites that should move to ZAP next (separate work):
- vms/da/store.go            (DA blob/cert storage as JSON)
- vms/platformvm/airdrop     (airdrop claims as JSON in db)
- vms/chainadapter/appchain  (SQLite materializer schema/data blobs)
- vms/chainadapter/messaging (conversation codec)
- staking/kms.go             (KMS HTTP client — external technically, leave)
- utils/{bimap,ips}          (small marshaler shims — low priority)
2026-06-06 22:26:02 -07:00

371 lines
12 KiB
Go

// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package auth
import (
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
jwt "github.com/golang-jwt/jwt/v4"
"github.com/stretchr/testify/require"
"github.com/luxfi/log"
"github.com/luxfi/node/utils/password"
)
var (
testPassword = "password!@#$%$#@!"
hashedPassword = password.Hash{}
unAuthorizedResponseRegex = `^{"jsonrpc":"2.0","error":{"code":-32600,"message":"(.*)"},"id":1}`
errTest = errors.New("non-nil error")
hostName = "http://127.0.0.1:9630"
)
func init() {
if err := hashedPassword.Set(testPassword); err != nil {
panic(err)
}
}
// Always returns 200 (http.StatusOK)
var dummyHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
func TestNewTokenWrongPassword(t *testing.T) {
require := require.New(t)
auth := NewFromHash(log.NewNoOpLogger(), "auth", hashedPassword)
_, err := auth.NewToken("", defaultTokenLifespan, []string{"endpoint1, endpoint2"})
require.ErrorIs(err, password.ErrEmptyPassword)
_, err = auth.NewToken("notThePassword", defaultTokenLifespan, []string{"endpoint1, endpoint2"})
require.ErrorIs(err, errWrongPassword)
}
func TestNewTokenHappyPath(t *testing.T) {
require := require.New(t)
auth := NewFromHash(log.NewNoOpLogger(), "auth", hashedPassword).(*auth)
now := time.Now()
auth.clock.Set(now)
// Make a token
endpoints := []string{"endpoint1", "endpoint2", "endpoint3"}
tokenStr, err := auth.NewToken(testPassword, defaultTokenLifespan, endpoints)
require.NoError(err)
// Parse the token
token, err := jwt.ParseWithClaims(tokenStr, &endpointClaims{}, func(*jwt.Token) (interface{}, error) {
auth.lock.RLock()
defer auth.lock.RUnlock()
return auth.password.Password[:], nil
})
require.NoError(err)
require.IsType(&endpointClaims{}, token.Claims)
claims := token.Claims.(*endpointClaims)
require.Equal(endpoints, claims.Endpoints)
shouldExpireAt := jwt.NewNumericDate(now.Add(defaultTokenLifespan))
require.Equal(shouldExpireAt, claims.ExpiresAt)
}
func TestTokenHasWrongSig(t *testing.T) {
require := require.New(t)
auth := NewFromHash(log.NewNoOpLogger(), "auth", hashedPassword).(*auth)
// Make a token
endpoints := []string{"endpoint1", "endpoint2", "endpoint3"}
tokenStr, err := auth.NewToken(testPassword, defaultTokenLifespan, endpoints)
require.NoError(err)
// Try to parse the token using the wrong password
_, err = jwt.ParseWithClaims(tokenStr, &endpointClaims{}, func(*jwt.Token) (interface{}, error) {
auth.lock.RLock()
defer auth.lock.RUnlock()
return []byte(""), nil
})
require.ErrorIs(err, jwt.ErrSignatureInvalid)
// Try to parse the token using the wrong password
_, err = jwt.ParseWithClaims(tokenStr, &endpointClaims{}, func(*jwt.Token) (interface{}, error) {
auth.lock.RLock()
defer auth.lock.RUnlock()
return []byte("notThePassword"), nil
})
require.ErrorIs(err, jwt.ErrSignatureInvalid)
}
func TestChangePassword(t *testing.T) {
require := require.New(t)
auth := NewFromHash(log.NewNoOpLogger(), "auth", hashedPassword).(*auth)
// Password must be at least 20 chars for Fair strength
password2 := "fejhkefjhefjhefhjeXX" // #nosec G101
var err error
err = auth.ChangePassword("", password2)
require.ErrorIs(err, errWrongPassword)
err = auth.ChangePassword("notThePassword", password2)
require.ErrorIs(err, errWrongPassword)
err = auth.ChangePassword(testPassword, "")
require.ErrorIs(err, password.ErrEmptyPassword)
require.NoError(auth.ChangePassword(testPassword, password2))
require.True(auth.password.Check(password2))
// Password must be at least 20 chars for Fair strength
password3 := "ufwhwohwfohawfhwdwdX" // #nosec G101
err = auth.ChangePassword(testPassword, password3)
require.ErrorIs(err, errWrongPassword)
require.NoError(auth.ChangePassword(password2, password3))
}
func TestRevokeToken(t *testing.T) {
require := require.New(t)
auth := NewFromHash(log.NewNoOpLogger(), "auth", hashedPassword).(*auth)
// Make a token
endpoints := []string{"/ext/info", "/ext/bc/X", "/ext/metrics"}
tokenStr, err := auth.NewToken(testPassword, defaultTokenLifespan, endpoints)
require.NoError(err)
require.NoError(auth.RevokeToken(tokenStr, testPassword))
require.Len(auth.revoked, 1)
}
func TestWrapHandlerHappyPath(t *testing.T) {
require := require.New(t)
auth := NewFromHash(log.NewNoOpLogger(), "auth", hashedPassword)
// Make a token
endpoints := []string{"/ext/info", "/ext/bc/X", "/ext/metrics"}
tokenStr, err := auth.NewToken(testPassword, defaultTokenLifespan, endpoints)
require.NoError(err)
wrappedHandler := auth.WrapHandler(dummyHandler)
for _, endpoint := range endpoints {
req := httptest.NewRequest(http.MethodPost, hostName+endpoint, strings.NewReader(""))
req.Header.Add("Authorization", headerValStart+tokenStr)
rr := httptest.NewRecorder()
wrappedHandler.ServeHTTP(rr, req)
require.Equal(http.StatusOK, rr.Code)
}
}
func TestWrapHandlerRevokedToken(t *testing.T) {
require := require.New(t)
auth := NewFromHash(log.NewNoOpLogger(), "auth", hashedPassword)
// Make a token
endpoints := []string{"/ext/info", "/ext/bc/X", "/ext/metrics"}
tokenStr, err := auth.NewToken(testPassword, defaultTokenLifespan, endpoints)
require.NoError(err)
require.NoError(auth.RevokeToken(tokenStr, testPassword))
wrappedHandler := auth.WrapHandler(dummyHandler)
for _, endpoint := range endpoints {
req := httptest.NewRequest(http.MethodPost, hostName+endpoint, strings.NewReader(""))
req.Header.Add("Authorization", headerValStart+tokenStr)
rr := httptest.NewRecorder()
wrappedHandler.ServeHTTP(rr, req)
require.Equal(http.StatusUnauthorized, rr.Code)
require.Contains(rr.Body.String(), errTokenRevoked.Error())
require.Regexp(unAuthorizedResponseRegex, rr.Body.String())
}
}
func TestWrapHandlerExpiredToken(t *testing.T) {
require := require.New(t)
auth := NewFromHash(log.NewNoOpLogger(), "auth", hashedPassword).(*auth)
auth.clock.Set(time.Now().Add(-2 * defaultTokenLifespan))
// Make a token that expired well in the past
endpoints := []string{"/ext/info", "/ext/bc/X", "/ext/metrics"}
tokenStr, err := auth.NewToken(testPassword, defaultTokenLifespan, endpoints)
require.NoError(err)
wrappedHandler := auth.WrapHandler(dummyHandler)
for _, endpoint := range endpoints {
req := httptest.NewRequest(http.MethodPost, hostName+endpoint, strings.NewReader(""))
req.Header.Add("Authorization", headerValStart+tokenStr)
rr := httptest.NewRecorder()
wrappedHandler.ServeHTTP(rr, req)
require.Equal(http.StatusUnauthorized, rr.Code)
require.Contains(rr.Body.String(), "expired")
require.Regexp(unAuthorizedResponseRegex, rr.Body.String())
}
}
func TestWrapHandlerNoAuthToken(t *testing.T) {
require := require.New(t)
auth := NewFromHash(log.NewNoOpLogger(), "auth", hashedPassword)
endpoints := []string{"/ext/info", "/ext/bc/X", "/ext/metrics"}
wrappedHandler := auth.WrapHandler(dummyHandler)
for _, endpoint := range endpoints {
req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://127.0.0.1:9630%s", endpoint), strings.NewReader(""))
rr := httptest.NewRecorder()
wrappedHandler.ServeHTTP(rr, req)
require.Equal(http.StatusUnauthorized, rr.Code)
require.Contains(rr.Body.String(), errNoToken.Error())
require.Regexp(unAuthorizedResponseRegex, rr.Body.String())
}
}
func TestWrapHandlerUnauthorizedEndpoint(t *testing.T) {
require := require.New(t)
auth := NewFromHash(log.NewNoOpLogger(), "auth", hashedPassword)
// Make a token
endpoints := []string{"/ext/info"}
tokenStr, err := auth.NewToken(testPassword, defaultTokenLifespan, endpoints)
require.NoError(err)
unauthorizedEndpoints := []string{"/ext/bc/X", "/ext/metrics", "", "/foo", "/ext/info/foo"}
wrappedHandler := auth.WrapHandler(dummyHandler)
for _, endpoint := range unauthorizedEndpoints {
req := httptest.NewRequest(http.MethodPost, hostName+endpoint, strings.NewReader(""))
req.Header.Add("Authorization", headerValStart+tokenStr)
rr := httptest.NewRecorder()
wrappedHandler.ServeHTTP(rr, req)
require.Equal(http.StatusUnauthorized, rr.Code)
require.Contains(rr.Body.String(), errTokenInsufficientPermission.Error())
require.Regexp(unAuthorizedResponseRegex, rr.Body.String())
}
}
func TestWrapHandlerAuthEndpoint(t *testing.T) {
require := require.New(t)
auth := NewFromHash(log.NewNoOpLogger(), "auth", hashedPassword)
// Make a token
endpoints := []string{"/ext/info", "/ext/bc/X", "/ext/metrics", "", "/foo", "/ext/info/foo"}
tokenStr, err := auth.NewToken(testPassword, defaultTokenLifespan, endpoints)
require.NoError(err)
wrappedHandler := auth.WrapHandler(dummyHandler)
req := httptest.NewRequest(http.MethodPost, "http://127.0.0.1:9630/ext/auth", strings.NewReader(""))
req.Header.Add("Authorization", headerValStart+tokenStr)
rr := httptest.NewRecorder()
wrappedHandler.ServeHTTP(rr, req)
require.Equal(http.StatusOK, rr.Code)
}
func TestWrapHandlerAccessAll(t *testing.T) {
require := require.New(t)
auth := NewFromHash(log.NewNoOpLogger(), "auth", hashedPassword)
// Make a token that allows access to all endpoints
endpoints := []string{"/ext/info", "/ext/bc/X", "/ext/metrics", "", "/foo", "/ext/foo/info"}
tokenStr, err := auth.NewToken(testPassword, defaultTokenLifespan, []string{"*"})
require.NoError(err)
wrappedHandler := auth.WrapHandler(dummyHandler)
for _, endpoint := range endpoints {
req := httptest.NewRequest(http.MethodPost, hostName+endpoint, strings.NewReader(""))
req.Header.Add("Authorization", headerValStart+tokenStr)
rr := httptest.NewRecorder()
wrappedHandler.ServeHTTP(rr, req)
require.Equal(http.StatusOK, rr.Code)
}
}
func TestWriteUnauthorizedResponse(t *testing.T) {
require := require.New(t)
rr := httptest.NewRecorder()
writeUnauthorizedResponse(rr, errTest)
require.Equal(http.StatusUnauthorized, rr.Code)
require.Equal(`{"jsonrpc":"2.0","error":{"code":-32600,"message":"non-nil error"},"id":1}`, rr.Body.String())
}
func TestWrapHandlerMutatedRevokedToken(t *testing.T) {
require := require.New(t)
auth := NewFromHash(log.NewNoOpLogger(), "auth", hashedPassword)
// Make a token
endpoints := []string{"/ext/info", "/ext/bc/X", "/ext/metrics"}
tokenStr, err := auth.NewToken(testPassword, defaultTokenLifespan, endpoints)
require.NoError(err)
require.NoError(auth.RevokeToken(tokenStr, testPassword))
wrappedHandler := auth.WrapHandler(dummyHandler)
for _, endpoint := range endpoints {
req := httptest.NewRequest(http.MethodPost, hostName+endpoint, strings.NewReader(""))
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s=", tokenStr)) // The appended = at the end looks like padding
rr := httptest.NewRecorder()
wrappedHandler.ServeHTTP(rr, req)
require.Equal(http.StatusUnauthorized, rr.Code)
}
}
func TestWrapHandlerInvalidSigningMethod(t *testing.T) {
require := require.New(t)
auth := NewFromHash(log.NewNoOpLogger(), "auth", hashedPassword).(*auth)
// Make a token
endpoints := []string{"/ext/info", "/ext/bc/X", "/ext/metrics"}
idBytes := [tokenIDByteLen]byte{}
_, err := rand.Read(idBytes[:])
require.NoError(err)
id := base64.RawURLEncoding.EncodeToString(idBytes[:])
claims := endpointClaims{
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(auth.clock.Time().Add(defaultTokenLifespan)),
ID: id,
},
Endpoints: endpoints,
}
token := jwt.NewWithClaims(jwt.SigningMethodHS512, &claims)
tokenStr, err := token.SignedString(auth.password.Password[:])
require.NoError(err)
wrappedHandler := auth.WrapHandler(dummyHandler)
for _, endpoint := range endpoints {
req := httptest.NewRequest(http.MethodPost, hostName+endpoint, strings.NewReader(""))
req.Header.Add("Authorization", headerValStart+tokenStr)
rr := httptest.NewRecorder()
wrappedHandler.ServeHTTP(rr, req)
require.Equal(http.StatusUnauthorized, rr.Code)
require.Contains(rr.Body.String(), errInvalidSigningMethod.Error())
require.Regexp(unAuthorizedResponseRegex, rr.Body.String())
}
}