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
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()

View File

@ -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

View File

@ -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)
# ------------------------------------------------------------------------------