From 68ca8122674e633d84a84d75665fa90e85e6b085 Mon Sep 17 00:00:00 2001 From: Sanju Sivalingam Date: Wed, 18 Feb 2026 11:46:48 +0530 Subject: [PATCH] revert(server): use direct DB queries for all auth validation Reverts middleware and dashboard WS to direct DB session lookups. Replaces auth.api.verifyApiKey in device WS with direct DB query using SHA-256 hash matching, removing dependency on BETTER_AUTH_SECRET for auth validation. --- server/src/middleware/auth.ts | 45 ++++++++++++++++++++++++++++++----- server/src/ws/dashboard.ts | 18 ++++++++------ server/src/ws/device.ts | 44 +++++++++++++++++++++++++++------- 3 files changed, 86 insertions(+), 21 deletions(-) diff --git a/server/src/middleware/auth.ts b/server/src/middleware/auth.ts index 1323426..06b6142 100644 --- a/server/src/middleware/auth.ts +++ b/server/src/middleware/auth.ts @@ -1,5 +1,8 @@ import type { Context, Next } from "hono"; -import { auth } from "../auth.js"; +import { db } from "../db.js"; +import { session as sessionTable, user as userTable } from "../schema.js"; +import { eq } from "drizzle-orm"; +import { getCookie } from "hono/cookie"; /** Hono Env type for routes protected by sessionMiddleware */ export type AuthEnv = { @@ -10,13 +13,43 @@ export type AuthEnv = { }; export async function sessionMiddleware(c: Context, next: Next) { - const session = await auth.api.getSession({ headers: c.req.raw.headers }); - - if (!session) { + // Extract session token from cookie (same approach as dashboard WS auth) + const rawCookie = getCookie(c, "better-auth.session_token"); + if (!rawCookie) { return c.json({ error: "unauthorized" }, 401); } - c.set("user", session.user); - c.set("session", session.session); + // Token may have a signature appended after a dot — use only the token part + const token = rawCookie.split(".")[0]; + + // Direct DB lookup (proven to work, unlike auth.api.getSession) + const rows = await db + .select({ + sessionId: sessionTable.id, + userId: sessionTable.userId, + }) + .from(sessionTable) + .where(eq(sessionTable.token, token)) + .limit(1); + + if (rows.length === 0) { + return c.json({ error: "unauthorized" }, 401); + } + + const { sessionId, userId } = rows[0]; + + // Fetch user info + const users = await db + .select({ id: userTable.id, name: userTable.name, email: userTable.email }) + .from(userTable) + .where(eq(userTable.id, userId)) + .limit(1); + + if (users.length === 0) { + return c.json({ error: "unauthorized" }, 401); + } + + c.set("user", users[0]); + c.set("session", { id: sessionId, userId }); await next(); } diff --git a/server/src/ws/dashboard.ts b/server/src/ws/dashboard.ts index 831038d..9f8933d 100644 --- a/server/src/ws/dashboard.ts +++ b/server/src/ws/dashboard.ts @@ -1,5 +1,7 @@ import type { ServerWebSocket } from "bun"; -import { auth } from "../auth.js"; +import { db } from "../db.js"; +import { session as sessionTable } from "../schema.js"; +import { eq } from "drizzle-orm"; import { sessions, type WebSocketData } from "./sessions.js"; interface DashboardAuthMessage { @@ -34,17 +36,19 @@ export async function handleDashboardMessage( return; } - // Validate session via better-auth - const session = await auth.api.getSession({ - headers: new Headers({ cookie: `better-auth.session_token=${token}` }), - }); + // Look up session directly in DB + const rows = await db + .select({ userId: sessionTable.userId }) + .from(sessionTable) + .where(eq(sessionTable.token, token)) + .limit(1); - if (!session) { + if (rows.length === 0) { ws.send(JSON.stringify({ type: "auth_error", message: "Invalid session" })); return; } - const userId = session.user.id; + const userId = rows[0].userId; // Mark connection as authenticated ws.data.authenticated = true; diff --git a/server/src/ws/device.ts b/server/src/ws/device.ts index 553a4dc..6016d90 100644 --- a/server/src/ws/device.ts +++ b/server/src/ws/device.ts @@ -1,13 +1,26 @@ import type { ServerWebSocket } from "bun"; import type { DeviceMessage } from "@droidclaw/shared"; import { eq, and } from "drizzle-orm"; -import { auth } from "../auth.js"; import { db } from "../db.js"; -import { llmConfig, device } from "../schema.js"; +import { apikey, llmConfig, device } from "../schema.js"; import { sessions, type WebSocketData } from "./sessions.js"; import { runPipeline } from "../agent/pipeline.js"; import type { LLMConfig } from "../agent/llm.js"; +/** + * Hash an API key the same way better-auth does: + * SHA-256 → base64url (no padding). + */ +async function hashApiKey(key: string): Promise { + const data = new TextEncoder().encode(key); + const hash = await crypto.subtle.digest("SHA-256", data); + // base64url encode without padding + return btoa(String.fromCharCode(...new Uint8Array(hash))) + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=+$/, ""); +} + /** Track running agent sessions to prevent duplicates per device */ const activeSessions = new Map(); @@ -77,22 +90,37 @@ export async function handleDeviceMessage( if (msg.type === "auth") { try { - const result = await auth.api.verifyApiKey({ - body: { key: msg.apiKey }, - }); + // Hash the incoming key and look it up directly in the DB + const hashedKey = await hashApiKey(msg.apiKey); + const rows = await db + .select({ id: apikey.id, userId: apikey.userId, enabled: apikey.enabled, expiresAt: apikey.expiresAt }) + .from(apikey) + .where(eq(apikey.key, hashedKey)) + .limit(1); - if (!result.valid || !result.key) { + if (rows.length === 0 || !rows[0].enabled) { ws.send( JSON.stringify({ type: "auth_error", - message: result.error?.message ?? "Invalid API key", + message: "Invalid API key", + }) + ); + return; + } + + // Check expiration + if (rows[0].expiresAt && rows[0].expiresAt < new Date()) { + ws.send( + JSON.stringify({ + type: "auth_error", + message: "API key expired", }) ); return; } const deviceId = crypto.randomUUID(); - const userId = result.key.userId; + const userId = rows[0].userId; // Build device name from device info const name = msg.deviceInfo