fix(auth): use internal secret for web→server calls instead of cookie forwarding
Cookie forwarding between dash.droidclaw.ai and tunnel.droidclaw.ai was unreliable. Now the web app passes userId + shared internal secret via headers. Also removes debug logging from device auth and session middleware.
This commit is contained in:
@@ -2,6 +2,7 @@ export const env = {
|
|||||||
DATABASE_URL: process.env.DATABASE_URL!,
|
DATABASE_URL: process.env.DATABASE_URL!,
|
||||||
PORT: parseInt(process.env.PORT || "8080"),
|
PORT: parseInt(process.env.PORT || "8080"),
|
||||||
CORS_ORIGIN: process.env.CORS_ORIGIN || "http://localhost:5173",
|
CORS_ORIGIN: process.env.CORS_ORIGIN || "http://localhost:5173",
|
||||||
|
INTERNAL_SECRET: process.env.INTERNAL_SECRET || "",
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!env.DATABASE_URL) {
|
if (!env.DATABASE_URL) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { db } from "../db.js";
|
|||||||
import { session as sessionTable, user as userTable } from "../schema.js";
|
import { session as sessionTable, user as userTable } from "../schema.js";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { getCookie } from "hono/cookie";
|
import { getCookie } from "hono/cookie";
|
||||||
|
import { env } from "../env.js";
|
||||||
|
|
||||||
/** Hono Env type for routes protected by sessionMiddleware */
|
/** Hono Env type for routes protected by sessionMiddleware */
|
||||||
export type AuthEnv = {
|
export type AuthEnv = {
|
||||||
@@ -13,16 +14,35 @@ export type AuthEnv = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export async function sessionMiddleware(c: Context, next: Next) {
|
export async function sessionMiddleware(c: Context, next: Next) {
|
||||||
// Extract session token from cookie (same approach as dashboard WS auth)
|
// ── Internal server-to-server auth (web app → server) ──
|
||||||
|
const internalSecret = c.req.header("x-internal-secret");
|
||||||
|
const internalUserId = c.req.header("x-internal-user-id");
|
||||||
|
|
||||||
|
if (internalSecret && internalUserId && env.INTERNAL_SECRET && internalSecret === env.INTERNAL_SECRET) {
|
||||||
|
const users = await db
|
||||||
|
.select({ id: userTable.id, name: userTable.name, email: userTable.email })
|
||||||
|
.from(userTable)
|
||||||
|
.where(eq(userTable.id, internalUserId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (users.length === 0) {
|
||||||
|
return c.json({ error: "unauthorized" }, 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
c.set("user", users[0]);
|
||||||
|
c.set("session", { id: "internal", userId: internalUserId });
|
||||||
|
await next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Cookie-based auth (browser → server) ──
|
||||||
const rawCookie = getCookie(c, "better-auth.session_token");
|
const rawCookie = getCookie(c, "better-auth.session_token");
|
||||||
if (!rawCookie) {
|
if (!rawCookie) {
|
||||||
console.log(`[SessionMiddleware] No session cookie. Headers: ${JSON.stringify(Object.fromEntries(c.req.raw.headers.entries())).slice(0, 200)}`);
|
|
||||||
return c.json({ error: "unauthorized" }, 401);
|
return c.json({ error: "unauthorized" }, 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Token may have a signature appended after a dot — use only the token part
|
// Token may have a signature appended after a dot — use only the token part
|
||||||
const token = rawCookie.split(".")[0];
|
const token = rawCookie.split(".")[0];
|
||||||
console.log(`[SessionMiddleware] cookie prefix: ${rawCookie.slice(0, 20)}... token prefix: ${token.slice(0, 20)}...`);
|
|
||||||
|
|
||||||
// Direct DB lookup
|
// Direct DB lookup
|
||||||
const rows = await db
|
const rows = await db
|
||||||
|
|||||||
@@ -92,12 +92,6 @@ export async function handleDeviceMessage(
|
|||||||
try {
|
try {
|
||||||
// Hash the incoming key and look it up directly in the DB
|
// Hash the incoming key and look it up directly in the DB
|
||||||
const hashedKey = await hashApiKey(msg.apiKey);
|
const hashedKey = await hashApiKey(msg.apiKey);
|
||||||
console.log(`[Device Auth] key prefix: ${msg.apiKey.slice(0, 10)}... hash: ${hashedKey.slice(0, 16)}...`);
|
|
||||||
|
|
||||||
// Debug: list all keys in DB
|
|
||||||
const allKeys = await db.select({ id: apikey.id, keyPrefix: apikey.start, hash: apikey.key, enabled: apikey.enabled }).from(apikey);
|
|
||||||
console.log(`[Device Auth] DB has ${allKeys.length} keys:`, allKeys.map(k => `${k.keyPrefix ?? "?"} hash=${k.hash.slice(0, 16)}... enabled=${k.enabled}`));
|
|
||||||
|
|
||||||
const rows = await db
|
const rows = await db
|
||||||
.select({ id: apikey.id, userId: apikey.userId, enabled: apikey.enabled, expiresAt: apikey.expiresAt })
|
.select({ id: apikey.id, userId: apikey.userId, enabled: apikey.enabled, expiresAt: apikey.expiresAt })
|
||||||
.from(apikey)
|
.from(apikey)
|
||||||
@@ -105,7 +99,6 @@ export async function handleDeviceMessage(
|
|||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (rows.length === 0 || !rows[0].enabled) {
|
if (rows.length === 0 || !rows[0].enabled) {
|
||||||
console.log(`[Device Auth] REJECTED: no matching key found (or disabled)`);
|
|
||||||
ws.send(
|
ws.send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
type: "auth_error",
|
type: "auth_error",
|
||||||
|
|||||||
@@ -161,15 +161,19 @@ export const listSessionSteps = query(
|
|||||||
// ─── Commands (write operations) ─────────────────────────────
|
// ─── Commands (write operations) ─────────────────────────────
|
||||||
|
|
||||||
const SERVER_URL = () => env.SERVER_URL || 'http://localhost:8080';
|
const SERVER_URL = () => env.SERVER_URL || 'http://localhost:8080';
|
||||||
|
const INTERNAL_SECRET = () => env.INTERNAL_SECRET || '';
|
||||||
|
|
||||||
/** Forward a request to the DroidClaw server with auth cookies */
|
/** Forward a request to the DroidClaw server with internal auth */
|
||||||
async function serverFetch(path: string, body: Record<string, unknown>) {
|
async function serverFetch(path: string, body: Record<string, unknown>) {
|
||||||
const { request } = getRequestEvent();
|
const { locals } = getRequestEvent();
|
||||||
|
if (!locals.user) throw new Error('unauthorized');
|
||||||
|
|
||||||
const res = await fetch(`${SERVER_URL()}${path}`, {
|
const res = await fetch(`${SERVER_URL()}${path}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
cookie: request.headers.get('cookie') ?? ''
|
'x-internal-secret': INTERNAL_SECRET(),
|
||||||
|
'x-internal-user-id': locals.user.id
|
||||||
},
|
},
|
||||||
body: JSON.stringify(body)
|
body: JSON.stringify(body)
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user