From 5bd31f87f0be0e3a29d9f538a947a724c74d8e2b Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Wed, 26 Nov 2025 15:48:38 -0500 Subject: [PATCH] only allow one device auth per session --- messages/en-US.json | 4 +-- server/auth/sessions/verifySession.ts | 8 ++++- server/db/pg/schema/schema.ts | 3 +- server/db/sqlite/schema/schema.ts | 3 +- server/routers/auth/verifyDeviceWebAuth.ts | 40 +++++++++++++++++----- 5 files changed, 45 insertions(+), 13 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index f3ae34a7..76908b29 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -2140,7 +2140,7 @@ "deviceOrganizationsAccess": "Access to all organizations your account has access to", "deviceAuthorize": "Authorize {applicationName}", "deviceConnected": "Device Connected!", - "deviceAuthorizedMessage": "Your device is authorized to access your account.", + "deviceAuthorizedMessage": "Device is authorized to access your account.", "pangolinCloud": "Pangolin Cloud", "viewDevices": "View Devices", "viewDevicesDescription": "Manage your connected devices", @@ -2202,5 +2202,5 @@ "enterIdentifier": "Enter identifier", "identifier": "Identifier", "deviceLoginUseDifferentAccount": "Not you? Use a different account.", - "deviceLoginDeviceRequestingAccessToAccount": "Your device is requesting access to this account." + "deviceLoginDeviceRequestingAccessToAccount": "A device is requesting access to this account." } diff --git a/server/auth/sessions/verifySession.ts b/server/auth/sessions/verifySession.ts index 68a1f17e..01b32ef6 100644 --- a/server/auth/sessions/verifySession.ts +++ b/server/auth/sessions/verifySession.ts @@ -18,13 +18,19 @@ export async function verifySession(req: Request, forceLogin?: boolean) { user: null }; } + if (res.session.deviceAuthUsed) { + return { + session: null, + user: null + }; + } if (!res.session.issuedAt) { return { session: null, user: null }; } - const mins = 3 * 60 * 1000; + const mins = 5 * 60 * 1000; const now = new Date().getTime(); if (now - res.session.issuedAt > mins) { return { diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index d15676a0..120a7aa3 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -287,7 +287,8 @@ export const sessions = pgTable("session", { .notNull() .references(() => users.userId, { onDelete: "cascade" }), expiresAt: bigint("expiresAt", { mode: "number" }).notNull(), - issuedAt: bigint("issuedAt", { mode: "number" }) + issuedAt: bigint("issuedAt", { mode: "number" }), + deviceAuthUsed: boolean("deviceAuthUsed") }); export const newtSessions = pgTable("newtSession", { diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index 634afd36..a5f3d0f6 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -415,7 +415,8 @@ export const sessions = sqliteTable("session", { .notNull() .references(() => users.userId, { onDelete: "cascade" }), expiresAt: integer("expiresAt").notNull(), - issuedAt: integer("issuedAt") + issuedAt: integer("issuedAt"), + deviceAuthUsed: integer("deviceAuthUsed", { mode: "boolean" }) }); export const newtSessions = sqliteTable("newtSession", { diff --git a/server/routers/auth/verifyDeviceWebAuth.ts b/server/routers/auth/verifyDeviceWebAuth.ts index 715b299a..be0e0ff2 100644 --- a/server/routers/auth/verifyDeviceWebAuth.ts +++ b/server/routers/auth/verifyDeviceWebAuth.ts @@ -5,7 +5,7 @@ import { fromError } from "zod-validation-error"; import HttpCode from "@server/types/HttpCode"; import logger from "@server/logger"; import { response } from "@server/lib/response"; -import { db, deviceWebAuthCodes } from "@server/db"; +import { db, deviceWebAuthCodes, sessions } from "@server/db"; import { eq, and, gt } from "drizzle-orm"; import { encodeHexLowerCase } from "@oslojs/encoding"; import { sha256 } from "@oslojs/crypto/sha2"; @@ -44,20 +44,36 @@ export async function verifyDeviceWebAuth( ): Promise { const { user, session } = req; if (!user || !session) { - logger.debug("Unauthorized attempt to verify device web auth code"); - return next(unauthorized()); + return next(createHttpError(HttpCode.UNAUTHORIZED, "Unauthorized")); + } + + if (session.deviceAuthUsed) { + return next( + createHttpError( + HttpCode.UNAUTHORIZED, + "Device web auth code already used for this session" + ) + ); } if (!session.issuedAt) { - logger.debug("Session missing issuedAt timestamp"); - return next(unauthorized()); + return next( + createHttpError( + HttpCode.UNAUTHORIZED, + "Session issuedAt timestamp missing" + ) + ); } // make sure sessions is not older than 5 minutes const now = Date.now(); - if (now - session.issuedAt > 3 * 60 * 1000) { - logger.debug("Session is too old to verify device web auth code"); - return next(unauthorized()); + if (now - session.issuedAt > 5 * 60 * 1000) { + return next( + createHttpError( + HttpCode.UNAUTHORIZED, + "Session is too old to verify device web auth code" + ) + ); } const parsedBody = bodySchema.safeParse(req.body); @@ -134,6 +150,14 @@ export async function verifyDeviceWebAuth( }) .where(eq(deviceWebAuthCodes.codeId, deviceCode.codeId)); + // Also update the session to mark that device auth was used + await db + .update(sessions) + .set({ + deviceAuthUsed: true + }) + .where(eq(sessions.sessionId, session.sessionId)); + return response(res, { data: { success: true,