Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cmd/geth, node, rpc: implement jwt tokens #24364

Merged
merged 9 commits into from
Mar 7, 2022
Next Next commit
rpc, node: refactor request validation and add jwt validation
holiman authored and MariusVanDerWijden committed Feb 25, 2022
commit 52ff63860e97ac9c393b5858498b22cebf1c9bfb
1 change: 1 addition & 0 deletions cmd/geth/main.go
Original file line number Diff line number Diff line change
@@ -164,6 +164,7 @@ var (
utils.HTTPListenAddrFlag,
utils.HTTPPortFlag,
utils.HTTPCORSDomainFlag,
utils.JWTSecretFlag,
utils.HTTPVirtualHostsFlag,
utils.GraphQLEnabledFlag,
utils.GraphQLCORSDomainFlag,
1 change: 1 addition & 0 deletions cmd/geth/usage.go
Original file line number Diff line number Diff line change
@@ -135,6 +135,7 @@ var AppHelpFlagGroups = []flags.FlagGroup{
Flags: []cli.Flag{
utils.IPCDisabledFlag,
utils.IPCPathFlag,
utils.JWTSecretFlag,
utils.HTTPEnabledFlag,
utils.HTTPListenAddrFlag,
utils.HTTPPortFlag,
8 changes: 8 additions & 0 deletions cmd/utils/flags.go
Original file line number Diff line number Diff line change
@@ -518,6 +518,10 @@ var (
Usage: "Sets a cap on transaction fee (in ether) that can be sent via the RPC APIs (0 = no cap)",
Value: ethconfig.Defaults.RPCTxFeeCap,
}
JWTSecretFlag = cli.StringFlag{
Name: "jwt-secret",
MariusVanDerWijden marked this conversation as resolved.
Show resolved Hide resolved
Usage: "JWT secret to use for authenticated RPC endpoints",
MariusVanDerWijden marked this conversation as resolved.
Show resolved Hide resolved
}
// Logging and debug settings
EthStatsURLFlag = cli.StringFlag{
Name: "ethstats",
@@ -1218,6 +1222,10 @@ func SetNodeConfig(ctx *cli.Context, cfg *node.Config) {
setDataDir(ctx, cfg)
setSmartCard(ctx, cfg)

if ctx.GlobalIsSet(JWTSecretFlag.Name) {
cfg.JwtSecret = ctx.GlobalString(JWTSecretFlag.Name)
}

if ctx.GlobalIsSet(ExternalSignerFlag.Name) {
cfg.ExternalSigner = ctx.GlobalString(ExternalSignerFlag.Name)
}
9 changes: 5 additions & 4 deletions eth/catalyst/api.go
Original file line number Diff line number Diff line change
@@ -36,10 +36,11 @@ func Register(stack *node.Node, backend *eth.Ethereum) error {
log.Warn("Catalyst mode enabled", "protocol", "eth")
stack.RegisterAPIs([]rpc.API{
{
Namespace: "engine",
Version: "1.0",
Service: NewConsensusAPI(backend),
Public: true,
Namespace: "engine",
Version: "1.0",
Service: NewConsensusAPI(backend),
Public: true,
Authenticated: true,
},
})
return nil
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -27,6 +27,7 @@ require (
github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff
github.com/go-ole/go-ole v1.2.1 // indirect
github.com/go-stack/stack v1.8.0
github.com/golang-jwt/jwt/v4 v4.3.0 // indirect
github.com/golang/protobuf v1.4.3
github.com/golang/snappy v0.0.4
github.com/google/gofuzz v1.1.1-0.20200604201612-c04b05f3adfa
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -167,6 +167,10 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me
github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU=
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt/v4 v4.3.0 h1:kHL1vqdqWNfATmA0FNMdmZNMyZI1U6O31X4rlIPoBog=
github.com/golang-jwt/jwt/v4 v4.3.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
2 changes: 1 addition & 1 deletion graphql/service.go
Original file line number Diff line number Diff line change
@@ -74,7 +74,7 @@ func newHandler(stack *node.Node, backend ethapi.Backend, cors, vhosts []string)
return err
}
h := handler{Schema: s}
handler := node.NewHTTPHandlerStack(h, cors, vhosts)
handler := node.NewHTTPHandlerStack(h, cors, vhosts, nil)

stack.RegisterHandler("GraphQL UI", "/graphql/ui", GraphiQL{})
stack.RegisterHandler("GraphQL", "/graphql", handler)
9 changes: 5 additions & 4 deletions les/catalyst/api.go
Original file line number Diff line number Diff line change
@@ -34,10 +34,11 @@ func Register(stack *node.Node, backend *les.LightEthereum) error {
log.Warn("Catalyst mode enabled", "protocol", "les")
stack.RegisterAPIs([]rpc.API{
{
Namespace: "engine",
Version: "1.0",
Service: NewConsensusAPI(backend),
Public: true,
Namespace: "engine",
Version: "1.0",
Service: NewConsensusAPI(backend),
Public: true,
Authenticated: true,
},
})
return nil
5 changes: 3 additions & 2 deletions node/api.go
Original file line number Diff line number Diff line change
@@ -274,11 +274,12 @@ func (api *privateAdminAPI) StartWS(host *string, port *int, allowedOrigins *str
}

// Enable WebSocket on the server.
server := api.node.wsServerForPort(*port)
server := api.node.wsServerForPort(*port, false)
if err := server.setListenAddr(*host, *port); err != nil {
return false, err
}
if err := server.enableWS(api.node.rpcAPIs, config); err != nil {
openApis, _ := api.node.GetAPIs()
if err := server.enableWS(openApis, config); err != nil {
return false, err
}
if err := server.start(); err != nil {
4 changes: 4 additions & 0 deletions node/config.go
Original file line number Diff line number Diff line change
@@ -36,6 +36,7 @@ import (

const (
datadirPrivateKey = "nodekey" // Path within the datadir to the node's private key
datadirJWTKey = "jwtsecret" // Path within the datadir to the node's jwt secret
datadirDefaultKeyStore = "keystore" // Path within the datadir to the keystore
datadirStaticNodes = "static-nodes.json" // Path within the datadir to the static node list
datadirTrustedNodes = "trusted-nodes.json" // Path within the datadir to the trusted node list
@@ -190,6 +191,9 @@ type Config struct {

// AllowUnprotectedTxs allows non EIP-155 protected transactions to be send over RPC.
AllowUnprotectedTxs bool `toml:",omitempty"`

// JwtSecret is the hex-encoded jwt secret.
JwtSecret string `toml:",omitempty"`
MariusVanDerWijden marked this conversation as resolved.
Show resolved Hide resolved
}

// IPCEndpoint resolves an IPC endpoint based on a configured value, taking into
78 changes: 78 additions & 0 deletions node/jwt_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright 2022 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.

package node

import (
"net/http"
"strings"
"time"

"github.com/golang-jwt/jwt/v4"
)

type jwtHandler struct {
keyFunc func(token *jwt.Token) (interface{}, error)
next http.Handler
}

// newJWTHandler creates a http.Handler with jwt authentication support.
func newJWTHandler(secret []byte, next http.Handler) http.Handler {
return &jwtHandler{
keyFunc: func(token *jwt.Token) (interface{}, error) {
return secret, nil
},
next: next,
}
}

// ServeHTTP implements http.Handler
func (handler *jwtHandler) ServeHTTP(out http.ResponseWriter, r *http.Request) {
var (
strToken string
claims jwt.RegisteredClaims
)
if auth := r.Header.Get("Authorization"); strings.HasPrefix(auth, "Bearer ") {
strToken = strings.TrimPrefix(auth, "Bearer ")
}
if len(strToken) == 0 {
http.Error(out, "missing token", http.StatusForbidden)
return
}
// We explicitly set only HS256 allowed, and also disables the
// claim-check: the RegisteredClaims internally requires 'iat' to
// be no later than 'now', but we allow for a bit of drift.
token, err := jwt.ParseWithClaims(strToken, &claims, handler.keyFunc,
jwt.WithValidMethods([]string{"HS256"}),
jwt.WithoutClaimsValidation())

switch {
case err != nil:
http.Error(out, err.Error(), http.StatusForbidden)
case !token.Valid:
http.Error(out, "invalid token", http.StatusForbidden)
case !claims.VerifyExpiresAt(time.Now(), false): // optional
http.Error(out, "token is expired", http.StatusForbidden)
case claims.IssuedAt == nil:
http.Error(out, "missing issued-at", http.StatusForbidden)
case time.Since(claims.IssuedAt.Time) > 5*time.Second:
http.Error(out, "stale token", http.StatusForbidden)
case time.Until(claims.IssuedAt.Time) > 5*time.Second:
http.Error(out, "future token", http.StatusForbidden)
default:
handler.next.ServeHTTP(out, r)
}
}
Loading