mirror of
https://github.com/luxfi/node.git
synced 2026-07-27 03:39:39 +00:00
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)
371 lines
12 KiB
Go
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())
|
|
}
|
|
}
|