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