fix csrf token hardset, remove cookie from localstorage

This commit is contained in:
Zimeng Xiong
2026-02-10 13:16:04 -08:00
parent 1117dc584e
commit bb028ef2db
23 changed files with 412 additions and 145 deletions
+1
View File
@@ -3,6 +3,7 @@ PORT=8000
NODE_ENV=production
DATABASE_URL=file:/app/prisma/dev.db
FRONTEND_URL=http://localhost:6767
TRUST_PROXY=1
JWT_SECRET=change-this-secret-in-production-min-32-chars
# Optional Feature Flags (all default to false for backward compatibility)
+15
View File
@@ -21,6 +21,13 @@ import {
type AuthModeService,
} from "./auth/authMode";
import { getCsrfValidationClientIds } from "./security/csrfClient";
import {
clearAuthCookies,
readCookie,
REFRESH_TOKEN_COOKIE_NAME,
setAccessTokenCookie,
setAuthCookies,
} from "./auth/cookies";
interface JwtPayload {
userId: string;
@@ -380,6 +387,10 @@ export const createAuthRouter = (deps: CreateAuthRouterDeps): express.Router =>
bootstrapUserId: BOOTSTRAP_USER_ID,
defaultSystemConfigId: DEFAULT_SYSTEM_CONFIG_ID,
clearAuthEnabledCache: authModeService.clearAuthEnabledCache,
setAuthCookies,
setAccessTokenCookie,
clearAuthCookies,
readRefreshTokenFromRequest: (req) => readCookie(req, REFRESH_TOKEN_COOKIE_NAME),
});
registerAdminRoutes({
@@ -401,6 +412,8 @@ export const createAuthRouter = (deps: CreateAuthRouterDeps): express.Router =>
getRefreshTokenExpiresAt,
config,
defaultSystemConfigId: DEFAULT_SYSTEM_CONFIG_ID,
setAuthCookies,
requireCsrf,
});
registerAccountRoutes({
@@ -414,6 +427,8 @@ export const createAuthRouter = (deps: CreateAuthRouterDeps): express.Router =>
config,
generateTokens,
getRefreshTokenExpiresAt,
setAuthCookies,
requireCsrf,
});
return router;
+14
View File
@@ -34,6 +34,12 @@ type RegisterAccountRoutesDeps = {
options?: { impersonatorId?: string }
) => { accessToken: string; refreshToken: string };
getRefreshTokenExpiresAt: () => Date;
setAuthCookies: (
req: Request,
res: Response,
tokens: { accessToken: string; refreshToken: string }
) => void;
requireCsrf: (req: Request, res: Response) => boolean;
};
export const registerAccountRoutes = (deps: RegisterAccountRoutesDeps) => {
@@ -48,6 +54,8 @@ export const registerAccountRoutes = (deps: RegisterAccountRoutesDeps) => {
config,
generateTokens,
getRefreshTokenExpiresAt,
setAuthCookies,
requireCsrf,
} = deps;
router.post("/password-reset-request", loginAttemptRateLimiter, async (req: Request, res: Response) => {
@@ -210,6 +218,7 @@ export const registerAccountRoutes = (deps: RegisterAccountRoutesDeps) => {
router.put("/profile", requireAuth, async (req: Request, res: Response) => {
try {
if (!(await ensureAuthEnabled(res))) return;
if (!requireCsrf(req, res)) return;
if (!req.user) {
return res.status(401).json({
error: "Unauthorized",
@@ -261,6 +270,7 @@ export const registerAccountRoutes = (deps: RegisterAccountRoutesDeps) => {
router.put("/email", requireAuth, accountActionRateLimiter, async (req: Request, res: Response) => {
try {
if (!(await ensureAuthEnabled(res))) return;
if (!requireCsrf(req, res)) return;
if (!req.user) {
return res.status(401).json({ error: "Unauthorized", message: "User not authenticated" });
}
@@ -347,6 +357,7 @@ export const registerAccountRoutes = (deps: RegisterAccountRoutesDeps) => {
}
const { accessToken, refreshToken } = generateTokens(updatedUser.id, updatedUser.email);
setAuthCookies(req, res, { accessToken, refreshToken });
if (config.enableRefreshTokenRotation) {
const expiresAt = getRefreshTokenExpiresAt();
try {
@@ -387,6 +398,7 @@ export const registerAccountRoutes = (deps: RegisterAccountRoutesDeps) => {
router.post("/change-password", requireAuth, accountActionRateLimiter, async (req: Request, res: Response) => {
try {
if (!(await ensureAuthEnabled(res))) return;
if (!requireCsrf(req, res)) return;
if (!req.user) {
return res.status(401).json({ error: "Unauthorized", message: "User not authenticated" });
}
@@ -463,6 +475,7 @@ export const registerAccountRoutes = (deps: RegisterAccountRoutesDeps) => {
router.post("/must-reset-password", requireAuth, accountActionRateLimiter, async (req: Request, res: Response) => {
try {
if (!(await ensureAuthEnabled(res))) return;
if (!requireCsrf(req, res)) return;
if (!req.user) {
return res.status(401).json({ error: "Unauthorized", message: "User not authenticated" });
}
@@ -528,6 +541,7 @@ export const registerAccountRoutes = (deps: RegisterAccountRoutesDeps) => {
}
const { accessToken, refreshToken } = generateTokens(updatedUser.id, updatedUser.email);
setAuthCookies(req, res, { accessToken, refreshToken });
if (config.enableRefreshTokenRotation) {
const expiresAt = getRefreshTokenExpiresAt();
try {
+17
View File
@@ -64,6 +64,12 @@ type RegisterAdminRoutesDeps = {
enableRefreshTokenRotation: boolean;
};
defaultSystemConfigId: string;
setAuthCookies: (
req: Request,
res: Response,
tokens: { accessToken: string; refreshToken: string }
) => void;
requireCsrf: (req: Request, res: Response) => boolean;
};
export const registerAdminRoutes = (deps: RegisterAdminRoutesDeps) => {
@@ -86,11 +92,14 @@ export const registerAdminRoutes = (deps: RegisterAdminRoutesDeps) => {
getRefreshTokenExpiresAt,
config,
defaultSystemConfigId,
setAuthCookies,
requireCsrf,
} = deps;
router.post("/registration/toggle", requireAuth, async (req: Request, res: Response) => {
try {
if (!(await ensureAuthEnabled(res))) return;
if (!requireCsrf(req, res)) return;
if (!requireAdmin(req, res)) return;
const parsed = registrationToggleSchema.safeParse(req.body);
@@ -117,6 +126,7 @@ export const registerAdminRoutes = (deps: RegisterAdminRoutesDeps) => {
router.post("/admins", requireAuth, async (req: Request, res: Response) => {
try {
if (!(await ensureAuthEnabled(res))) return;
if (!requireCsrf(req, res)) return;
if (!requireAdmin(req, res)) return;
const parsed = adminRoleUpdateSchema.safeParse(req.body);
@@ -220,6 +230,7 @@ export const registerAdminRoutes = (deps: RegisterAdminRoutesDeps) => {
router.put("/rate-limit/login", requireAuth, async (req: Request, res: Response) => {
try {
if (!(await ensureAuthEnabled(res))) return;
if (!requireCsrf(req, res)) return;
if (!requireAdmin(req, res)) return;
const parsed = loginRateLimitUpdateSchema.safeParse(req.body);
@@ -265,6 +276,7 @@ export const registerAdminRoutes = (deps: RegisterAdminRoutesDeps) => {
router.post("/rate-limit/login/reset", requireAuth, async (req: Request, res: Response) => {
try {
if (!(await ensureAuthEnabled(res))) return;
if (!requireCsrf(req, res)) return;
if (!requireAdmin(req, res)) return;
const parsed = loginRateLimitResetSchema.safeParse(req.body);
@@ -302,6 +314,7 @@ export const registerAdminRoutes = (deps: RegisterAdminRoutesDeps) => {
router.post("/users", requireAuth, accountActionRateLimiter, async (req: Request, res: Response) => {
try {
if (!(await ensureAuthEnabled(res))) return;
if (!requireCsrf(req, res)) return;
if (!requireAdmin(req, res)) return;
const parsed = adminCreateUserSchema.safeParse(req.body);
@@ -386,6 +399,7 @@ export const registerAdminRoutes = (deps: RegisterAdminRoutesDeps) => {
router.patch("/users/:id", requireAuth, async (req: Request, res: Response) => {
try {
if (!(await ensureAuthEnabled(res))) return;
if (!requireCsrf(req, res)) return;
if (!requireAdmin(req, res)) return;
const userId = String(req.params.id || "").trim();
@@ -494,6 +508,7 @@ export const registerAdminRoutes = (deps: RegisterAdminRoutesDeps) => {
router.post("/users/:id/reset-password", requireAuth, accountActionRateLimiter, async (req: Request, res: Response) => {
try {
if (!(await ensureAuthEnabled(res))) return;
if (!requireCsrf(req, res)) return;
if (!requireAdmin(req, res)) return;
if (req.user.impersonatorId) {
@@ -583,6 +598,7 @@ export const registerAdminRoutes = (deps: RegisterAdminRoutesDeps) => {
router.post("/impersonate", requireAuth, accountActionRateLimiter, async (req: Request, res: Response) => {
try {
if (!(await ensureAuthEnabled(res))) return;
if (!requireCsrf(req, res)) return;
if (!requireAdmin(req, res)) return;
const parsed = impersonateSchema.safeParse(req.body);
@@ -606,6 +622,7 @@ export const registerAdminRoutes = (deps: RegisterAdminRoutesDeps) => {
const { accessToken, refreshToken } = generateTokens(target.id, target.email, {
impersonatorId: req.user.id,
});
setAuthCookies(req, res, { accessToken, refreshToken });
if (config.enableRefreshTokenRotation) {
const expiresAt = getRefreshTokenExpiresAt();
+106
View File
@@ -0,0 +1,106 @@
import type { Request, Response } from "express";
import ms, { type StringValue } from "ms";
import { config } from "../config";
export const ACCESS_TOKEN_COOKIE_NAME = "excalidash-access-token";
export const REFRESH_TOKEN_COOKIE_NAME = "excalidash-refresh-token";
const DEFAULT_ACCESS_TTL_MS = 15 * 60 * 1000;
const DEFAULT_REFRESH_TTL_MS = 7 * 24 * 60 * 60 * 1000;
const parseDurationToMs = (value: string, fallbackMs: number): number => {
const parsed = ms(value as StringValue);
if (typeof parsed === "number" && Number.isFinite(parsed) && parsed > 0) {
return parsed;
}
return fallbackMs;
};
const ACCESS_TOKEN_COOKIE_MAX_AGE_MS = parseDurationToMs(
config.jwtAccessExpiresIn,
DEFAULT_ACCESS_TTL_MS
);
const REFRESH_TOKEN_COOKIE_MAX_AGE_MS = parseDurationToMs(
config.jwtRefreshExpiresIn,
DEFAULT_REFRESH_TTL_MS
);
const requestUsesHttps = (req: Request): boolean => {
if (req.secure) return true;
const forwardedProto = req.headers["x-forwarded-proto"];
const raw = Array.isArray(forwardedProto) ? forwardedProto[0] : forwardedProto;
const firstHop = String(raw || "")
.split(",")[0]
.trim()
.toLowerCase();
return firstHop === "https";
};
const shouldUseSecureCookies = (req: Request): boolean => requestUsesHttps(req);
const baseCookieOptions = (req: Request) => ({
httpOnly: true,
secure: shouldUseSecureCookies(req),
sameSite: "lax" as const,
path: "/",
});
export const setAuthCookies = (
req: Request,
res: Response,
tokens: { accessToken: string; refreshToken: string }
): void => {
res.cookie(ACCESS_TOKEN_COOKIE_NAME, tokens.accessToken, {
...baseCookieOptions(req),
maxAge: ACCESS_TOKEN_COOKIE_MAX_AGE_MS,
});
res.cookie(REFRESH_TOKEN_COOKIE_NAME, tokens.refreshToken, {
...baseCookieOptions(req),
maxAge: REFRESH_TOKEN_COOKIE_MAX_AGE_MS,
});
};
export const setAccessTokenCookie = (
req: Request,
res: Response,
accessToken: string
): void => {
res.cookie(ACCESS_TOKEN_COOKIE_NAME, accessToken, {
...baseCookieOptions(req),
maxAge: ACCESS_TOKEN_COOKIE_MAX_AGE_MS,
});
};
export const clearAuthCookies = (req: Request, res: Response): void => {
const options = baseCookieOptions(req);
res.clearCookie(ACCESS_TOKEN_COOKIE_NAME, options);
res.clearCookie(REFRESH_TOKEN_COOKIE_NAME, options);
};
export const parseCookieHeader = (
cookieHeader: string | undefined
): Record<string, string> => {
if (!cookieHeader) return {};
const cookies: Record<string, string> = {};
for (const part of cookieHeader.split(";")) {
const [rawKey, ...rawValueParts] = part.split("=");
if (!rawKey || rawValueParts.length === 0) continue;
const key = rawKey.trim();
if (!key) continue;
const rawValue = rawValueParts.join("=").trim();
try {
cookies[key] = decodeURIComponent(rawValue);
} catch {
cookies[key] = rawValue;
}
}
return cookies;
};
export const readCookie = (req: Request, cookieName: string): string | null => {
const cookies = parseCookieHeader(req.headers.cookie);
const value = cookies[cookieName];
if (!value || value.trim().length === 0) return null;
return value;
};
+133 -1
View File
@@ -57,6 +57,14 @@ type RegisterCoreRoutesDeps = {
bootstrapUserId: string;
defaultSystemConfigId: string;
clearAuthEnabledCache: () => void;
setAuthCookies: (
req: Request,
res: Response,
tokens: { accessToken: string; refreshToken: string }
) => void;
setAccessTokenCookie: (req: Request, res: Response, accessToken: string) => void;
clearAuthCookies: (req: Request, res: Response) => void;
readRefreshTokenFromRequest: (req: Request) => string | null;
};
class HttpError extends Error {
@@ -88,6 +96,10 @@ export const registerCoreRoutes = (deps: RegisterCoreRoutesDeps) => {
bootstrapUserId,
defaultSystemConfigId,
clearAuthEnabledCache,
setAuthCookies,
setAccessTokenCookie,
clearAuthCookies,
readRefreshTokenFromRequest,
} = deps;
const getUserTrashCollectionId = (userId: string): string => `trash:${userId}`;
@@ -158,6 +170,7 @@ export const registerCoreRoutes = (deps: RegisterCoreRoutesDeps) => {
}
const { accessToken, refreshToken } = generateTokens(user.id, user.email);
setAuthCookies(req, res, { accessToken, refreshToken });
if (config.enableRefreshTokenRotation) {
const expiresAt = getRefreshTokenExpiresAt();
@@ -257,6 +270,7 @@ export const registerCoreRoutes = (deps: RegisterCoreRoutesDeps) => {
}
const { accessToken, refreshToken } = generateTokens(user.id, user.email);
setAuthCookies(req, res, { accessToken, refreshToken });
if (config.enableRefreshTokenRotation) {
const expiresAt = getRefreshTokenExpiresAt();
@@ -375,6 +389,7 @@ export const registerCoreRoutes = (deps: RegisterCoreRoutesDeps) => {
}
const { accessToken, refreshToken } = generateTokens(user.id, user.email);
setAuthCookies(req, res, { accessToken, refreshToken });
if (config.enableRefreshTokenRotation) {
const expiresAt = getRefreshTokenExpiresAt();
@@ -431,7 +446,9 @@ export const registerCoreRoutes = (deps: RegisterCoreRoutesDeps) => {
router.post("/refresh", async (req: Request, res: Response) => {
try {
if (!(await ensureAuthEnabled(res))) return;
const { refreshToken: oldRefreshToken } = req.body;
const oldRefreshTokenFromBody =
typeof req.body?.refreshToken === "string" ? req.body.refreshToken : null;
const oldRefreshToken = oldRefreshTokenFromBody || readRefreshTokenFromRequest(req);
if (!oldRefreshToken || typeof oldRefreshToken !== "string") {
return res.status(400).json({
@@ -513,6 +530,10 @@ export const registerCoreRoutes = (deps: RegisterCoreRoutesDeps) => {
});
});
setAuthCookies(req, res, {
accessToken,
refreshToken: newRefreshToken,
});
return res.json({
accessToken,
refreshToken: newRefreshToken,
@@ -555,6 +576,7 @@ export const registerCoreRoutes = (deps: RegisterCoreRoutesDeps) => {
signOptions
);
setAccessTokenCookie(req, res, accessToken);
res.json({ accessToken });
} catch {
return res.status(401).json({
@@ -571,6 +593,116 @@ export const registerCoreRoutes = (deps: RegisterCoreRoutesDeps) => {
}
});
router.post("/logout", optionalAuth, async (req: Request, res: Response) => {
try {
if (!(await ensureAuthEnabled(res))) return;
if (!requireCsrf(req, res)) return;
clearAuthCookies(req, res);
if (config.enableRefreshTokenRotation && req.user?.id) {
await prisma.refreshToken.updateMany({
where: { userId: req.user.id, revoked: false },
data: { revoked: true },
});
}
return res.json({ ok: true });
} catch (error) {
console.error("Logout error:", error);
return res.status(500).json({
error: "Internal server error",
message: "Failed to logout",
});
}
});
router.post("/stop-impersonation", requireAuth, async (req: Request, res: Response) => {
try {
if (!(await ensureAuthEnabled(res))) return;
if (!requireCsrf(req, res)) return;
if (!req.user) {
return res.status(401).json({
error: "Unauthorized",
message: "User not authenticated",
});
}
if (!req.user.impersonatorId) {
return res.status(409).json({
error: "Conflict",
message: "Not currently impersonating another user",
});
}
const impersonator = await prisma.user.findUnique({
where: { id: req.user.impersonatorId },
select: {
id: true,
username: true,
email: true,
name: true,
role: true,
mustResetPassword: true,
isActive: true,
},
});
if (!impersonator || !impersonator.isActive || impersonator.role !== "ADMIN") {
return res.status(403).json({
error: "Forbidden",
message: "Impersonator account is unavailable or no longer authorized",
});
}
const { accessToken, refreshToken } = generateTokens(
impersonator.id,
impersonator.email
);
setAuthCookies(req, res, { accessToken, refreshToken });
if (config.enableRefreshTokenRotation) {
const expiresAt = getRefreshTokenExpiresAt();
try {
await prisma.refreshToken.create({
data: {
userId: impersonator.id,
token: hashTokenForStorage(refreshToken),
expiresAt,
},
});
} catch (error) {
if (isMissingRefreshTokenTableError(error)) {
return res.status(503).json({
error: "Service unavailable",
message: "Refresh token storage is unavailable. Please run database migrations.",
});
}
throw error;
}
}
return res.json({
user: {
id: impersonator.id,
username: impersonator.username,
email: impersonator.email,
name: impersonator.name,
role: impersonator.role,
mustResetPassword: impersonator.mustResetPassword,
},
accessToken,
refreshToken,
});
} catch (error) {
console.error("Stop impersonation error:", error);
return res.status(500).json({
error: "Internal server error",
message: "Failed to stop impersonation",
});
}
});
router.get("/me", requireAuth, async (req: Request, res: Response) => {
try {
if (!(await ensureAuthEnabled(res))) return;
+21 -5
View File
@@ -1,9 +1,25 @@
import { z } from "zod";
const productionStrongPasswordMessage =
"Password must be at least 12 characters and include upper, lower, number, and symbol";
const strongPasswordPattern =
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^A-Za-z0-9]).{12,100}$/;
const passwordSchema = z
.string()
.min(8)
.max(100)
.refine(
(value) =>
process.env.NODE_ENV !== "production" || strongPasswordPattern.test(value),
{ message: productionStrongPasswordMessage }
);
export const registerSchema = z.object({
username: z.string().trim().min(3).max(50).optional(),
email: z.string().email().toLowerCase().trim(),
password: z.string().min(8).max(100),
password: passwordSchema,
name: z.string().trim().min(1).max(100),
});
@@ -34,7 +50,7 @@ export const authEnabledToggleSchema = z.object({
export const adminCreateUserSchema = z.object({
username: z.string().trim().min(3).max(50).optional(),
email: z.string().email().toLowerCase().trim(),
password: z.string().min(8).max(100),
password: passwordSchema,
name: z.string().trim().min(1).max(100),
role: z.enum(["ADMIN", "USER"]).optional(),
mustResetPassword: z.boolean().optional(),
@@ -74,7 +90,7 @@ export const passwordResetRequestSchema = z.object({
export const passwordResetConfirmSchema = z.object({
token: z.string().min(1),
password: z.string().min(8).max(100),
password: passwordSchema,
});
export const updateProfileSchema = z.object({
@@ -88,9 +104,9 @@ export const updateEmailSchema = z.object({
export const changePasswordSchema = z.object({
currentPassword: z.string(),
newPassword: z.string().min(8).max(100),
newPassword: passwordSchema,
});
export const mustResetPasswordSchema = z.object({
newPassword: z.string().min(8).max(100),
newPassword: passwordSchema,
});
+11 -2
View File
@@ -118,11 +118,20 @@ export const config: Config = {
// Validate JWT_SECRET strength in production
if (config.nodeEnv === "production") {
const normalizedSecret = config.jwtSecret.trim();
const insecureJwtSecretPlaceholders = new Set([
"your-secret-key-change-in-production",
"change-this-secret-in-production-min-32-chars",
]);
if (config.jwtSecret.length < 32) {
throw new Error("JWT_SECRET must be at least 32 characters long in production");
}
if (config.jwtSecret === "your-secret-key-change-in-production") {
throw new Error("JWT_SECRET must be changed from default value in production");
if (
insecureJwtSecretPlaceholders.has(normalizedSecret) ||
normalizedSecret.toLowerCase().includes("change-this-secret")
) {
throw new Error("JWT_SECRET must be changed from placeholder/default value in production");
}
}
+13 -9
View File
@@ -109,15 +109,19 @@ const initializeUploadDir = async () => {
const app = express();
// Trust proxy headers (X-Forwarded-For, X-Real-IP) from nginx.
// Default to a single trusted proxy hop unless TRUST_PROXY is explicitly configured.
// Set TRUST_PROXY=true only when you fully trust all upstream proxy hops.
const trustProxyConfig = (process.env.TRUST_PROXY ?? "1").trim();
const trustProxyValue = trustProxyConfig === "true"
? true
: trustProxyConfig === "false"
? false
: Number.parseInt(trustProxyConfig, 10) || 1;
// Trust proxy headers (X-Forwarded-For, X-Real-IP) only when explicitly configured.
// Safe default is disabled to avoid spoofed client IPs when running without a trusted proxy.
// Set TRUST_PROXY=1 (or a specific hop count) when deploying behind reverse proxies.
const trustProxyConfig = (process.env.TRUST_PROXY ?? "false").trim();
const parsedProxyHops = Number.parseInt(trustProxyConfig, 10);
const trustProxyValue =
trustProxyConfig === "true"
? true
: trustProxyConfig === "false"
? false
: Number.isFinite(parsedProxyHops) && parsedProxyHops > 0
? parsedProxyHops
: false;
app.set("trust proxy", trustProxyValue);
if (trustProxyValue === true) {
+7 -6
View File
@@ -4,6 +4,7 @@ import { config } from "../config";
import { PrismaClient } from "../generated/client";
import { prisma as defaultPrisma } from "../db/prisma";
import { createAuthModeService, type AuthModeService } from "../auth/authMode";
import { ACCESS_TOKEN_COOKIE_NAME, readCookie } from "../auth/cookies";
// Extend Express Request type to include user
declare global {
@@ -46,14 +47,14 @@ const isJwtPayload = (decoded: unknown): decoded is JwtPayload => {
const extractToken = (req: Request): string | null => {
const authHeader = req.headers.authorization;
if (!authHeader || typeof authHeader !== "string") return null;
const parts = authHeader.split(" ");
if (parts.length !== 2 || parts[0] !== "Bearer") {
return null;
if (authHeader && typeof authHeader === "string") {
const parts = authHeader.split(" ");
if (parts.length === 2 && parts[0] === "Bearer") {
return parts[1] || null;
}
}
return parts[1];
return readCookie(req, ACCESS_TOKEN_COOKIE_NAME);
};
const verifyToken = (token: string): JwtPayload | null => {
+8 -1
View File
@@ -2,6 +2,7 @@ import jwt from "jsonwebtoken";
import { Server } from "socket.io";
import { PrismaClient } from "../generated/client";
import { AuthModeService } from "../auth/authMode";
import { ACCESS_TOKEN_COOKIE_NAME, parseCookieHeader } from "../auth/cookies";
interface User {
id: string;
@@ -87,7 +88,13 @@ export const registerSocketHandlers = ({
io.use(async (socket, next) => {
try {
const token = socket.handshake.auth?.token as string | undefined;
const tokenFromAuth = socket.handshake.auth?.token as string | undefined;
const tokenFromCookie = (() => {
const cookies = parseCookieHeader(socket.handshake.headers.cookie);
const value = cookies[ACCESS_TOKEN_COOKIE_NAME];
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
})();
const token = tokenFromAuth || tokenFromCookie;
const userId = await getSocketAuthUserId(token);
if (!userId) {