Authenticate

Although the original protocol does not support it, we have added authentication over TCP for InfluxDB line protocol. This works by using an elliptic curve P-256 JSON Web Token (JWT) to sign a server challenge. This page shows how to authenticate clients with QuestDB when using InfluxDB line protocol for the TCP endpoint.

Prerequisites#

QuestDB should be running and accessible and can be started via Docker, the binaries or Homebrew for macOS users.

The jose package is a C-language implementation of the Javascript Object Signing and Encryption standard and may be used for convenience to generate cryptographic keys. It's also recommended to install jq for parsing the JSON output from the keys generated by jose

brew install jose
brew install jq

Server configuration#

In order to use this feature, you need to create an authentication file using the following template:

testUser1 ec-p-256-sha256 fLKYEaoEb9lrn3nkwLDA-M_xnuFOdSt9y0Z7_vWSHLU Dt5tbS1dEDMSYfym3fgMv0B99szno-dFc1rYF9t0aac
# [key/user id] [key type] {keyX keyY}

Only elliptic curve (P-256) are supported (key type ec-p-256-sha256). An authentication file can be generated using the jose utility with the following command.

jose jwk gen -i '{"alg":"ES256", "kid": "testUser1"}' -o /var/lib/questdb/conf/full_auth.json
KID=$(cat /var/lib/questdb/conf/full_auth.json | jq -r '.kid')
X=$(cat /var/lib/questdb/conf/full_auth.json | jq -r '.x')
Y=$(cat /var/lib/questdb/conf/full_auth.json | jq -r '.y')
echo "$KID ec-p-256-sha256 $X $Y" | tee /var/lib/questdb/conf/auth.txt

Once you created the file, you will need to reference it in the server configuration:

/path/to/server.conf
line.tcp.auth.db.path=conf/auth.txt

Client keys#

For the server configuration above, the corresponding JSON Web Key must be stored on the client side. When sending a fully-composed JWK, it will have the following keys:

{
"kty": "EC",
"d": "5UjEMuA0Pj5pjK8a-fa24dyIf-Es5mYny3oE_Wmus48",
"crv": "P-256",
"kid": "testUser1",
"x": "fLKYEaoEb9lrn3nkwLDA-M_xnuFOdSt9y0Z7_vWSHLU",
"y": "Dt5tbS1dEDMSYfym3fgMv0B99szno-dFc1rYF9t0aac"
}

For this kind of key, the d property is used to generate the the secret key. The x and y parameters are used to generate the public key (values that we retrieve in the server authentication file).

Authentication#

The server will now expect the client to send its key id (terminated with \n) straight after connect(). The server will respond with a challenge (printable characters terminated with \n). The client needs to sign the challenge and respond to the server with the base64 encoded signature (terminated with \n). If all is good the client can then continue, if not the server will disconnect and log the failure.

const { Socket } = require("net")
const { Crypto } = require("node-webcrypto-ossl")
const crypto = new Crypto()
const PORT = 9009
const HOST = "localhost"
const PRIVATE_KEY = "5UjEMuA0Pj5pjK8a-fa24dyIf-Es5mYny3oE_Wmus48"
const PUBLIC_KEY = {
x: "fLKYEaoEb9lrn3nkwLDA-M_xnuFOdSt9y0Z7_vWSHLU",
y: "Dt5tbS1dEDMSYfym3fgMv0B99szno-dFc1rYF9t0aac",
}
const JWK = {
...PUBLIC_KEY,
kid: "testUser1",
kty: "EC",
d: PRIVATE_KEY,
crv: "P-256",
}
const client = new Socket()
async function write(data) {
return new Promise((resolve) => {
client.write(data, () => {
resolve()
})
})
}
async function authenticate(challenge) {
// Check for trailing \n which ends the challenge
if (challenge.slice(-1).readInt8() === 10) {
const apiKey = await crypto.subtle.importKey(
"jwk",
JWK,
{ name: "ECDSA", namedCurve: "P-256" },
true,
["sign"],
)
const signature = await crypto.subtle.sign(
{ name: "ECDSA", hash: "SHA-256" },
apiKey,
challenge.slice(0, challenge.length - 1),
)
await write(`${Buffer.from(signature).toString("base64")}\n`)
return true
}
return false
}
async function sendData() {
const rows = [
`test,location=us temperature=22.4 ${Date.now() * 1e6}`,
`test,location=us temperature=21.4 ${Date.now() * 1e6}`,
]
for (row of rows) {
await write(`${row}\n`)
}
}
async function run() {
let authenticated = false
let data
client.on("data", async function (raw) {
data = !data ? raw : Buffer.concat([data, raw])
if (!authenticated) {
authenticated = await authenticate(data)
await sendData()
setTimeout(() => {
client.destroy()
}, 0)
}
})
client.on("ready", async function () {
await write(`${JWK.kid}\n`)
})
client.connect(PORT, HOST)
}
run()