apply jwt auth to rpcHttpServer and update jwt auth of rpcWebsocketServer

fixes #967
This commit is contained in:
jangko 2022-07-18 11:35:50 +07:00
parent 6cfaaf5b45
commit e6938af437
No known key found for this signature in database
GPG Key ID: 31702AE10541E6B9
3 changed files with 145 additions and 50 deletions

View File

@ -168,9 +168,34 @@ proc localServices(nimbus: NimbusNode, conf: NimbusConf,
info "metrics", registry info "metrics", registry
discard setTimer(Moment.fromNow(conf.logMetricsInterval.seconds), logMetrics) discard setTimer(Moment.fromNow(conf.logMetricsInterval.seconds), logMetrics)
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 # Creating RPC Server
if conf.rpcEnabled: 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) setupCommonRpc(nimbus.ethNode, conf, nimbus.rpcServer)
# Enable RPC APIs based on RPC flags and protocol flags # Enable RPC APIs based on RPC flags and protocol flags
@ -188,23 +213,24 @@ proc localServices(nimbus: NimbusNode, conf: NimbusConf,
nimbus.rpcServer.start() nimbus.rpcServer.start()
# Provide JWT authentication handler for websockets # Provide JWT authentication handler for rpcWebsocketServer
let jwtHook = block: let wsJwtAuthHook = wsJwtAuth(jwtKey)
# 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)
# Creating Websocket RPC Server # Creating Websocket RPC Server
if conf.wsEnabled: if conf.wsEnabled:
let enableAuthHook = conf.engineApiWsEnabled and
conf.engineApiWsPort == conf.wsPort
let hooks = if enableAuthHook:
@[wsJwtAuthHook]
else:
@[]
# Construct server object # Construct server object
nimbus.wsRpcServer = newRpcWebSocketServer( nimbus.wsRpcServer = newRpcWebSocketServer(
initTAddress(conf.wsAddress, conf.wsPort), initTAddress(conf.wsAddress, conf.wsPort),
authHandler = jwtHook) authHooks = hooks
)
setupCommonRpc(nimbus.ethNode, conf, nimbus.wsRpcServer) setupCommonRpc(nimbus.ethNode, conf, nimbus.wsRpcServer)
# Enable Websocket RPC APIs based on RPC flags and protocol flags # 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.engineApiEnabled:
if conf.engineApiPort != conf.rpcPort: if conf.engineApiPort != conf.rpcPort:
nimbus.engineApiServer = newRpcHttpServer([ nimbus.engineApiServer = newRpcHttpServer(
initTAddress(conf.engineApiAddress, conf.engineApiPort) [initTAddress(conf.engineApiAddress, conf.engineApiPort)],
]) authHooks = @[httpJwtAuthHook]
)
setupEngineAPI(nimbus.sealingEngine, nimbus.engineApiServer) setupEngineAPI(nimbus.sealingEngine, nimbus.engineApiServer)
setupEthRpc(nimbus.ethNode, nimbus.ctx, chainDB, nimbus.txPool, nimbus.engineApiServer) setupEthRpc(nimbus.ethNode, nimbus.ctx, chainDB, nimbus.txPool, nimbus.engineApiServer)
nimbus.engineApiServer.start() nimbus.engineApiServer.start()
@ -275,7 +302,8 @@ proc localServices(nimbus: NimbusNode, conf: NimbusConf,
if conf.engineApiWsPort != conf.wsPort: if conf.engineApiWsPort != conf.wsPort:
nimbus.engineApiWsServer = newRpcWebSocketServer( nimbus.engineApiWsServer = newRpcWebSocketServer(
initTAddress(conf.engineApiWsAddress, conf.engineApiWsPort), initTAddress(conf.engineApiWsAddress, conf.engineApiWsPort),
authHandler = jwtHook) authHooks = @[wsJwtAuthHook]
)
setupEngineAPI(nimbus.sealingEngine, nimbus.engineApiWsServer) setupEngineAPI(nimbus.sealingEngine, nimbus.engineApiWsServer)
setupEthRpc(nimbus.ethNode, nimbus.ctx, chainDB, nimbus.txPool, nimbus.engineApiWsServer) setupEthRpc(nimbus.ethNode, nimbus.ctx, chainDB, nimbus.txPool, nimbus.engineApiWsServer)
nimbus.engineApiWsServer.start() nimbus.engineApiWsServer.start()

View File

@ -17,10 +17,10 @@ import
bearssl/rand, bearssl/rand,
chronicles, chronicles,
chronos, chronos,
chronos/apps/http/httptable, chronos/apps/http/[httptable, httpserver],
json_rpc/servers/websocketserver, json_rpc/rpcserver,
httputils, httputils,
websock/types as ws, websock/websock as ws,
nimcrypto/[hmac, utils], nimcrypto/[hmac, utils],
stew/[byteutils, objects, results], stew/[byteutils, objects, results],
../config ../config
@ -40,10 +40,6 @@ const
32 32
type type
JwtAuthHandler* = ##\
## Generic authentication handler, also provided by the web-socket server.
RpcWebSocketServerAuth
JwtSharedKey* = ##\ JwtSharedKey* = ##\
## Convenience type, needed quite often ## Convenience type, needed quite often
distinct array[jwtMinSecretLen,byte] distinct array[jwtMinSecretLen,byte]
@ -183,7 +179,7 @@ proc fromHex*(key: var JwtSharedKey, src: string): Result[void,JwtError] =
except ValueError: except ValueError:
err(jwtKeyInvalidHexString) 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 ## Standard shared key random generator. If a fixed key is needed, a
## function like ## function like
## :: ## ::
@ -255,37 +251,57 @@ proc jwtSharedSecret*(rndSecret: JwtGenSecret; config: NimbusConf):
except ValueError: except ValueError:
return err(jwtKeyInvalidHexString) return err(jwtKeyInvalidHexString)
proc jwtSharedSecret*(rng: ref HmacDrbgContext; config: NimbusConf): proc jwtSharedSecret*(rng: ref rand.HmacDrbgContext; config: NimbusConf):
Result[JwtSharedKey, JwtError] Result[JwtSharedKey, JwtError]
{.gcsafe, raises: [Defect,JwtExcept].} = {.gcsafe, raises: [Defect,JwtExcept].} =
## Variant of `jwtSharedSecret()` with explicit random generator argument. ## Variant of `jwtSharedSecret()` with explicit random generator argument.
safeExecutor("jwtSharedSecret"): safeExecutor("jwtSharedSecret"):
result = rng.jwtGenSecret.jwtSharedSecret(config) result = rng.jwtGenSecret.jwtSharedSecret(config)
proc httpJwtAuth*(key: JwtSharedKey): HttpAuthHook =
proc jwtAuthHandler*(key: JwtSharedKey): JwtAuthHandler = proc handler(req: HttpRequestRef): Future[HttpResponseRef] {.async.} =
## Returns a JWT authentication handler that can be used with an HTTP header let auth = req.headers.getString("Authorization", "?")
## 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","?")
if auth.len < 9 or auth[0..6].cmpIgnoreCase("Bearer ") != 0: 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) let rc = auth[7..^1].strip.verifyTokenHS256(key)
if rc.isOk: if rc.isOk:
return ok() return HttpResponseRef(nil)
debug "Could not authenticate", debug "Could not authenticate",
error = rc.error error = rc.error
case rc.error: case rc.error:
of jwtTokenValidationError, jwtMethodUnsupported: of jwtTokenValidationError, jwtMethodUnsupported:
return err((Http401, "Unauthorized")) return await req.respond(Http401, "Unauthorized access")
else: 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 # End

View File

@ -18,13 +18,16 @@ import
./replay/pp, ./replay/pp,
confutils/defs, confutils/defs,
chronicles, chronicles,
chronos/apps/http/httpserver, chronos/apps/http/httpclient as chronoshttpclient,
chronos/apps/http/httptable,
eth/[common, keys, p2p], eth/[common, keys, p2p],
json_rpc/rpcserver, json_rpc/rpcserver,
nimcrypto/[hmac, utils], nimcrypto/[hmac, utils],
stew/results, stew/results,
stint, stint,
unittest2 unittest2,
graphql,
graphql/[httpserver, httpclient]
type type
UnGuardedKey = UnGuardedKey =
@ -113,6 +116,32 @@ proc getHttpAuthReqHeader2(secret: JwtSharedKey; time: uint64): HttpTable =
let bearer = secret.UnGuardedKey.getSignedToken2($getIatToken(time)) let bearer = secret.UnGuardedKey.getSignedToken2($getIatToken(time))
result.add("aUtHoRiZaTiOn", "Bearer " & bearer) 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 # Test Runners
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
@ -224,9 +253,15 @@ proc runJwtAuth(noisy = true; keyFile = jwtKeyFile) =
secret = fakeKey.fakeGenSecret.jwtSharedSecret(config) secret = fakeKey.fakeGenSecret.jwtSharedSecret(config)
# The wrapper contains the handler function with the captured shared key # 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": suite "EngineAuth: Http/rpc authentication mechanics":
let server = createServer(serverAddress, @[authHook])
server.start()
test &"JSW/HS256 authentication using shared secret file {fileInfo}": test &"JSW/HS256 authentication using shared secret file {fileInfo}":
# Just to make sure that we made a proper choice. Typically, all # Just to make sure that we made a proper choice. Typically, all
@ -249,11 +284,18 @@ proc runJwtAuth(noisy = true; keyFile = jwtKeyFile) =
setTraceLevel() setTraceLevel()
# Run http authorisation request # Run http authorisation request
let htCode = req.handler let client = setupClient(serverAddress)
noisy.say "***", "result", let res = waitFor client.sendRequest(query, req.toList)
" htCode=", htCode 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() setErrorLevel()
test &"JSW/HS256, ditto with protected header variant": test &"JSW/HS256, ditto with protected header variant":
@ -268,13 +310,22 @@ proc runJwtAuth(noisy = true; keyFile = jwtKeyFile) =
setTraceLevel() setTraceLevel()
# Run http authorisation request # Run http authorisation request
let htCode = req.handler let client = setupClient(serverAddress)
noisy.say "***", "result", let res = waitFor client.sendRequest(query, req.toList)
" htCode=", htCode 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() setErrorLevel()
waitFor server.closeWait()
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# Main function(s) # Main function(s)
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------