From e6938af437927aa1ddbee265ea2507fc15b16113 Mon Sep 17 00:00:00 2001 From: jangko Date: Mon, 18 Jul 2022 11:35:50 +0700 Subject: [PATCH] apply jwt auth to rpcHttpServer and update jwt auth of rpcWebsocketServer fixes #967 --- nimbus/nimbus.nim | 60 ++++++++++++++++++++++++--------- nimbus/rpc/jwt_auth.nim | 62 +++++++++++++++++++++------------- tests/test_jwt_auth.nim | 73 ++++++++++++++++++++++++++++++++++------- 3 files changed, 145 insertions(+), 50 deletions(-) diff --git a/nimbus/nimbus.nim b/nimbus/nimbus.nim index 99fa21c09..12dab0c91 100644 --- a/nimbus/nimbus.nim +++ b/nimbus/nimbus.nim @@ -168,9 +168,34 @@ proc localServices(nimbus: NimbusNode, conf: NimbusConf, info "metrics", registry discard setTimer(Moment.fromNow(conf.logMetricsInterval.seconds), logMetrics) discard setTimer(Moment.fromNow(conf.logMetricsInterval.seconds), logMetrics) + + # Provide JWT authentication handler for websockets + let jwtKey = block: + # Create or load shared secret + let rc = nimbus.ctx.rng.jwtSharedSecret(conf) + if rc.isErr: + error "Failed create or load shared secret", + msg = $(rc.unsafeError) # avoid side effects + quit(QuitFailure) + rc.value + + # Provide JWT authentication handler for rpcHttpServer + let httpJwtAuthHook = httpJwtAuth(jwtKey) + # Creating RPC Server if conf.rpcEnabled: - nimbus.rpcServer = newRpcHttpServer([initTAddress(conf.rpcAddress, conf.rpcPort)]) + let enableAuthHook = conf.engineApiEnabled and + conf.engineApiPort == conf.rpcPort + + let hooks = if enableAuthHook: + @[httpJwtAuthHook] + else: + @[] + + nimbus.rpcServer = newRpcHttpServer( + [initTAddress(conf.rpcAddress, conf.rpcPort)], + authHooks = hooks + ) setupCommonRpc(nimbus.ethNode, conf, nimbus.rpcServer) # Enable RPC APIs based on RPC flags and protocol flags @@ -188,23 +213,24 @@ proc localServices(nimbus: NimbusNode, conf: NimbusConf, nimbus.rpcServer.start() - # Provide JWT authentication handler for websockets - let jwtHook = block: - # Create or load shared secret - let rc = nimbus.ctx.rng.jwtSharedSecret(conf) - if rc.isErr: - error "Failed create or load shared secret", - msg = $(rc.unsafeError) # avoid side effects - quit(QuitFailure) - # Authentcation handler constructor - some(rc.value.jwtAuthHandler) + # Provide JWT authentication handler for rpcWebsocketServer + let wsJwtAuthHook = wsJwtAuth(jwtKey) # Creating Websocket RPC Server if conf.wsEnabled: + let enableAuthHook = conf.engineApiWsEnabled and + conf.engineApiWsPort == conf.wsPort + + let hooks = if enableAuthHook: + @[wsJwtAuthHook] + else: + @[] + # Construct server object nimbus.wsRpcServer = newRpcWebSocketServer( initTAddress(conf.wsAddress, conf.wsPort), - authHandler = jwtHook) + authHooks = hooks + ) setupCommonRpc(nimbus.ethNode, conf, nimbus.wsRpcServer) # Enable Websocket RPC APIs based on RPC flags and protocol flags @@ -260,9 +286,10 @@ proc localServices(nimbus: NimbusNode, conf: NimbusConf, if conf.engineApiEnabled: if conf.engineApiPort != conf.rpcPort: - nimbus.engineApiServer = newRpcHttpServer([ - initTAddress(conf.engineApiAddress, conf.engineApiPort) - ]) + nimbus.engineApiServer = newRpcHttpServer( + [initTAddress(conf.engineApiAddress, conf.engineApiPort)], + authHooks = @[httpJwtAuthHook] + ) setupEngineAPI(nimbus.sealingEngine, nimbus.engineApiServer) setupEthRpc(nimbus.ethNode, nimbus.ctx, chainDB, nimbus.txPool, nimbus.engineApiServer) nimbus.engineApiServer.start() @@ -275,7 +302,8 @@ proc localServices(nimbus: NimbusNode, conf: NimbusConf, if conf.engineApiWsPort != conf.wsPort: nimbus.engineApiWsServer = newRpcWebSocketServer( initTAddress(conf.engineApiWsAddress, conf.engineApiWsPort), - authHandler = jwtHook) + authHooks = @[wsJwtAuthHook] + ) setupEngineAPI(nimbus.sealingEngine, nimbus.engineApiWsServer) setupEthRpc(nimbus.ethNode, nimbus.ctx, chainDB, nimbus.txPool, nimbus.engineApiWsServer) nimbus.engineApiWsServer.start() diff --git a/nimbus/rpc/jwt_auth.nim b/nimbus/rpc/jwt_auth.nim index 44457b102..4f4c6e35d 100644 --- a/nimbus/rpc/jwt_auth.nim +++ b/nimbus/rpc/jwt_auth.nim @@ -17,10 +17,10 @@ import bearssl/rand, chronicles, chronos, - chronos/apps/http/httptable, - json_rpc/servers/websocketserver, + chronos/apps/http/[httptable, httpserver], + json_rpc/rpcserver, httputils, - websock/types as ws, + websock/websock as ws, nimcrypto/[hmac, utils], stew/[byteutils, objects, results], ../config @@ -40,10 +40,6 @@ const 32 type - JwtAuthHandler* = ##\ - ## Generic authentication handler, also provided by the web-socket server. - RpcWebSocketServerAuth - JwtSharedKey* = ##\ ## Convenience type, needed quite often distinct array[jwtMinSecretLen,byte] @@ -183,7 +179,7 @@ proc fromHex*(key: var JwtSharedKey, src: string): Result[void,JwtError] = except ValueError: err(jwtKeyInvalidHexString) -proc jwtGenSecret*(rng: ref HmacDrbgContext): JwtGenSecret = +proc jwtGenSecret*(rng: ref rand.HmacDrbgContext): JwtGenSecret = ## Standard shared key random generator. If a fixed key is needed, a ## function like ## :: @@ -255,37 +251,57 @@ proc jwtSharedSecret*(rndSecret: JwtGenSecret; config: NimbusConf): except ValueError: return err(jwtKeyInvalidHexString) -proc jwtSharedSecret*(rng: ref HmacDrbgContext; config: NimbusConf): +proc jwtSharedSecret*(rng: ref rand.HmacDrbgContext; config: NimbusConf): Result[JwtSharedKey, JwtError] {.gcsafe, raises: [Defect,JwtExcept].} = ## Variant of `jwtSharedSecret()` with explicit random generator argument. safeExecutor("jwtSharedSecret"): result = rng.jwtGenSecret.jwtSharedSecret(config) - -proc jwtAuthHandler*(key: JwtSharedKey): JwtAuthHandler = - ## Returns a JWT authentication handler that can be used with an HTTP header - ## based call back system as the web socket server. - ## - ## The argument `key` is captured by the session handler for JWT - ## authentication. The function `jwtSharedSecret()` provides such a key. - result = proc(req: HttpTable): Result[void,(HttpCode,string)] {.gcsafe.} = - let auth = req.getString("Authorization","?") +proc httpJwtAuth*(key: JwtSharedKey): HttpAuthHook = + proc handler(req: HttpRequestRef): Future[HttpResponseRef] {.async.} = + let auth = req.headers.getString("Authorization", "?") if auth.len < 9 or auth[0..6].cmpIgnoreCase("Bearer ") != 0: - return err((Http403, "Missing Token")) + return await req.respond(Http403, "Missing authorization token") let rc = auth[7..^1].strip.verifyTokenHS256(key) if rc.isOk: - return ok() + return HttpResponseRef(nil) debug "Could not authenticate", - error = rc.error + error = rc.error case rc.error: of jwtTokenValidationError, jwtMethodUnsupported: - return err((Http401, "Unauthorized")) + return await req.respond(Http401, "Unauthorized access") else: - return err((Http403, "Malformed Token")) + return await req.respond(Http403, "Malformed token") + + result = HttpAuthHook(handler) + +proc wsJwtAuth*(key: JwtSharedKey): WsAuthHook = + proc handler(req: ws.HttpRequest): Future[bool] {.async.} = + let auth = req.headers.getString("Authorization", "?") + if auth.len < 9 or auth[0..6].cmpIgnoreCase("Bearer ") != 0: + await req.sendResponse(code = Http403, data = "Missing authorization token") + return false + + let rc = auth[7..^1].strip.verifyTokenHS256(key) + if rc.isOk: + return true + + debug "Could not authenticate", + error = rc.error + + case rc.error: + of jwtTokenValidationError, jwtMethodUnsupported: + await req.sendResponse(code = Http403, data = "Unauthorized access") + else: + await req.sendResponse(code = Http403, data = "Malformed token") + + return false + + result = WsAuthHook(handler) # ------------------------------------------------------------------------------ # End diff --git a/tests/test_jwt_auth.nim b/tests/test_jwt_auth.nim index eae503714..ced3c520c 100644 --- a/tests/test_jwt_auth.nim +++ b/tests/test_jwt_auth.nim @@ -18,13 +18,16 @@ import ./replay/pp, confutils/defs, chronicles, - chronos/apps/http/httpserver, + chronos/apps/http/httpclient as chronoshttpclient, + chronos/apps/http/httptable, eth/[common, keys, p2p], json_rpc/rpcserver, nimcrypto/[hmac, utils], stew/results, stint, - unittest2 + unittest2, + graphql, + graphql/[httpserver, httpclient] type UnGuardedKey = @@ -113,6 +116,32 @@ proc getHttpAuthReqHeader2(secret: JwtSharedKey; time: uint64): HttpTable = let bearer = secret.UnGuardedKey.getSignedToken2($getIatToken(time)) result.add("aUtHoRiZaTiOn", "Bearer " & bearer) +proc createServer(serverAddress: TransportAddress, authHooks: seq[HttpAuthHook] = @[]): GraphqlHttpServerRef = + let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr} + var ctx = GraphqlRef.new() + + const schema = """type Query {name: String}""" + let r = ctx.parseSchema(schema) + if r.isErr: + debugEcho r.error + return + + let res = GraphqlHttpServerRef.new( + graphql = ctx, + address = serverAddress, + socketFlags = socketFlags, + authHooks = authHooks + ) + + if res.isErr(): + debugEcho res.error + return + + res.get() + +proc setupClient(address: TransportAddress): GraphqlHttpClientRef = + GraphqlHttpClientRef.new(address, secure = false).get() + # ------------------------------------------------------------------------------ # Test Runners # ------------------------------------------------------------------------------ @@ -224,9 +253,15 @@ proc runJwtAuth(noisy = true; keyFile = jwtKeyFile) = secret = fakeKey.fakeGenSecret.jwtSharedSecret(config) # The wrapper contains the handler function with the captured shared key - handler = secret.value.jwtAuthHandler + authHook = secret.value.httpJwtAuth + + const + serverAddress = initTAddress("127.0.0.1:8547") + query = """{ __type(name: "ID") { kind }}""" suite "EngineAuth: Http/rpc authentication mechanics": + let server = createServer(serverAddress, @[authHook]) + server.start() test &"JSW/HS256 authentication using shared secret file {fileInfo}": # Just to make sure that we made a proper choice. Typically, all @@ -249,11 +284,18 @@ proc runJwtAuth(noisy = true; keyFile = jwtKeyFile) = setTraceLevel() # Run http authorisation request - let htCode = req.handler - noisy.say "***", "result", - " htCode=", htCode + let client = setupClient(serverAddress) + let res = waitFor client.sendRequest(query, req.toList) + check res.isOk + if res.isErr: + noisy.say "***", res.error + return + + let resp = res.get() + check resp.status == 200 + check resp.reason == "OK" + check resp.response == """{"data":{"__type":{"kind":"SCALAR"}}}""" - check htCode.isOk setErrorLevel() test &"JSW/HS256, ditto with protected header variant": @@ -268,13 +310,22 @@ proc runJwtAuth(noisy = true; keyFile = jwtKeyFile) = setTraceLevel() # Run http authorisation request - let htCode = req.handler - noisy.say "***", "result", - " htCode=", htCode + let client = setupClient(serverAddress) + let res = waitFor client.sendRequest(query, req.toList) + check res.isOk + if res.isErr: + noisy.say "***", res.error + return + + let resp = res.get() + check resp.status == 200 + check resp.reason == "OK" + check resp.response == """{"data":{"__type":{"kind":"SCALAR"}}}""" - check htCode.isOk setErrorLevel() + waitFor server.closeWait() + # ------------------------------------------------------------------------------ # Main function(s) # ------------------------------------------------------------------------------