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 }