Improved JWT security
This commit is contained in:
30
package-lock.json
generated
30
package-lock.json
generated
@@ -31,6 +31,7 @@
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/cookie-parser": "^1.4.9",
|
||||
"@types/jszip": "^3.4.0",
|
||||
"@types/multer": "^2.0.0",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
@@ -49,6 +50,7 @@
|
||||
"chalk": "^4.1.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.2.0",
|
||||
"drizzle-orm": "^0.44.3",
|
||||
@@ -4625,6 +4627,15 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/cookie-parser": {
|
||||
"version": "1.4.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.9.tgz",
|
||||
"integrity": "sha512-tGZiZ2Gtc4m3wIdLkZ8mkj1T6CEHb35+VApbL2T14Dew8HA7c+04dmKqsKRNC+8RJPm16JEK0tFSwdZqubfc4g==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/express": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/cors": {
|
||||
"version": "2.8.19",
|
||||
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
|
||||
@@ -7045,6 +7056,25 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-parser": {
|
||||
"version": "1.4.7",
|
||||
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz",
|
||||
"integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie": "0.7.2",
|
||||
"cookie-signature": "1.0.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-signature": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/core-util-is": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/cookie-parser": "^1.4.9",
|
||||
"@types/jszip": "^3.4.0",
|
||||
"@types/multer": "^2.0.0",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
@@ -67,6 +68,7 @@
|
||||
"chalk": "^4.1.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.2.0",
|
||||
"drizzle-orm": "^0.44.3",
|
||||
@@ -128,4 +130,4 @@
|
||||
"typescript-eslint": "^8.40.0",
|
||||
"vite": "^7.1.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import express from "express";
|
||||
import bodyParser from "body-parser";
|
||||
import multer from "multer";
|
||||
import cookieParser from "cookie-parser";
|
||||
import userRoutes from "./routes/users.js";
|
||||
import sshRoutes from "./routes/ssh.js";
|
||||
import alertRoutes from "./routes/alerts.js";
|
||||
@@ -34,7 +35,33 @@ const authenticateJWT = authManager.createAuthMiddleware();
|
||||
const requireAdmin = authManager.createAdminMiddleware();
|
||||
app.use(
|
||||
cors({
|
||||
origin: "*",
|
||||
origin: (origin, callback) => {
|
||||
// Allow requests with no origin (like mobile apps or curl requests)
|
||||
if (!origin) return callback(null, true);
|
||||
|
||||
// Allow localhost and 127.0.0.1 for development
|
||||
const allowedOrigins = [
|
||||
"http://localhost:5173",
|
||||
"http://localhost:3000",
|
||||
"http://127.0.0.1:5173",
|
||||
"http://127.0.0.1:3000"
|
||||
];
|
||||
|
||||
if (origin.startsWith("https://")) {
|
||||
return callback(null, true);
|
||||
}
|
||||
|
||||
if (origin.startsWith("http://")) {
|
||||
return callback(null, true);
|
||||
}
|
||||
|
||||
if (allowedOrigins.includes(origin)) {
|
||||
return callback(null, true);
|
||||
}
|
||||
|
||||
// Reject other origins
|
||||
callback(new Error("Not allowed by CORS"));
|
||||
},
|
||||
credentials: true,
|
||||
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
||||
allowedHeaders: [
|
||||
@@ -179,6 +206,7 @@ async function fetchGitHubAPI(
|
||||
}
|
||||
|
||||
app.use(bodyParser.json());
|
||||
app.use(cookieParser());
|
||||
|
||||
app.get("/health", (req, res) => {
|
||||
res.json({ status: "ok" });
|
||||
|
||||
@@ -787,9 +787,10 @@ router.get("/oidc/callback", async (req, res) => {
|
||||
|
||||
const redirectUrl = new URL(frontendUrl);
|
||||
redirectUrl.searchParams.set("success", "true");
|
||||
redirectUrl.searchParams.set("token", token);
|
||||
|
||||
res.redirect(redirectUrl.toString());
|
||||
return res
|
||||
.cookie("jwt", token, authManager.getSecureCookieOptions(req, 50 * 24 * 60 * 60 * 1000))
|
||||
.redirect(redirectUrl.toString());
|
||||
} catch (err) {
|
||||
authLogger.error("OIDC callback failed", err);
|
||||
|
||||
@@ -919,17 +920,45 @@ router.post("/login", async (req, res) => {
|
||||
dataUnlocked: true,
|
||||
});
|
||||
|
||||
return res.json({
|
||||
token,
|
||||
is_admin: !!userRecord.is_admin,
|
||||
username: userRecord.username,
|
||||
});
|
||||
return res
|
||||
.cookie("jwt", token, authManager.getSecureCookieOptions(req, 24 * 60 * 60 * 1000))
|
||||
.json({
|
||||
success: true,
|
||||
is_admin: !!userRecord.is_admin,
|
||||
username: userRecord.username,
|
||||
});
|
||||
} catch (err) {
|
||||
authLogger.error("Failed to log in user", err);
|
||||
return res.status(500).json({ error: "Login failed" });
|
||||
}
|
||||
});
|
||||
|
||||
// Route: Logout user
|
||||
// POST /users/logout
|
||||
router.post("/logout", async (req, res) => {
|
||||
try {
|
||||
// Try to get userId from JWT if available
|
||||
const userId = (req as any).userId;
|
||||
|
||||
if (userId) {
|
||||
// User is authenticated - clear data session
|
||||
authManager.logoutUser(userId);
|
||||
authLogger.info("User logged out", {
|
||||
operation: "user_logout",
|
||||
userId,
|
||||
});
|
||||
}
|
||||
|
||||
// Always clear the JWT cookie
|
||||
return res
|
||||
.clearCookie("jwt", authManager.getSecureCookieOptions(req))
|
||||
.json({ success: true, message: "Logged out successfully" });
|
||||
} catch (err) {
|
||||
authLogger.error("Logout failed", err);
|
||||
return res.status(500).json({ error: "Logout failed" });
|
||||
}
|
||||
});
|
||||
|
||||
// Route: Get current user's info using JWT
|
||||
// GET /users/me
|
||||
router.get("/me", authenticateJWT, async (req: Request, res: Response) => {
|
||||
@@ -1525,11 +1554,13 @@ router.post("/totp/verify-login", async (req, res) => {
|
||||
expiresIn: "50d",
|
||||
});
|
||||
|
||||
return res.json({
|
||||
token,
|
||||
is_admin: !!userRecord.is_admin,
|
||||
username: userRecord.username,
|
||||
});
|
||||
return res
|
||||
.cookie("jwt", token, authManager.getSecureCookieOptions(req, 50 * 24 * 60 * 60 * 1000))
|
||||
.json({
|
||||
success: true,
|
||||
is_admin: !!userRecord.is_admin,
|
||||
username: userRecord.username,
|
||||
});
|
||||
} catch (err) {
|
||||
authLogger.error("TOTP verification failed", err);
|
||||
return res.status(500).json({ error: "TOTP verification failed" });
|
||||
@@ -1895,26 +1926,7 @@ router.get("/data-status", authenticateJWT, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Route: User logout (clear data session)
|
||||
// POST /users/logout
|
||||
router.post("/logout", authenticateJWT, async (req, res) => {
|
||||
const userId = (req as any).userId;
|
||||
|
||||
try {
|
||||
authManager.logoutUser(userId);
|
||||
authLogger.info("User logged out", {
|
||||
operation: "user_logout",
|
||||
userId,
|
||||
});
|
||||
res.json({ message: "Logged out successfully" });
|
||||
} catch (err) {
|
||||
authLogger.error("Logout failed", err, {
|
||||
operation: "logout_error",
|
||||
userId,
|
||||
});
|
||||
res.status(500).json({ error: "Logout failed" });
|
||||
}
|
||||
});
|
||||
// Duplicate logout route removed - handled by the main logout route above
|
||||
|
||||
// Route: Change user password (re-encrypt data keys)
|
||||
// POST /users/change-password
|
||||
@@ -2259,12 +2271,14 @@ router.post("/recovery/login", async (req, res) => {
|
||||
userId: sessionData.userId,
|
||||
});
|
||||
|
||||
res.json({
|
||||
token,
|
||||
is_admin: !!user[0].is_admin,
|
||||
username: user[0].username,
|
||||
message: "Login successful via recovery"
|
||||
});
|
||||
return res
|
||||
.cookie("jwt", token, authManager.getSecureCookieOptions(req, 24 * 60 * 60 * 1000))
|
||||
.json({
|
||||
success: true,
|
||||
is_admin: !!user[0].is_admin,
|
||||
username: user[0].username,
|
||||
message: "Login successful via recovery"
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
authLogger.error("Recovery login failed", error, {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import express from "express";
|
||||
import cors from "cors";
|
||||
import cookieParser from "cookie-parser";
|
||||
import { Client as SSHClient } from "ssh2";
|
||||
import { getDb } from "../database/db/index.js";
|
||||
import { sshCredentials } from "../database/db/schema.js";
|
||||
@@ -49,7 +50,37 @@ const app = express();
|
||||
|
||||
app.use(
|
||||
cors({
|
||||
origin: "*",
|
||||
origin: (origin, callback) => {
|
||||
// Allow requests with no origin (like mobile apps or curl requests)
|
||||
if (!origin) return callback(null, true);
|
||||
|
||||
// Allow localhost and 127.0.0.1 for development
|
||||
const allowedOrigins = [
|
||||
"http://localhost:5173",
|
||||
"http://localhost:3000",
|
||||
"http://127.0.0.1:5173",
|
||||
"http://127.0.0.1:3000"
|
||||
];
|
||||
|
||||
// Allow any HTTPS origin (production deployments)
|
||||
if (origin.startsWith("https://")) {
|
||||
return callback(null, true);
|
||||
}
|
||||
|
||||
// Allow any HTTP origin for self-hosted scenarios
|
||||
if (origin.startsWith("http://")) {
|
||||
return callback(null, true);
|
||||
}
|
||||
|
||||
// Check against allowed development origins
|
||||
if (allowedOrigins.includes(origin)) {
|
||||
return callback(null, true);
|
||||
}
|
||||
|
||||
// Reject other origins
|
||||
callback(new Error("Not allowed by CORS"));
|
||||
},
|
||||
credentials: true,
|
||||
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||
allowedHeaders: [
|
||||
"Content-Type",
|
||||
@@ -59,6 +90,7 @@ app.use(
|
||||
],
|
||||
}),
|
||||
);
|
||||
app.use(cookieParser());
|
||||
app.use(express.json({ limit: "1gb" }));
|
||||
app.use(express.urlencoded({ limit: "1gb", extended: true }));
|
||||
app.use(express.raw({ limit: "5gb", type: "application/octet-stream" }));
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import express from "express";
|
||||
import net from "net";
|
||||
import cors from "cors";
|
||||
import cookieParser from "cookie-parser";
|
||||
import { Client, type ConnectConfig } from "ssh2";
|
||||
import { getDb } from "../database/db/index.js";
|
||||
import { sshData, sshCredentials } from "../database/db/schema.js";
|
||||
@@ -278,7 +279,37 @@ function validateHostId(
|
||||
const app = express();
|
||||
app.use(
|
||||
cors({
|
||||
origin: "*",
|
||||
origin: (origin, callback) => {
|
||||
// Allow requests with no origin (like mobile apps or curl requests)
|
||||
if (!origin) return callback(null, true);
|
||||
|
||||
// Allow localhost and 127.0.0.1 for development
|
||||
const allowedOrigins = [
|
||||
"http://localhost:5173",
|
||||
"http://localhost:3000",
|
||||
"http://127.0.0.1:5173",
|
||||
"http://127.0.0.1:3000"
|
||||
];
|
||||
|
||||
// Allow any HTTPS origin (production deployments)
|
||||
if (origin.startsWith("https://")) {
|
||||
return callback(null, true);
|
||||
}
|
||||
|
||||
// Allow any HTTP origin for self-hosted scenarios
|
||||
if (origin.startsWith("http://")) {
|
||||
return callback(null, true);
|
||||
}
|
||||
|
||||
// Check against allowed development origins
|
||||
if (allowedOrigins.includes(origin)) {
|
||||
return callback(null, true);
|
||||
}
|
||||
|
||||
// Reject other origins
|
||||
callback(new Error("Not allowed by CORS"));
|
||||
},
|
||||
credentials: true,
|
||||
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
||||
allowedHeaders: [
|
||||
"Content-Type",
|
||||
@@ -288,21 +319,7 @@ app.use(
|
||||
],
|
||||
}),
|
||||
);
|
||||
app.use((req, res, next) => {
|
||||
res.header("Access-Control-Allow-Origin", "*");
|
||||
res.header(
|
||||
"Access-Control-Allow-Headers",
|
||||
"Content-Type, Authorization, User-Agent, X-Electron-App",
|
||||
);
|
||||
res.header(
|
||||
"Access-Control-Allow-Methods",
|
||||
"GET, POST, PUT, PATCH, DELETE, OPTIONS",
|
||||
);
|
||||
if (req.method === "OPTIONS") {
|
||||
return res.sendStatus(204);
|
||||
}
|
||||
next();
|
||||
});
|
||||
app.use(cookieParser());
|
||||
app.use(express.json({ limit: "1mb" }));
|
||||
|
||||
// Add authentication middleware - Linus principle: eliminate special cases
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import express from "express";
|
||||
import cors from "cors";
|
||||
import cookieParser from "cookie-parser";
|
||||
import { Client } from "ssh2";
|
||||
import { ChildProcess } from "child_process";
|
||||
import axios from "axios";
|
||||
@@ -20,7 +21,37 @@ import { SystemCrypto } from "../utils/system-crypto.js";
|
||||
const app = express();
|
||||
app.use(
|
||||
cors({
|
||||
origin: "*",
|
||||
origin: (origin, callback) => {
|
||||
// Allow requests with no origin (like mobile apps or curl requests)
|
||||
if (!origin) return callback(null, true);
|
||||
|
||||
// Allow localhost and 127.0.0.1 for development
|
||||
const allowedOrigins = [
|
||||
"http://localhost:5173",
|
||||
"http://localhost:3000",
|
||||
"http://127.0.0.1:5173",
|
||||
"http://127.0.0.1:3000"
|
||||
];
|
||||
|
||||
// Allow any HTTPS origin (production deployments)
|
||||
if (origin.startsWith("https://")) {
|
||||
return callback(null, true);
|
||||
}
|
||||
|
||||
// Allow any HTTP origin for self-hosted scenarios
|
||||
if (origin.startsWith("http://")) {
|
||||
return callback(null, true);
|
||||
}
|
||||
|
||||
// Check against allowed development origins
|
||||
if (allowedOrigins.includes(origin)) {
|
||||
return callback(null, true);
|
||||
}
|
||||
|
||||
// Reject other origins
|
||||
callback(new Error("Not allowed by CORS"));
|
||||
},
|
||||
credentials: true,
|
||||
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||
allowedHeaders: [
|
||||
"Origin",
|
||||
@@ -33,6 +64,7 @@ app.use(
|
||||
],
|
||||
}),
|
||||
);
|
||||
app.use(cookieParser());
|
||||
app.use(express.json());
|
||||
|
||||
const activeTunnels = new Map<string, Client>();
|
||||
|
||||
@@ -207,17 +207,39 @@ class AuthManager {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get secure cookie options based on request
|
||||
*/
|
||||
getSecureCookieOptions(req: any, maxAge: number = 24 * 60 * 60 * 1000) {
|
||||
return {
|
||||
httpOnly: true, // Prevent XSS attacks
|
||||
secure: req.secure || req.headers['x-forwarded-proto'] === 'https', // Detect HTTPS properly
|
||||
sameSite: "strict" as const, // Prevent CSRF attacks
|
||||
maxAge: maxAge, // Session duration in milliseconds
|
||||
path: "/", // Available site-wide
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Authentication middleware
|
||||
*/
|
||||
createAuthMiddleware() {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
const authHeader = req.headers["authorization"];
|
||||
if (!authHeader?.startsWith("Bearer ")) {
|
||||
return res.status(401).json({ error: "Missing Authorization header" });
|
||||
// Try to get JWT from secure HttpOnly cookie first
|
||||
let token = req.cookies?.jwt;
|
||||
|
||||
// Fallback to Authorization header for backward compatibility
|
||||
if (!token) {
|
||||
const authHeader = req.headers["authorization"];
|
||||
if (authHeader?.startsWith("Bearer ")) {
|
||||
token = authHeader.split(" ")[1];
|
||||
}
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).json({ error: "Missing authentication token" });
|
||||
}
|
||||
|
||||
const token = authHeader.split(" ")[1];
|
||||
const payload = await this.verifyJWTToken(token);
|
||||
|
||||
if (!payload) {
|
||||
|
||||
@@ -103,8 +103,8 @@ export function AdminSettings({
|
||||
const [importPassword, setImportPassword] = React.useState("");
|
||||
|
||||
React.useEffect(() => {
|
||||
const jwt = getCookie("jwt");
|
||||
if (!jwt) return;
|
||||
// JWT is now automatically sent via HttpOnly cookies
|
||||
// No need to check for JWT cookie manually
|
||||
|
||||
if (isElectron()) {
|
||||
const serverUrl = (window as any).configuredServerUrl;
|
||||
@@ -147,8 +147,8 @@ export function AdminSettings({
|
||||
}, []);
|
||||
|
||||
const fetchUsers = async () => {
|
||||
const jwt = getCookie("jwt");
|
||||
if (!jwt) return;
|
||||
// JWT is now automatically sent via HttpOnly cookies
|
||||
// No need to check for JWT cookie manually
|
||||
|
||||
if (isElectron()) {
|
||||
const serverUrl = (window as any).configuredServerUrl;
|
||||
@@ -172,7 +172,7 @@ export function AdminSettings({
|
||||
|
||||
const handleToggleRegistration = async (checked: boolean) => {
|
||||
setRegLoading(true);
|
||||
const jwt = getCookie("jwt");
|
||||
// JWT is now automatically sent via HttpOnly cookies
|
||||
try {
|
||||
await updateRegistrationAllowed(checked);
|
||||
setAllowRegistration(checked);
|
||||
@@ -204,7 +204,7 @@ export function AdminSettings({
|
||||
return;
|
||||
}
|
||||
|
||||
const jwt = getCookie("jwt");
|
||||
// JWT is now automatically sent via HttpOnly cookies
|
||||
try {
|
||||
await updateOIDCConfig(oidcConfig);
|
||||
toast.success(t("admin.oidcConfigurationUpdated"));
|
||||
@@ -226,7 +226,7 @@ export function AdminSettings({
|
||||
if (!newAdminUsername.trim()) return;
|
||||
setMakeAdminLoading(true);
|
||||
setMakeAdminError(null);
|
||||
const jwt = getCookie("jwt");
|
||||
// JWT is now automatically sent via HttpOnly cookies
|
||||
try {
|
||||
await makeUserAdmin(newAdminUsername.trim());
|
||||
toast.success(t("admin.userIsNowAdmin", { username: newAdminUsername }));
|
||||
@@ -243,7 +243,7 @@ export function AdminSettings({
|
||||
|
||||
const handleRemoveAdminStatus = async (username: string) => {
|
||||
confirmWithToast(t("admin.removeAdminStatus", { username }), async () => {
|
||||
const jwt = getCookie("jwt");
|
||||
// JWT is now automatically sent via HttpOnly cookies
|
||||
try {
|
||||
await removeAdminStatus(username);
|
||||
toast.success(t("admin.adminStatusRemoved", { username }));
|
||||
@@ -258,7 +258,7 @@ export function AdminSettings({
|
||||
confirmWithToast(
|
||||
t("admin.deleteUser", { username }),
|
||||
async () => {
|
||||
const jwt = getCookie("jwt");
|
||||
// JWT is now automatically sent via HttpOnly cookies
|
||||
try {
|
||||
await deleteUser(username);
|
||||
toast.success(t("admin.userDeletedSuccessfully", { username }));
|
||||
@@ -292,7 +292,7 @@ export function AdminSettings({
|
||||
|
||||
setExportLoading(true);
|
||||
try {
|
||||
const jwt = getCookie("jwt");
|
||||
// JWT is now automatically sent via HttpOnly cookies
|
||||
const apiUrl = isElectron()
|
||||
? `${(window as any).configuredServerUrl}/database/export`
|
||||
: "http://localhost:30001/database/export";
|
||||
@@ -300,9 +300,9 @@ export function AdminSettings({
|
||||
const response = await fetch(apiUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "include", // Include HttpOnly cookies
|
||||
body: JSON.stringify({ password: exportPassword }),
|
||||
});
|
||||
|
||||
@@ -352,7 +352,7 @@ export function AdminSettings({
|
||||
|
||||
setImportLoading(true);
|
||||
try {
|
||||
const jwt = getCookie("jwt");
|
||||
// JWT is now automatically sent via HttpOnly cookies
|
||||
const apiUrl = isElectron()
|
||||
? `${(window as any).configuredServerUrl}/database/import`
|
||||
: "http://localhost:30001/database/import";
|
||||
@@ -364,9 +364,7 @@ export function AdminSettings({
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
credentials: "include", // Include HttpOnly cookies
|
||||
body: formData,
|
||||
});
|
||||
|
||||
|
||||
@@ -27,46 +27,36 @@ function AppContent() {
|
||||
|
||||
useEffect(() => {
|
||||
const checkAuth = () => {
|
||||
const jwt = getCookie("jwt");
|
||||
if (jwt) {
|
||||
setAuthLoading(true);
|
||||
getUserInfo()
|
||||
.then((meRes) => {
|
||||
setIsAuthenticated(true);
|
||||
setIsAdmin(!!meRes.is_admin);
|
||||
setUsername(meRes.username || null);
|
||||
|
||||
// Check if user data is unlocked
|
||||
if (!meRes.data_unlocked) {
|
||||
// Data is locked - user needs to re-authenticate
|
||||
console.warn("User data is locked - re-authentication required");
|
||||
setIsAuthenticated(false);
|
||||
setIsAdmin(false);
|
||||
setUsername(null);
|
||||
document.cookie = "jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
// With HttpOnly cookies, we can't check for JWT presence from frontend
|
||||
// Instead, we'll try to get user info and handle the response
|
||||
setAuthLoading(true);
|
||||
getUserInfo()
|
||||
.then((meRes) => {
|
||||
setIsAuthenticated(true);
|
||||
setIsAdmin(!!meRes.is_admin);
|
||||
setUsername(meRes.username || null);
|
||||
|
||||
// Check if user data is unlocked
|
||||
if (!meRes.data_unlocked) {
|
||||
// Data is locked - user needs to re-authenticate
|
||||
console.warn("User data is locked - re-authentication required");
|
||||
setIsAuthenticated(false);
|
||||
setIsAdmin(false);
|
||||
setUsername(null);
|
||||
|
||||
// Check if this is a session expiration error
|
||||
const errorCode = err?.response?.data?.code;
|
||||
if (errorCode === "SESSION_EXPIRED") {
|
||||
console.warn("Session expired - please log in again");
|
||||
}
|
||||
|
||||
document.cookie =
|
||||
"jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
|
||||
})
|
||||
.finally(() => setAuthLoading(false));
|
||||
} else {
|
||||
setIsAuthenticated(false);
|
||||
setIsAdmin(false);
|
||||
setUsername(null);
|
||||
setAuthLoading(false);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
setIsAuthenticated(false);
|
||||
setIsAdmin(false);
|
||||
setUsername(null);
|
||||
|
||||
// Check if this is a session expiration error
|
||||
const errorCode = err?.response?.data?.code;
|
||||
if (errorCode === "SESSION_EXPIRED") {
|
||||
console.warn("Session expired - please log in again");
|
||||
}
|
||||
})
|
||||
.finally(() => setAuthLoading(false));
|
||||
};
|
||||
|
||||
checkAuth();
|
||||
|
||||
@@ -206,21 +206,13 @@ export function HomepageAuth({
|
||||
return;
|
||||
}
|
||||
|
||||
if (!res || !res.token) {
|
||||
throw new Error(t("errors.noTokenReceived"));
|
||||
if (!res || !res.success) {
|
||||
throw new Error(t("errors.loginFailed"));
|
||||
}
|
||||
|
||||
setCookie("jwt", res.token);
|
||||
|
||||
// DEBUG: Verify JWT was set correctly
|
||||
const verifyJWT = getCookie("jwt");
|
||||
console.log("JWT Set Debug:", {
|
||||
originalToken: res.token.substring(0, 20) + "...",
|
||||
retrievedToken: verifyJWT ? verifyJWT.substring(0, 20) + "..." : null,
|
||||
match: res.token === verifyJWT,
|
||||
tokenLength: res.token.length,
|
||||
retrievedLength: verifyJWT?.length || 0
|
||||
});
|
||||
// JWT token is now automatically set as HttpOnly cookie by backend
|
||||
// No need to manually manage the token on frontend
|
||||
console.log("Login successful - JWT set as secure HttpOnly cookie");
|
||||
|
||||
[meRes] = await Promise.all([getUserInfo()]);
|
||||
|
||||
@@ -254,7 +246,7 @@ export function HomepageAuth({
|
||||
setIsAdmin(false);
|
||||
setUsername(null);
|
||||
setUserId(null);
|
||||
setCookie("jwt", "", -1);
|
||||
// HttpOnly cookies cannot be cleared from JavaScript - backend handles this
|
||||
if (err?.response?.data?.error?.includes("Database")) {
|
||||
setDbConnectionFailed(true);
|
||||
} else {
|
||||
@@ -306,11 +298,8 @@ export function HomepageAuth({
|
||||
try {
|
||||
const response = await loginWithRecovery(localUsername, recoveryTempToken);
|
||||
|
||||
// Auto-login successful - use same cookie mechanism as normal login
|
||||
setCookie("jwt", response.token);
|
||||
|
||||
// DEBUG: Verify JWT was set correctly (same as normal login)
|
||||
const verifyJWT = getCookie("jwt");
|
||||
// JWT token is now automatically set as HttpOnly cookie by backend
|
||||
console.log("Recovery login successful - JWT set as secure HttpOnly cookie");
|
||||
|
||||
setLoggedIn(true);
|
||||
setIsAdmin(response.is_admin);
|
||||
@@ -442,11 +431,12 @@ export function HomepageAuth({
|
||||
try {
|
||||
const res = await verifyTOTPLogin(totpTempToken, totpCode);
|
||||
|
||||
if (!res || !res.token) {
|
||||
throw new Error(t("errors.noTokenReceived"));
|
||||
if (!res || !res.success) {
|
||||
throw new Error(t("errors.loginFailed"));
|
||||
}
|
||||
|
||||
setCookie("jwt", res.token);
|
||||
// JWT token is now automatically set as HttpOnly cookie by backend
|
||||
console.log("TOTP login successful - JWT set as secure HttpOnly cookie");
|
||||
const meRes = await getUserInfo();
|
||||
|
||||
setInternalLoggedIn(true);
|
||||
@@ -515,7 +505,8 @@ export function HomepageAuth({
|
||||
setOidcLoading(true);
|
||||
setError(null);
|
||||
|
||||
setCookie("jwt", token);
|
||||
// JWT token is now automatically set as HttpOnly cookie by backend
|
||||
console.log("OIDC login successful - JWT set as secure HttpOnly cookie");
|
||||
getUserInfo()
|
||||
.then((meRes) => {
|
||||
setInternalLoggedIn(true);
|
||||
@@ -543,7 +534,7 @@ export function HomepageAuth({
|
||||
setIsAdmin(false);
|
||||
setUsername(null);
|
||||
setUserId(null);
|
||||
setCookie("jwt", "", -1);
|
||||
// HttpOnly cookies cannot be cleared from JavaScript - backend handles this
|
||||
window.history.replaceState(
|
||||
{},
|
||||
document.title,
|
||||
@@ -800,7 +791,7 @@ export function HomepageAuth({
|
||||
)}
|
||||
|
||||
{!internalLoggedIn &&
|
||||
(!authLoading || !getCookie("jwt")) &&
|
||||
!authLoading &&
|
||||
!totpRequired && (
|
||||
<>
|
||||
<div className="flex gap-2 mb-6">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState } from "react";
|
||||
import { ChevronUp, User2, HardDrive, Menu, ChevronRight } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getCookie, setCookie, isElectron } from "@/ui/main-axios.ts";
|
||||
import { getCookie, setCookie, isElectron, logoutUser } from "@/ui/main-axios.ts";
|
||||
|
||||
import {
|
||||
Sidebar,
|
||||
@@ -66,14 +66,23 @@ interface SidebarProps {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
if (isElectron()) {
|
||||
localStorage.removeItem("jwt");
|
||||
} else {
|
||||
document.cookie = "jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
|
||||
async function handleLogout() {
|
||||
try {
|
||||
// Call backend logout endpoint to clear HttpOnly cookie and data session
|
||||
await logoutUser();
|
||||
|
||||
// Clear any local storage (for Electron)
|
||||
if (isElectron()) {
|
||||
localStorage.removeItem("jwt");
|
||||
}
|
||||
|
||||
// Reload the page to reset the application state
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
console.error("Logout failed:", error);
|
||||
// Even if logout fails, reload the page to reset state
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
export function LeftSidebar({
|
||||
|
||||
@@ -14,7 +14,7 @@ import { ChevronUp, Menu, User2 } from "lucide-react";
|
||||
import React, { useState, useEffect, useMemo, useCallback } from "react";
|
||||
import { Separator } from "@/components/ui/separator.tsx";
|
||||
import { FolderCard } from "@/ui/Mobile/Apps/Navigation/Hosts/FolderCard.tsx";
|
||||
import { getSSHHosts } from "@/ui/main-axios.ts";
|
||||
import { getSSHHosts, logoutUser } from "@/ui/main-axios.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Input } from "@/components/ui/input.tsx";
|
||||
import {
|
||||
@@ -55,9 +55,18 @@ interface LeftSidebarProps {
|
||||
username?: string | null;
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
document.cookie = "jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
|
||||
window.location.reload();
|
||||
async function handleLogout() {
|
||||
try {
|
||||
// Call backend logout endpoint to clear HttpOnly cookie and data session
|
||||
await logoutUser();
|
||||
|
||||
// Reload the page to reset the application state
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
console.error("Logout failed:", error);
|
||||
// Even if logout fails, reload the page to reset state
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
export function LeftSidebar({
|
||||
|
||||
@@ -176,11 +176,12 @@ export function HomepageAuth({
|
||||
return;
|
||||
}
|
||||
|
||||
if (!res || !res.token) {
|
||||
throw new Error(t("errors.noTokenReceived"));
|
||||
if (!res || !res.success) {
|
||||
throw new Error(t("errors.loginFailed"));
|
||||
}
|
||||
|
||||
setCookie("jwt", res.token);
|
||||
// JWT token is now automatically set as HttpOnly cookie by backend
|
||||
console.log("Login successful - JWT set as secure HttpOnly cookie");
|
||||
[meRes] = await Promise.all([getUserInfo()]);
|
||||
|
||||
setInternalLoggedIn(true);
|
||||
@@ -213,7 +214,7 @@ export function HomepageAuth({
|
||||
setIsAdmin(false);
|
||||
setUsername(null);
|
||||
setUserId(null);
|
||||
setCookie("jwt", "", -1);
|
||||
// HttpOnly cookies cannot be cleared from JavaScript - backend handles this
|
||||
if (err?.response?.data?.error?.includes("Database")) {
|
||||
setDbError(t("errors.databaseConnection"));
|
||||
} else {
|
||||
@@ -321,11 +322,12 @@ export function HomepageAuth({
|
||||
try {
|
||||
const res = await verifyTOTPLogin(totpTempToken, totpCode);
|
||||
|
||||
if (!res || !res.token) {
|
||||
throw new Error(t("errors.noTokenReceived"));
|
||||
if (!res || !res.success) {
|
||||
throw new Error(t("errors.loginFailed"));
|
||||
}
|
||||
|
||||
setCookie("jwt", res.token);
|
||||
// JWT token is now automatically set as HttpOnly cookie by backend
|
||||
console.log("TOTP login successful - JWT set as secure HttpOnly cookie");
|
||||
const meRes = await getUserInfo();
|
||||
|
||||
setInternalLoggedIn(true);
|
||||
@@ -390,11 +392,12 @@ export function HomepageAuth({
|
||||
return;
|
||||
}
|
||||
|
||||
if (success && token) {
|
||||
if (success) {
|
||||
setOidcLoading(true);
|
||||
setError(null);
|
||||
|
||||
setCookie("jwt", token);
|
||||
// JWT token is now automatically set as HttpOnly cookie by backend
|
||||
console.log("OIDC login successful - JWT set as secure HttpOnly cookie");
|
||||
getUserInfo()
|
||||
.then((meRes) => {
|
||||
setInternalLoggedIn(true);
|
||||
@@ -422,7 +425,7 @@ export function HomepageAuth({
|
||||
setIsAdmin(false);
|
||||
setUsername(null);
|
||||
setUserId(null);
|
||||
setCookie("jwt", "", -1);
|
||||
// HttpOnly cookies cannot be cleared from JavaScript - backend handles this
|
||||
window.history.replaceState(
|
||||
{},
|
||||
document.title,
|
||||
@@ -522,7 +525,7 @@ export function HomepageAuth({
|
||||
)}
|
||||
|
||||
{!internalLoggedIn &&
|
||||
(!authLoading || !getCookie("jwt")) &&
|
||||
!authLoading &&
|
||||
!totpRequired && (
|
||||
<>
|
||||
<div className="flex gap-2 mb-6">
|
||||
|
||||
@@ -25,46 +25,36 @@ const AppContent: FC = () => {
|
||||
|
||||
useEffect(() => {
|
||||
const checkAuth = () => {
|
||||
const jwt = getCookie("jwt");
|
||||
if (jwt) {
|
||||
setAuthLoading(true);
|
||||
getUserInfo()
|
||||
.then((meRes) => {
|
||||
setIsAuthenticated(true);
|
||||
setIsAdmin(!!meRes.is_admin);
|
||||
setUsername(meRes.username || null);
|
||||
|
||||
// Check if user data is unlocked
|
||||
if (!meRes.data_unlocked) {
|
||||
// Data is locked - user needs to re-authenticate
|
||||
console.warn("User data is locked - re-authentication required");
|
||||
setIsAuthenticated(false);
|
||||
setIsAdmin(false);
|
||||
setUsername(null);
|
||||
document.cookie = "jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
// With HttpOnly cookies, we can't check for JWT presence from frontend
|
||||
// Instead, we'll try to get user info and handle the response
|
||||
setAuthLoading(true);
|
||||
getUserInfo()
|
||||
.then((meRes) => {
|
||||
setIsAuthenticated(true);
|
||||
setIsAdmin(!!meRes.is_admin);
|
||||
setUsername(meRes.username || null);
|
||||
|
||||
// Check if user data is unlocked
|
||||
if (!meRes.data_unlocked) {
|
||||
// Data is locked - user needs to re-authenticate
|
||||
console.warn("User data is locked - re-authentication required");
|
||||
setIsAuthenticated(false);
|
||||
setIsAdmin(false);
|
||||
setUsername(null);
|
||||
|
||||
// Check if this is a session expiration error
|
||||
const errorCode = err?.response?.data?.code;
|
||||
if (errorCode === "SESSION_EXPIRED") {
|
||||
console.warn("Session expired - please log in again");
|
||||
}
|
||||
|
||||
document.cookie =
|
||||
"jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
|
||||
})
|
||||
.finally(() => setAuthLoading(false));
|
||||
} else {
|
||||
setIsAuthenticated(false);
|
||||
setIsAdmin(false);
|
||||
setUsername(null);
|
||||
setAuthLoading(false);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
setIsAuthenticated(false);
|
||||
setIsAdmin(false);
|
||||
setUsername(null);
|
||||
|
||||
// Check if this is a session expiration error
|
||||
const errorCode = err?.response?.data?.code;
|
||||
if (errorCode === "SESSION_EXPIRED") {
|
||||
console.warn("Session expired - please log in again");
|
||||
}
|
||||
})
|
||||
.finally(() => setAuthLoading(false));
|
||||
};
|
||||
|
||||
checkAuth();
|
||||
|
||||
@@ -15,7 +15,7 @@ import { ChevronUp, Menu, User2 } from "lucide-react";
|
||||
import React, { useState, useEffect, useMemo, useCallback } from "react";
|
||||
import { Separator } from "@/components/ui/separator.tsx";
|
||||
import { FolderCard } from "@/ui/Mobile/Navigation/Hosts/FolderCard.tsx";
|
||||
import { getSSHHosts } from "@/ui/main-axios.ts";
|
||||
import { getSSHHosts, logoutUser } from "@/ui/main-axios.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Input } from "@/components/ui/input.tsx";
|
||||
import {
|
||||
@@ -56,9 +56,18 @@ interface LeftSidebarProps {
|
||||
username?: string | null;
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
document.cookie = "jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
|
||||
window.location.reload();
|
||||
async function handleLogout() {
|
||||
try {
|
||||
// Call backend logout endpoint to clear HttpOnly cookie and data session
|
||||
await logoutUser();
|
||||
|
||||
// Reload the page to reset the application state
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
console.error("Logout failed:", error);
|
||||
// Even if logout fails, reload the page to reset state
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
export function LeftSidebar({
|
||||
|
||||
@@ -62,6 +62,9 @@ export type ServerMetrics = {
|
||||
|
||||
interface AuthResponse {
|
||||
token: string;
|
||||
success?: boolean;
|
||||
is_admin?: boolean;
|
||||
username?: string;
|
||||
}
|
||||
|
||||
interface UserInfo {
|
||||
@@ -112,6 +115,8 @@ export function setCookie(name: string, value: string, days = 7): void {
|
||||
if (isElectron()) {
|
||||
localStorage.setItem(name, value);
|
||||
} else {
|
||||
// Note: For secure authentication, cookies should be set by the backend
|
||||
// This function is kept for backward compatibility with non-auth cookies
|
||||
const expires = new Date(Date.now() + days * 864e5).toUTCString();
|
||||
document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=/`;
|
||||
}
|
||||
@@ -140,6 +145,7 @@ function createApiInstance(
|
||||
baseURL,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
timeout: 30000,
|
||||
withCredentials: true, // Required for HttpOnly cookies to be sent cross-origin
|
||||
});
|
||||
|
||||
instance.interceptors.request.use((config) => {
|
||||
@@ -149,7 +155,6 @@ function createApiInstance(
|
||||
(config as any).startTime = startTime;
|
||||
(config as any).requestId = requestId;
|
||||
|
||||
const token = getCookie("jwt");
|
||||
const method = config.method?.toUpperCase() || "UNKNOWN";
|
||||
const url = config.url || "UNKNOWN";
|
||||
const fullUrl = `${config.baseURL}${url}`;
|
||||
@@ -167,14 +172,8 @@ function createApiInstance(
|
||||
logger.requestStart(method, fullUrl, context);
|
||||
}
|
||||
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
} else if (process.env.NODE_ENV === "development") {
|
||||
authLogger.warn(
|
||||
"No JWT token found, request will be unauthenticated",
|
||||
context,
|
||||
);
|
||||
}
|
||||
// Note: JWT token is now automatically sent via secure HttpOnly cookies
|
||||
// No need to manually set Authorization header for cookie-based auth
|
||||
|
||||
if (isElectron()) {
|
||||
config.headers["X-Electron-App"] = "true";
|
||||
@@ -276,20 +275,19 @@ function createApiInstance(
|
||||
const errorCode = (error.response?.data as any)?.code;
|
||||
const isSessionExpired = errorCode === "SESSION_EXPIRED";
|
||||
|
||||
// Clear authentication state
|
||||
if (isElectron()) {
|
||||
localStorage.removeItem("jwt");
|
||||
} else {
|
||||
document.cookie =
|
||||
"jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
|
||||
// For web, the secure HttpOnly cookie will be cleared by the backend
|
||||
// We can't clear HttpOnly cookies from JavaScript
|
||||
localStorage.removeItem("jwt");
|
||||
}
|
||||
|
||||
// If session expired, show notification and reload page
|
||||
if (isSessionExpired && typeof window !== "undefined") {
|
||||
// Show user-friendly notification
|
||||
console.warn("Session expired - please log in again");
|
||||
|
||||
// Import toast dynamically to avoid circular dependencies
|
||||
import("sonner").then(({ toast }) => {
|
||||
toast.warning("Session expired - please log in again");
|
||||
});
|
||||
@@ -1526,12 +1524,28 @@ export async function loginUser(
|
||||
): Promise<AuthResponse> {
|
||||
try {
|
||||
const response = await authApi.post("/users/login", { username, password });
|
||||
return response.data;
|
||||
// JWT token is now set as secure HttpOnly cookie by backend
|
||||
// Return success status and user info
|
||||
return {
|
||||
token: "cookie-based", // Placeholder since token is in HttpOnly cookie
|
||||
success: response.data.success,
|
||||
is_admin: response.data.is_admin,
|
||||
username: response.data.username,
|
||||
};
|
||||
} catch (error) {
|
||||
handleApiError(error, "login user");
|
||||
}
|
||||
}
|
||||
|
||||
export async function logoutUser(): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
const response = await authApi.post("/users/logout");
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
handleApiError(error, "logout user");
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUserInfo(): Promise<UserInfo> {
|
||||
try {
|
||||
const response = await authApi.get("/users/me");
|
||||
|
||||
Reference in New Issue
Block a user