github.com/core-coin/go-core/v2@v2.1.9/node/node_auth_test.go (about)

     1  // Copyright 2024 by the Authors
     2  // This file is part of the go-core library.
     3  //
     4  // The go-core 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-core 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-core 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/core-coin/go-core/v2/common/hexutil"
    30  	"github.com/core-coin/go-core/v2/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 authTest struct {
    41  	name            string
    42  	endpoint        string
    43  	prov            rpc.HTTPAuth
    44  	expectDialFail  bool
    45  	expectCall1Fail bool
    46  	expectCall2Fail bool
    47  }
    48  
    49  func (at *authTest) Run(t *testing.T) {
    50  	ctx := context.Background()
    51  	cl, err := rpc.DialOptions(ctx, at.endpoint, rpc.WithHTTPAuth(at.prov))
    52  	if at.expectDialFail {
    53  		if err == nil {
    54  			t.Fatal("expected initial dial to fail")
    55  		} else {
    56  			return
    57  		}
    58  	}
    59  	if err != nil {
    60  		t.Fatalf("failed to dial rpc endpoint: %v", err)
    61  	}
    62  
    63  	var x string
    64  	err = cl.CallContext(ctx, &x, "engine_helloWorld")
    65  	if at.expectCall1Fail {
    66  		if err == nil {
    67  			t.Fatal("expected call 1 to fail")
    68  		} else {
    69  			return
    70  		}
    71  	}
    72  	if err != nil {
    73  		t.Fatalf("failed to call rpc endpoint: %v", err)
    74  	}
    75  	if x != "hello engine" {
    76  		t.Fatalf("method was silent but did not return expected value: %q", x)
    77  	}
    78  
    79  	err = cl.CallContext(ctx, &x, "xcb_helloWorld")
    80  	if at.expectCall2Fail {
    81  		if err == nil {
    82  			t.Fatal("expected call 2 to fail")
    83  		} else {
    84  			return
    85  		}
    86  	}
    87  	if err != nil {
    88  		t.Fatalf("failed to call rpc endpoint: %v", err)
    89  	}
    90  	if x != "hello xcb" {
    91  		t.Fatalf("method was silent but did not return expected value: %q", x)
    92  	}
    93  }
    94  
    95  func TestAuthEndpoints(t *testing.T) {
    96  	var secret [32]byte
    97  	if _, err := crand.Read(secret[:]); err != nil {
    98  		t.Fatalf("failed to create jwt secret: %v", err)
    99  	}
   100  	// Gocore must read it from a file, and does not support in-memory JWT secrets, so we create a temporary file.
   101  	jwtPath := path.Join(t.TempDir(), "jwt_secret")
   102  	if err := os.WriteFile(jwtPath, []byte(hexutil.Encode(secret[:])), 0600); err != nil {
   103  		t.Fatalf("failed to prepare jwt secret file: %v", err)
   104  	}
   105  	// We get ports assigned by the node automatically
   106  	conf := &Config{
   107  		HTTPHost:  "127.0.0.1",
   108  		HTTPPort:  0,
   109  		WSHost:    "127.0.0.1",
   110  		WSPort:    0,
   111  		AuthAddr:  "127.0.0.1",
   112  		AuthPort:  0,
   113  		JWTSecret: jwtPath,
   114  
   115  		WSModules:   []string{"xcb", "engine"},
   116  		HTTPModules: []string{"xcb", "engine"}}
   117  	node, err := New(conf)
   118  	if err != nil {
   119  		t.Fatalf("could not create a new node: %v", err)
   120  	}
   121  	// register dummy apis so we can test the modules are available and reachable with authentication
   122  	node.RegisterAPIs([]rpc.API{
   123  		{
   124  			Namespace:     "engine",
   125  			Version:       "1.0",
   126  			Service:       helloRPC("hello engine"),
   127  			Public:        true,
   128  			Authenticated: true,
   129  		},
   130  		{
   131  			Namespace:     "xcb",
   132  			Version:       "1.0",
   133  			Service:       helloRPC("hello xcb"),
   134  			Public:        true,
   135  			Authenticated: true,
   136  		},
   137  	})
   138  	if err := node.Start(); err != nil {
   139  		t.Fatalf("failed to start test node: %v", err)
   140  	}
   141  	defer node.Close()
   142  
   143  	// sanity check we are running different endpoints
   144  	if a, b := node.WSEndpoint(), node.WSAuthEndpoint(); a == b {
   145  		t.Fatalf("expected ws and auth-ws endpoints to be different, got: %q and %q", a, b)
   146  	}
   147  	if a, b := node.HTTPEndpoint(), node.HTTPAuthEndpoint(); a == b {
   148  		t.Fatalf("expected http and auth-http endpoints to be different, got: %q and %q", a, b)
   149  	}
   150  
   151  	goodAuth := NewJWTAuth(secret)
   152  	var otherSecret [32]byte
   153  	if _, err := crand.Read(otherSecret[:]); err != nil {
   154  		t.Fatalf("failed to create jwt secret: %v", err)
   155  	}
   156  	badAuth := NewJWTAuth(otherSecret)
   157  
   158  	notTooLong := time.Second * 57
   159  	tooLong := time.Second * 60
   160  	requestDelay := time.Second * 1
   161  
   162  	testCases := []authTest{
   163  		// Auth works
   164  		{name: "ws good", endpoint: node.WSAuthEndpoint(), prov: goodAuth, expectCall1Fail: false},
   165  		{name: "http good", endpoint: node.HTTPAuthEndpoint(), prov: goodAuth, expectCall1Fail: false},
   166  
   167  		// Try a bad auth
   168  		{name: "ws bad", endpoint: node.WSAuthEndpoint(), prov: badAuth, expectDialFail: true},      // ws auth is immediate
   169  		{name: "http bad", endpoint: node.HTTPAuthEndpoint(), prov: badAuth, expectCall1Fail: true}, // http auth is on first call
   170  
   171  		// A common mistake with JWT is to allow the "none" algorithm, which is a valid JWT but not secure.
   172  		{name: "ws none", endpoint: node.WSAuthEndpoint(), prov: noneAuth(secret), expectDialFail: true},
   173  		{name: "http none", endpoint: node.HTTPAuthEndpoint(), prov: noneAuth(secret), expectCall1Fail: true},
   174  
   175  		// claims of 5 seconds or more, older or newer, are not allowed
   176  		{name: "ws too old", endpoint: node.WSAuthEndpoint(), prov: offsetTimeAuth(secret, -tooLong), expectDialFail: true},
   177  		{name: "http too old", endpoint: node.HTTPAuthEndpoint(), prov: offsetTimeAuth(secret, -tooLong), expectCall1Fail: true},
   178  		// 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"
   179  		{name: "ws too new", endpoint: node.WSAuthEndpoint(), prov: offsetTimeAuth(secret, tooLong+requestDelay), expectDialFail: true},
   180  		{name: "http too new", endpoint: node.HTTPAuthEndpoint(), prov: offsetTimeAuth(secret, tooLong+requestDelay), expectCall1Fail: true},
   181  
   182  		// Try offset the time, but stay just within bounds
   183  		{name: "ws old", endpoint: node.WSAuthEndpoint(), prov: offsetTimeAuth(secret, -notTooLong)},
   184  		{name: "http old", endpoint: node.HTTPAuthEndpoint(), prov: offsetTimeAuth(secret, -notTooLong)},
   185  		{name: "ws new", endpoint: node.WSAuthEndpoint(), prov: offsetTimeAuth(secret, notTooLong)},
   186  		{name: "http new", endpoint: node.HTTPAuthEndpoint(), prov: offsetTimeAuth(secret, notTooLong)},
   187  
   188  		// ws only authenticates on initial dial, then continues communication
   189  		{name: "ws single auth", endpoint: node.WSAuthEndpoint(), prov: changingAuth(goodAuth, badAuth)},
   190  		{name: "http call fail auth", endpoint: node.HTTPAuthEndpoint(), prov: changingAuth(goodAuth, badAuth), expectCall2Fail: true},
   191  		{name: "http call fail time", endpoint: node.HTTPAuthEndpoint(), prov: changingAuth(goodAuth, offsetTimeAuth(secret, tooLong+requestDelay)), expectCall2Fail: true},
   192  	}
   193  
   194  	for _, testCase := range testCases {
   195  		t.Run(testCase.name, testCase.Run)
   196  	}
   197  }
   198  
   199  func noneAuth(secret [32]byte) rpc.HTTPAuth {
   200  	return func(header http.Header) error {
   201  		token := jwt.NewWithClaims(jwt.SigningMethodNone, jwt.MapClaims{
   202  			"iat": &jwt.NumericDate{Time: time.Now()},
   203  		})
   204  		s, err := token.SignedString(secret[:])
   205  		if err != nil {
   206  			return fmt.Errorf("failed to create JWT token: %w", err)
   207  		}
   208  		header.Set("Authorization", "Bearer "+s)
   209  		return nil
   210  	}
   211  }
   212  
   213  func changingAuth(provs ...rpc.HTTPAuth) rpc.HTTPAuth {
   214  	i := 0
   215  	return func(header http.Header) error {
   216  		i += 1
   217  		if i > len(provs) {
   218  			i = len(provs)
   219  		}
   220  		return provs[i-1](header)
   221  	}
   222  }
   223  
   224  func offsetTimeAuth(secret [32]byte, offset time.Duration) rpc.HTTPAuth {
   225  	return func(header http.Header) error {
   226  		token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
   227  			"iat": &jwt.NumericDate{Time: time.Now().Add(offset)},
   228  		})
   229  		s, err := token.SignedString(secret[:])
   230  		if err != nil {
   231  			return fmt.Errorf("failed to create JWT token: %w", err)
   232  		}
   233  		header.Set("Authorization", "Bearer "+s)
   234  		return nil
   235  	}
   236  }