Move the edge credential out of the hello message
The `hello` frame carried a `token` field holding the edge JWT. It is gone: the token now travels in an `Authorization: Bearer` header on the WebSocket upgrade request. A token in the first frame means the backend has already accepted a socket, allocated state for it and run its JSON through this schema and a switch statement for a peer that has proved nothing. Every message type refused to act without a verified farm, so nothing could be done with such a connection - but "it cannot do anything" is a weaker guarantee than "it does not exist", and it needed a timeout to stop anyone holding sockets open for free. Verified at the handshake, an unauthenticated peer gets an HTTP 401 and no socket at all. The alternative of a `?token=` query parameter was rejected outright: a bearer credential in a URL lands in access logs, proxy logs and error breadcrumbs, which is exactly what continuum-proxy's hand-written `Debug` for `PrinterCommand` exists to avoid for presigned URLs. `Authorization` is the header every log tool already knows to redact. `farm_id` stays on the message even though the token carries the same claim. It is checked against the token rather than trusted, so sending it is how a misconfigured gateway gets caught instead of silently working against the wrong farm. A browser cannot set headers on a WebSocket, so a future user-facing socket cannot copy this exactly; its equivalent is the subprotocol field, `new WebSocket(url, ["bearer", token])`, which is also verified at the upgrade. Breaking change to the edge protocol, which is free right now: nothing is deployed. `npm run typecheck` and `npm run build` clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -166,7 +166,16 @@ export const EdgeClientMessageSchema = Type.Union([
|
||||
type: Type.Literal("hello"),
|
||||
gateway_id: Type.String(),
|
||||
farm_id: Type.String(),
|
||||
token: Type.String(),
|
||||
/*
|
||||
* No credential here, deliberately. The edge JWT travels in an
|
||||
* `Authorization: Bearer` header on the upgrade request and is verified
|
||||
* before the socket exists. A `token` field used to live on this
|
||||
* message; it was removed, because a token in the first frame means a
|
||||
* live socket and a parsed message for a peer that has proved nothing.
|
||||
*
|
||||
* `farm_id` is redundant with the token's own claim, and is checked
|
||||
* against it rather than trusted.
|
||||
*/
|
||||
/**
|
||||
* Where this gateway serves camera restreams, e.g.
|
||||
* `http://gateway.local:1984`. Omitted by a gateway with no cameras or
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
{
|
||||
"type": "object",
|
||||
"title": "Hello",
|
||||
"description": "First message on every connection, before anything else is accepted. Carries the edge JWT minted by POST /auth/edge-token.",
|
||||
"description": "First message on every connection, before anything else is accepted. Carries NO credential: the edge JWT minted by POST /auth/edge-token goes in an Authorization: Bearer header on the WebSocket upgrade request, and is verified before the socket exists, so an unauthenticated peer gets an HTTP 401 and no socket at all. This message used to carry a 'token' field; it was removed, because a token in the first frame means a live socket and a parsed message for a peer that has proved nothing. farm_id here is redundant with the token's own claim and is checked against it rather than trusted - a gateway naming a farm its token was not minted for is refused.",
|
||||
"properties": {
|
||||
"type": {
|
||||
"const": "hello"
|
||||
@@ -18,9 +18,6 @@
|
||||
"farm_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"token": {
|
||||
"type": "string"
|
||||
},
|
||||
"stream_base_url": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
@@ -30,8 +27,7 @@
|
||||
"required": [
|
||||
"type",
|
||||
"gateway_id",
|
||||
"farm_id",
|
||||
"token"
|
||||
"farm_id"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user