github.com/authcall/reference-optimistic-geth@v0.0.0-20220816224302-06313bfeb8d2/node/node_auth_test.go (about)

     1  // Copyright 2022 The go-ethereum Authors
     2  // This file is part of the go-ethereum library.
     3  //
     4  // The go-ethereum library is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Lesser General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // The go-ethereum library is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    12  // GNU Lesser General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Lesser General Public License
    15  // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package node
    18  
    19  import (
    20  	"context"
    21  	crand "crypto/rand"
    22  	"fmt"
    23  	"net/http"
    24  	"os"
    25  	"path"
    26  	"testing"
    27  	"time"
    28  
    29  	"github.com/ethereum/go-ethereum/common/hexutil"
    30  	"github.com/ethereum/go-ethereum/rpc"
    31  	"github.com/golang-jwt/jwt/v4"
    32  )
    33  
    34  type helloRPC string
    35  
    36  func (ta helloRPC) HelloWorld() (string, error) {
    37  	return string(ta), nil
    38  }
    39  
    40  type TestAuthProvider func(header *http.Header) error
    41  
    42  func (fn TestAuthProvider) AddAuthHeader(header *http.Header) error {
    43  	return fn(header)
    44  }
    45  
    46  type authTest struct {
    47  	name            string
    48  	endpoint        string
    49  	prov            rpc.HeaderAuthProvider
    50  	expectDialFail  bool
    51  	expectCall1Fail bool
    52  	expectCall2Fail bool
    53  }
    54  
    55  func (at *authTest) Run(t *testing.T) {
    56  	ctx := context.Background()
    57  	cl, err := rpc.DialWithAuth(ctx, at.endpoint, at.prov)
    58  	if at.expectDialFail {
    59  		if err == nil {
    60  			t.Fatal("expected initial dial to fail")
    61  		} else {
    62  			return
    63  		}
    64  	}
    65  	if err != nil {
    66  		t.Fatalf("failed to dial rpc endpoint: %v", err)
    67  	}
    68  	var x string
    69  	err = cl.CallContext(ctx, &x, "engine_helloWorld")
    70  	if at.expectCall1Fail {
    71  		if err == nil {
    72  			t.Fatal("expected call 1 to fail")
    73  		} else {
    74  			return
    75  		}
    76  	}
    77  	if err != nil {
    78  		t.Fatalf("failed to call rpc endpoint: %v", err)
    79  	}
    80  	if x != "hello engine" {
    81  		t.Fatalf("method was silent but did not return expected value: %q", x)
    82  	}
    83  	err = cl.CallContext(ctx, &x, "eth_helloWorld")
    84  	if at.expectCall2Fail {
    85  		if err == nil {
    86  			t.Fatal("expected call 2 to fail")
    87  		} else {
    88  			return
    89  		}
    90  	}
    91  	if err != nil {
    92  		t.Fatalf("failed to call rpc endpoint: %v", err)
    93  	}
    94  	if x != "hello eth" {
    95  		t.Fatalf("method was silent but did not return expected value: %q", x)
    96  	}
    97  }
    98  
    99  func TestAuthEndpoints(t *testing.T) {
   100  	var secret [32]byte
   101  	if _, err := crand.Read(secret[:]); err != nil {
   102  		t.Fatalf("failed to create jwt secret: %v", err)
   103  	}
   104  	// Geth must read it from a file, and does not support in-memory JWT secrets, so we create a temporary file.
   105  	jwtPath := path.Join(t.TempDir(), "jwt_secret")
   106  	if err := os.WriteFile(jwtPath, []byte(hexutil.Encode(secret[:])), 0600); err != nil {
   107  		t.Fatalf("failed to prepare jwt secret file: %v", err)
   108  	}
   109  	// We get ports assigned by the node automatically
   110  	conf := &Config{
   111  		HTTPHost:  "127.0.0.1",
   112  		HTTPPort:  0,
   113  		WSHost:    "127.0.0.1",
   114  		WSPort:    0,
   115  		AuthAddr:  "127.0.0.1",
   116  		AuthPort:  0,
   117  		JWTSecret: jwtPath,
   118  
   119  		WSModules:   []string{"eth", "engine"},
   120  		HTTPModules: []string{"eth", "engine"},
   121  	}
   122  	node, err := New(conf)
   123  	if err != nil {
   124  		t.Fatalf("could not create a new node: %v", err)
   125  	}
   126  	// register dummy apis so we can test the modules are available and reachable with authentication
   127  	node.RegisterAPIs([]rpc.API{
   128  		{
   129  			Namespace:     "engine",
   130  			Version:       "1.0",
   131  			Service:       helloRPC("hello engine"),
   132  			Public:        true,
   133  			Authenticated: true,
   134  		},
   135  		{
   136  			Namespace:     "eth",
   137  			Version:       "1.0",
   138  			Service:       helloRPC("hello eth"),
   139  			Public:        true,
   140  			Authenticated: true,
   141  		},
   142  	})
   143  	if err := node.Start(); err != nil {
   144  		t.Fatalf("failed to start test node: %v", err)
   145  	}
   146  	defer node.Close()
   147  
   148  	// sanity check we are running different endpoints
   149  	if a, b := node.WSEndpoint(), node.WSAuthEndpoint(); a == b {
   150  		t.Fatalf("expected ws and auth-ws endpoints to be different, got: %q and %q", a, b)
   151  	}
   152  	if a, b := node.HTTPEndpoint(), node.HTTPAuthEndpoint(); a == b {
   153  		t.Fatalf("expected http and auth-http endpoints to be different, got: %q and %q", a, b)
   154  	}
   155  
   156  	goodAuth := rpc.NewJWTAuthProvider(secret)
   157  	var otherSecret [32]byte
   158  	if _, err := crand.Read(otherSecret[:]); err != nil {
   159  		t.Fatalf("failed to create jwt secret: %v", err)
   160  	}
   161  	badAuth := rpc.NewJWTAuthProvider(otherSecret)
   162  	noneAuth := TestAuthProvider(func(header *http.Header) error {
   163  		token := jwt.NewWithClaims(jwt.SigningMethodNone, jwt.MapClaims{
   164  			"iat": &jwt.NumericDate{Time: time.Now()},
   165  		})
   166  		s, err := token.SignedString(secret[:])
   167  		if err != nil {
   168  			return fmt.Errorf("failed to create JWT token: %w", err)
   169  		}
   170  		header.Add("Authorization", "Bearer "+s)
   171  		return nil
   172  	})
   173  	offsetTimeAuth := func(offset time.Duration) TestAuthProvider {
   174  		return func(header *http.Header) error {
   175  			token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
   176  				"iat": &jwt.NumericDate{Time: time.Now().Add(offset)},
   177  			})
   178  			s, err := token.SignedString(secret[:])
   179  			if err != nil {
   180  				return fmt.Errorf("failed to create JWT token: %w", err)
   181  			}
   182  			header.Add("Authorization", "Bearer "+s)
   183  			return nil
   184  		}
   185  	}
   186  	changingAuth := func(provs ...rpc.HeaderAuthProvider) TestAuthProvider {
   187  		i := 0
   188  		return func(header *http.Header) error {
   189  			i += 1
   190  			if i > len(provs) {
   191  				i = len(provs)
   192  			}
   193  			return provs[i-1].AddAuthHeader(header)
   194  		}
   195  	}
   196  
   197  	notTooLong := time.Second * 57
   198  	tooLong := time.Second * 60
   199  	requestDelay := time.Second
   200  
   201  	testCases := []authTest{
   202  		// Auth works
   203  		{name: "ws good", endpoint: node.WSAuthEndpoint(), prov: goodAuth, expectCall1Fail: false},
   204  		{name: "http good", endpoint: node.HTTPAuthEndpoint(), prov: goodAuth, expectCall1Fail: false},
   205  
   206  		// Try nil auth
   207  		{name: "ws nil auth provider", endpoint: node.WSAuthEndpoint(), prov: nil, expectDialFail: true},
   208  		{name: "http nil auth provider", endpoint: node.HTTPAuthEndpoint(), prov: nil, expectDialFail: true},
   209  
   210  		// Try a bad auth
   211  		{name: "ws bad", endpoint: node.WSAuthEndpoint(), prov: badAuth, expectDialFail: true},      // ws auth is immediate
   212  		{name: "http bad", endpoint: node.HTTPAuthEndpoint(), prov: badAuth, expectCall1Fail: true}, // http auth is on first call
   213  
   214  		// A common mistake with JWT is to allow the "none" algorithm, which is a valid JWT but not secure.
   215  		{name: "ws none", endpoint: node.WSAuthEndpoint(), prov: noneAuth, expectDialFail: true},
   216  		{name: "http none", endpoint: node.HTTPAuthEndpoint(), prov: noneAuth, expectCall1Fail: true},
   217  
   218  		// claims of 5 seconds or more, older or newer, are not allowed
   219  		{name: "ws too old", endpoint: node.WSAuthEndpoint(), prov: offsetTimeAuth(-tooLong), expectDialFail: true},
   220  		{name: "http too old", endpoint: node.HTTPAuthEndpoint(), prov: offsetTimeAuth(-tooLong), expectCall1Fail: true},
   221  		// note: for it to be too long we need to add a delay, so that once we receive the request, the difference has not dipped below the "tooLong"
   222  		{name: "ws too new", endpoint: node.WSAuthEndpoint(), prov: offsetTimeAuth(tooLong + requestDelay), expectDialFail: true},
   223  		{name: "http too new", endpoint: node.HTTPAuthEndpoint(), prov: offsetTimeAuth(tooLong + requestDelay), expectCall1Fail: true},
   224  
   225  		// Try offset the time, but stay just within bounds
   226  		{name: "ws old", endpoint: node.WSAuthEndpoint(), prov: offsetTimeAuth(-notTooLong)},
   227  		{name: "http old", endpoint: node.HTTPAuthEndpoint(), prov: offsetTimeAuth(-notTooLong)},
   228  		{name: "ws new", endpoint: node.WSAuthEndpoint(), prov: offsetTimeAuth(notTooLong)},
   229  		{name: "http new", endpoint: node.HTTPAuthEndpoint(), prov: offsetTimeAuth(notTooLong)},
   230  
   231  		// ws only authenticates on initial dial, then continues communication
   232  		{name: "ws single auth", endpoint: node.WSAuthEndpoint(), prov: changingAuth(goodAuth, badAuth)},
   233  		{name: "http call fail auth", endpoint: node.HTTPAuthEndpoint(), prov: changingAuth(goodAuth, badAuth), expectCall2Fail: true},
   234  		{name: "http call fail time", endpoint: node.HTTPAuthEndpoint(), prov: changingAuth(goodAuth, offsetTimeAuth(tooLong+requestDelay)), expectCall2Fail: true},
   235  	}
   236  
   237  	for _, testCase := range testCases {
   238  		t.Run(testCase.name, testCase.Run)
   239  	}
   240  }