Summary
Remote node connections fail because the node client connects to /ws, but the gateway only registers /ws/chat as its WebSocket route. Additionally, the HTTP auth middleware (auth_gate) blocks the WebSocket upgrade before the connection reaches the WebSocket handler, which has its own device-token authentication logic.
Steps to Reproduce
- Set up gateway on machine A with auth configured (password/passkey)
- On machine B, run
moltis node add --host ws://<gateway-host>:<port>/ws --token <device-token>
- Start the node service on machine B
- Observe that the node never appears as connected in the gateway UI
Expected Behavior
The node connects to the gateway via WebSocket, authenticates with its device token, and appears in the Nodes list.
Actual Behavior
The connection fails at the HTTP layer before the WebSocket upgrade. The gateway returns a 303 redirect (to /login) because:
-
No /ws route exists — The gateway only registers /ws/chat as its WebSocket endpoint (in build_gateway_base() in server.rs), but the node client connects to /ws.
-
Auth middleware blocks device-token connections — Even if the node connected to /ws/chat, the auth_gate middleware in auth_middleware.rs would reject it. The middleware's check_auth() function only validates session cookies and Bearer API keys. It does not check device tokens. Since the node client sends no cookies or API keys in the HTTP upgrade request, auth_gate returns Unauthorized and the request never reaches the WebSocket handler.
-
The WebSocket handler already supports device-token auth — ws.rs handle_connection() has complete device-token verification logic: it checks params.auth.device_token against the pairing store, assigns the NODE role, and registers the node. But this code is unreachable because the HTTP middleware rejects the request first.
Root Cause Analysis
There's an auth layering mismatch between the HTTP middleware and the WebSocket protocol:
- HTTP layer (
auth_middleware.rs → check_auth()): Checks session cookies and API keys only. No device token support.
- WebSocket layer (
ws.rs → handle_connection()): Checks device tokens, API keys, passwords, and legacy tokens. Full auth logic for nodes.
The node client's auth flow is designed as a two-phase process:
- HTTP WebSocket upgrade (no auth headers)
- JSON
connect message with device_token field (post-upgrade, at the application protocol level)
But phase 1 is blocked by the HTTP auth middleware, so phase 2 never executes.
Code References
Gateway route registration (server.rs → build_gateway_base())
Only /ws/chat is registered — no /ws route:
let mut router = Router::new()
.route("/health", get(health_handler))
.route("/ws/chat", get(ws_upgrade_handler));
Auth middleware (auth_middleware.rs)
is_public_path() does not include /ws or /ws/chat:
fn is_public_path(path: &str) -> bool {
matches!(
path,
"/health" | "/auth/callback" | "/manifest.json" | "/sw.js" | "/login" | "/setup-required"
) || path.starts_with("/api/auth/")
|| path.starts_with("/api/public/")
|| path.starts_with("/api/channels/msteams/")
|| path.starts_with("/assets/")
|| path.starts_with("/share/")
}
check_auth() checks cookies and API keys but not device tokens:
pub async fn check_auth(store: &CredentialStore, headers: &HeaderMap, is_local: bool) -> AuthResult {
// ... checks session cookie ...
// ... checks Bearer API key ...
// No device token check
AuthResult::Unauthorized
}
WebSocket handler (ws.rs → handle_connection())
Device token auth exists but is unreachable for remote nodes:
// Check device token first (used by paired nodes).
if !authenticated
&& let Some(ref dt) = params.auth.as_ref().and_then(|a| a.device_token.clone())
&& let Some(ref store) = state.pairing_store
{
match store.verify_device_token(dt).await {
Ok(Some(verification)) => {
authenticated = true;
// ... assigns NODE role, registers node ...
},
// ...
}
}
Suggested Fix
Add a /ws route and make it a public path so node connections bypass auth_gate:
- Add
/ws route in server.rs build_gateway_base():
let mut router = Router::new()
.route("/health", get(health_handler))
.route("/ws/chat", get(ws_upgrade_handler))
.route("/ws", get(ws_upgrade_handler)); // Node connections
- Add
/ws to is_public_path() in auth_middleware.rs:
fn is_public_path(path: &str) -> bool {
matches!(
path,
"/health" | "/auth/callback" | "/manifest.json" | "/sw.js" | "/login" | "/setup-required" | "/ws"
) || // ...
}
This is safe because:
- The WebSocket handler (
ws.rs) already has complete auth logic for all connection types
- Invalid device tokens are rejected and the connection is closed immediately
- Browser clients continue to use
/ws/chat behind cookie auth as before
/ws becomes the dedicated node entry point with device-token-only auth at the protocol level
Related
Summary
Remote node connections fail because the node client connects to
/ws, but the gateway only registers/ws/chatas its WebSocket route. Additionally, the HTTP auth middleware (auth_gate) blocks the WebSocket upgrade before the connection reaches the WebSocket handler, which has its own device-token authentication logic.Steps to Reproduce
moltis node add --host ws://<gateway-host>:<port>/ws --token <device-token>Expected Behavior
The node connects to the gateway via WebSocket, authenticates with its device token, and appears in the Nodes list.
Actual Behavior
The connection fails at the HTTP layer before the WebSocket upgrade. The gateway returns a 303 redirect (to
/login) because:No
/wsroute exists — The gateway only registers/ws/chatas its WebSocket endpoint (inbuild_gateway_base()inserver.rs), but the node client connects to/ws.Auth middleware blocks device-token connections — Even if the node connected to
/ws/chat, theauth_gatemiddleware inauth_middleware.rswould reject it. The middleware'scheck_auth()function only validates session cookies and Bearer API keys. It does not check device tokens. Since the node client sends no cookies or API keys in the HTTP upgrade request,auth_gatereturnsUnauthorizedand the request never reaches the WebSocket handler.The WebSocket handler already supports device-token auth —
ws.rshandle_connection()has complete device-token verification logic: it checksparams.auth.device_tokenagainst the pairing store, assigns theNODErole, and registers the node. But this code is unreachable because the HTTP middleware rejects the request first.Root Cause Analysis
There's an auth layering mismatch between the HTTP middleware and the WebSocket protocol:
auth_middleware.rs→check_auth()): Checks session cookies and API keys only. No device token support.ws.rs→handle_connection()): Checks device tokens, API keys, passwords, and legacy tokens. Full auth logic for nodes.The node client's auth flow is designed as a two-phase process:
connectmessage withdevice_tokenfield (post-upgrade, at the application protocol level)But phase 1 is blocked by the HTTP auth middleware, so phase 2 never executes.
Code References
Gateway route registration (
server.rs→build_gateway_base())Only
/ws/chatis registered — no/wsroute:Auth middleware (
auth_middleware.rs)is_public_path()does not include/wsor/ws/chat:check_auth()checks cookies and API keys but not device tokens:WebSocket handler (
ws.rs→handle_connection())Device token auth exists but is unreachable for remote nodes:
Suggested Fix
Add a
/wsroute and make it a public path so node connections bypassauth_gate:/wsroute inserver.rsbuild_gateway_base():/wstois_public_path()inauth_middleware.rs:This is safe because:
ws.rs) already has complete auth logic for all connection types/ws/chatbehind cookie auth as before/wsbecomes the dedicated node entry point with device-token-only auth at the protocol levelRelated