fix: harden user linking and session pruning

Security:
- Add unique constraint on headscale_user_id to prevent hijacking
- linkHeadscaleUser now rejects already-claimed Headscale users
- Onboarding dropdown filters out claimed users
- onboarding-skip action redirects on rejected claims

Maintenance:
- Replace probabilistic session pruning with setInterval (15m)
- Move pruning out of request path into server startup

Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019cce57-c9e1-7732-9709-8288127573a9
This commit is contained in:
Aarnav Tale
2026-03-08 00:48:32 -05:00
parent 45984ec639
commit 684a95b5e8
8 changed files with 58 additions and 11 deletions
+1 -3
View File
@@ -80,9 +80,7 @@ export async function loader({ request, context }: Route.LoaderArgs) {
})()
: userInfo.picture;
const hasUsers = await context.auth.hasAnyUsers();
const defaultRole = hasUsers ? "member" : "owner";
const userId = await context.auth.findOrCreateUser(claims.sub, defaultRole);
const userId = await context.auth.findOrCreateUser(claims.sub);
try {
const hsApi = context.hsApi.getRuntimeClient(context.oidc!.apiKey);
+4 -1
View File
@@ -34,7 +34,10 @@ export async function action({ request, context }: Route.ActionArgs) {
const headscaleUserId = formData.get("headscale_user_id")?.toString();
if (headscaleUserId) {
await context.auth.linkHeadscaleUser(principal.user.id, headscaleUserId);
const linked = await context.auth.linkHeadscaleUser(principal.user.id, headscaleUserId);
if (!linked) {
return redirect("/onboarding");
}
}
await context.db
+7 -4
View File
@@ -74,10 +74,13 @@ export async function loader({ request, context }: Route.LoaderArgs) {
firstMachine = nodes.find((n) => n.user?.id === matched.id);
} else {
needsUserLink = true;
headscaleUsers = apiUsers.map((u) => ({
id: u.id,
name: getUserDisplayName(u),
}));
const claimed = await context.auth.claimedHeadscaleUserIds();
headscaleUsers = apiUsers
.filter((u) => !claimed.has(u.id))
.map((u) => ({
id: u.id,
name: getUserDisplayName(u),
}));
}
}
} catch (e) {