Files
claude-code-conduit/server/auth.mjs
mikael-lovqvists-claude-agent 67c1c3f9a4 Add HMAC auth, user permissions, snake_case rename
Each request is signed with HMAC-SHA256 over timestamp+body using a
per-user secret loaded from a --secrets file (never env vars or git).
Users have a canApprove list controlling who may approve queued actions.
Queue entries track submitted_by for permission checks on approve/deny.

Also renames all identifiers to snake_case throughout the codebase.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 20:18:41 +00:00

47 lines
1.4 KiB
JavaScript

import { createHmac, timingSafeEqual } from "crypto";
export function create_auth_middleware(users) {
return function hmac_auth(req, res, next) {
const username = req.headers["x-conduit-user"];
const timestamp = req.headers["x-conduit-timestamp"];
const signature = req.headers["x-conduit-signature"];
if (!username || !timestamp || !signature) {
return res.status(401).json({ error: "Missing auth headers" });
}
const ts = parseInt(timestamp, 10);
if (!Number.isFinite(ts) || Date.now() - ts > 30_000) {
return res.status(401).json({ error: "Request expired" });
}
const user = users[username];
if (!user) {
return res.status(401).json({ error: "Unknown user" });
}
const raw_body = req.raw_body ?? "";
const expected = createHmac("sha256", user.secret)
.update(timestamp + "." + raw_body)
.digest("hex");
const sig_buf = Buffer.from(signature, "hex");
const expected_buf = Buffer.from(expected, "hex");
if (sig_buf.length !== expected_buf.length || !timingSafeEqual(sig_buf, expected_buf)) {
return res.status(401).json({ error: "Invalid signature" });
}
req.conduit_user = username;
next();
};
}
export function check_can_approve(users, requester_name, submitter_name) {
const requester = users[requester_name];
if (!requester) {
return false;
}
return requester.canApprove.includes(submitter_name);
}