Fix auth wiring: HTTP + TCP wire protocol authentication
- httpserver.nim: - Use config.jwtSecret instead of hardcoded default - Enforce auth on /query, /tables, /metrics when authEnabled=true - /auth endpoint now validates password against jwtSecret - server.nim: - Add TCP wire protocol auth (mkAuth message) - Reject queries with 401 until client authenticates with valid JWT - wire.nim: Add makeAuthOkMessage, makeAuthChallengeMessage, parseAuthMessage helpers
This commit is contained in:
@@ -46,8 +46,9 @@ proc newHttpServer*(config: BaraConfig): HttpServer =
|
|||||||
ctx.onChange = proc(ev: ChangeEvent) =
|
ctx.onChange = proc(ev: ChangeEvent) =
|
||||||
let msg = $ev.kind & " " & ev.table
|
let msg = $ev.kind & " " & ev.table
|
||||||
asyncCheck ws.broadcastToTable(ev.table, msg)
|
asyncCheck ws.broadcastToTable(ev.table, msg)
|
||||||
|
let secret = if config.jwtSecret.len > 0: config.jwtSecret else: "baradb-default-secret-change-in-production!"
|
||||||
HttpServer(config: config, running: false, db: db, ctx: ctx,
|
HttpServer(config: config, running: false, db: db, ctx: ctx,
|
||||||
secretKey: "baradb-default-secret-change-in-production!",
|
secretKey: secret,
|
||||||
metrics: Metrics(), ws: ws)
|
metrics: Metrics(), ws: ws)
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
# ----------------------------------------------------------------------
|
||||||
@@ -76,6 +77,24 @@ proc verifyToken*(server: HttpServer, tokenStr: string): (bool, string, string)
|
|||||||
except:
|
except:
|
||||||
return (false, "", "")
|
return (false, "", "")
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
# Auth helper
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
proc checkAuth(server: HttpServer, request: Request, ctx: Context): bool =
|
||||||
|
if not server.config.authEnabled:
|
||||||
|
return true
|
||||||
|
let authHeader = request.headers["Authorization"]
|
||||||
|
if authHeader.len == 0 or not authHeader.startsWith("Bearer "):
|
||||||
|
ctx.json(%*{"error": "Unauthorized"}, 401)
|
||||||
|
return false
|
||||||
|
let tokenStr = authHeader[7..^1]
|
||||||
|
let (valid, userId, role) = server.verifyToken(tokenStr)
|
||||||
|
if not valid:
|
||||||
|
ctx.json(%*{"error": "Unauthorized"}, 401)
|
||||||
|
return false
|
||||||
|
return true
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
# ----------------------------------------------------------------------
|
||||||
# Handlers
|
# Handlers
|
||||||
# ----------------------------------------------------------------------
|
# ----------------------------------------------------------------------
|
||||||
@@ -87,8 +106,11 @@ proc queryHandler(server: HttpServer): RequestHandler =
|
|||||||
server.metrics.queriesTotal += 1
|
server.metrics.queriesTotal += 1
|
||||||
|
|
||||||
# Auth check
|
# Auth check
|
||||||
let authHeader = request.headers["Authorization"]
|
if server.config.authEnabled:
|
||||||
if authHeader.len > 0 and authHeader.startsWith("Bearer "):
|
let authHeader = request.headers["Authorization"]
|
||||||
|
if authHeader.len == 0 or not authHeader.startsWith("Bearer "):
|
||||||
|
ctx.json(%*{"error": "Unauthorized"}, 401)
|
||||||
|
return
|
||||||
let tokenStr = authHeader[7..^1]
|
let tokenStr = authHeader[7..^1]
|
||||||
let (valid, userId, role) = server.verifyToken(tokenStr)
|
let (valid, userId, role) = server.verifyToken(tokenStr)
|
||||||
if not valid:
|
if not valid:
|
||||||
@@ -157,6 +179,9 @@ proc healthHandler(): RequestHandler =
|
|||||||
|
|
||||||
proc metricsHandler(server: HttpServer): RequestHandler =
|
proc metricsHandler(server: HttpServer): RequestHandler =
|
||||||
return proc(request: Request) {.gcsafe.} =
|
return proc(request: Request) {.gcsafe.} =
|
||||||
|
let ctx = newContext(request)
|
||||||
|
if not server.checkAuth(request, ctx):
|
||||||
|
return
|
||||||
let prometheus = "baradb_queries_total " & $server.metrics.queriesTotal & "\n" &
|
let prometheus = "baradb_queries_total " & $server.metrics.queriesTotal & "\n" &
|
||||||
"baradb_query_errors_total " & $server.metrics.queryErrors & "\n" &
|
"baradb_query_errors_total " & $server.metrics.queryErrors & "\n" &
|
||||||
"baradb_inserts_total " & $server.metrics.insertCount & "\n" &
|
"baradb_inserts_total " & $server.metrics.insertCount & "\n" &
|
||||||
@@ -174,6 +199,12 @@ proc authHandler(server: HttpServer): RequestHandler =
|
|||||||
return
|
return
|
||||||
|
|
||||||
let username = body["username"].getStr()
|
let username = body["username"].getStr()
|
||||||
|
let password = body["password"].getStr()
|
||||||
|
# Simple password check: must match jwtSecret when auth is enabled
|
||||||
|
if server.config.authEnabled and password != server.config.jwtSecret:
|
||||||
|
ctx.json(%*{"error": "Invalid credentials"}, 401)
|
||||||
|
return
|
||||||
|
|
||||||
let token = server.createToken(username, "user")
|
let token = server.createToken(username, "user")
|
||||||
ctx.json(%*{
|
ctx.json(%*{
|
||||||
"token": token,
|
"token": token,
|
||||||
@@ -214,6 +245,8 @@ proc tablesHandler(server: HttpServer): RequestHandler =
|
|||||||
return proc(request: Request) {.gcsafe.} =
|
return proc(request: Request) {.gcsafe.} =
|
||||||
{.cast(gcsafe).}:
|
{.cast(gcsafe).}:
|
||||||
let ctx = newContext(request)
|
let ctx = newContext(request)
|
||||||
|
if not server.checkAuth(request, ctx):
|
||||||
|
return
|
||||||
var tables = newJArray()
|
var tables = newJArray()
|
||||||
for name, tbl in server.ctx.tables:
|
for name, tbl in server.ctx.tables:
|
||||||
var cols = newJArray()
|
var cols = newJArray()
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import ../query/ast
|
|||||||
import ../query/executor
|
import ../query/executor
|
||||||
import ../storage/lsm
|
import ../storage/lsm
|
||||||
import ../core/mvcc
|
import ../core/mvcc
|
||||||
|
import jwt as jwtlib
|
||||||
|
|
||||||
type
|
type
|
||||||
Server* = ref object
|
Server* = ref object
|
||||||
@@ -216,6 +217,17 @@ proc slowQueryLog(logPath: string, query: string, durationMs: int, clientId: int
|
|||||||
f.write(line)
|
f.write(line)
|
||||||
except: discard
|
except: discard
|
||||||
|
|
||||||
|
proc verifyToken(secret, tokenStr: string): (bool, string, string) =
|
||||||
|
try:
|
||||||
|
let token = tokenStr.toJWT()
|
||||||
|
if not token.verify(secret, HS256):
|
||||||
|
return (false, "", "")
|
||||||
|
let userId = token.claims["sub"].node.str
|
||||||
|
let role = if "role" in token.claims: token.claims["role"].node.str else: "user"
|
||||||
|
return (true, userId, role)
|
||||||
|
except:
|
||||||
|
return (false, "", "")
|
||||||
|
|
||||||
proc handleClient(server: Server, client: AsyncSocket, clientId: int) {.async.} =
|
proc handleClient(server: Server, client: AsyncSocket, clientId: int) {.async.} =
|
||||||
info("Client " & $clientId & " connected")
|
info("Client " & $clientId & " connected")
|
||||||
var connCtx = cloneForConnection(server.ctx)
|
var connCtx = cloneForConnection(server.ctx)
|
||||||
@@ -223,6 +235,8 @@ proc handleClient(server: Server, client: AsyncSocket, clientId: int) {.async.}
|
|||||||
let queryTimeout = server.config.queryTimeoutMs
|
let queryTimeout = server.config.queryTimeoutMs
|
||||||
let slowThreshold = server.config.slowQueryThresholdMs
|
let slowThreshold = server.config.slowQueryThresholdMs
|
||||||
let slowLog = server.config.slowQueryLogPath
|
let slowLog = server.config.slowQueryLogPath
|
||||||
|
var authenticated = not server.config.authEnabled
|
||||||
|
let secret = if server.config.jwtSecret.len > 0: server.config.jwtSecret else: "baradb-default-secret-change-in-production!"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while true:
|
while true:
|
||||||
@@ -240,7 +254,24 @@ proc handleClient(server: Server, client: AsyncSocket, clientId: int) {.async.}
|
|||||||
if payload.len < int(header.length):
|
if payload.len < int(header.length):
|
||||||
break
|
break
|
||||||
|
|
||||||
|
if not authenticated and header.kind != mkAuth:
|
||||||
|
let err = makeErrorMessage(header.requestId, 401, "Authentication required")
|
||||||
|
await client.send(cast[string](err))
|
||||||
|
continue
|
||||||
|
|
||||||
case header.kind
|
case header.kind
|
||||||
|
of mkAuth:
|
||||||
|
let tokenStr = parseAuthMessage(cast[seq[byte]](payload))
|
||||||
|
let (valid, userId, role) = verifyToken(secret, tokenStr)
|
||||||
|
if valid:
|
||||||
|
authenticated = true
|
||||||
|
let okMsg = makeAuthOkMessage(header.requestId)
|
||||||
|
await client.send(cast[string](okMsg))
|
||||||
|
info("Client " & $clientId & " authenticated as " & userId)
|
||||||
|
else:
|
||||||
|
let err = makeErrorMessage(header.requestId, 403, "Invalid token")
|
||||||
|
await client.send(cast[string](err))
|
||||||
|
|
||||||
of mkQuery:
|
of mkQuery:
|
||||||
var pos = 0
|
var pos = 0
|
||||||
let queryStr = readString(cast[seq[byte]](payload), pos)
|
let queryStr = readString(cast[seq[byte]](payload), pos)
|
||||||
|
|||||||
@@ -312,3 +312,23 @@ proc makeCompleteMessage*(requestId: uint32, affectedRows: int): seq[byte] =
|
|||||||
payload: payload,
|
payload: payload,
|
||||||
)
|
)
|
||||||
return serializeMessage(msg)
|
return serializeMessage(msg)
|
||||||
|
|
||||||
|
proc makeAuthOkMessage*(requestId: uint32): seq[byte] =
|
||||||
|
var msg = WireMessage(
|
||||||
|
header: MessageHeader(kind: mkAuthOk, length: 0, requestId: requestId),
|
||||||
|
payload: @[],
|
||||||
|
)
|
||||||
|
return serializeMessage(msg)
|
||||||
|
|
||||||
|
proc makeAuthChallengeMessage*(requestId: uint32, challenge: string): seq[byte] =
|
||||||
|
var payload: seq[byte] = @[]
|
||||||
|
payload.writeString(challenge)
|
||||||
|
var msg = WireMessage(
|
||||||
|
header: MessageHeader(kind: mkAuthChallenge, length: uint32(payload.len), requestId: requestId),
|
||||||
|
payload: payload,
|
||||||
|
)
|
||||||
|
return serializeMessage(msg)
|
||||||
|
|
||||||
|
proc parseAuthMessage*(payload: openArray[byte]): string =
|
||||||
|
var pos = 0
|
||||||
|
return readString(payload, pos)
|
||||||
Reference in New Issue
Block a user