fix: replace the authentication with more secure one

This commit is contained in:
Dorian Niemiec 2024-10-14 17:33:34 +02:00
parent 796b6a71f0
commit 15f309aa7a
15 changed files with 573 additions and 155 deletions

View file

@ -5,6 +5,21 @@ PORT=3000
# If not set, the "attachments" directory in the script root will be used # If not set, the "attachments" directory in the script root will be used
ATTACHMENTS_PATH= ATTACHMENTS_PATH=
# MongoDB connection string
MONGODB_CONNSTRING=
# 256-bit secret key and 128-bit IV for password encryption
# You can generate the secret key using "openssl rand -base64 32" command
# You can generate the IV using "openssl rand -base64 16" command
ENCRYPTION_SECRETKEY=
ENCRYPTION_IV=
# JWT secret and cookie options
# You can generate the JWT secret using "openssl rand -base64 32" command
# If users access the webmail from HTTPS, set JWT_SECURECOOKIE to 1
JWT_SECRET=
JWT_SECURECOOKIE=
# Email receiving protocol and host parameters # Email receiving protocol and host parameters
# The EMAIL_RECV_PROTOCOL can be "pop3" or "imap" # The EMAIL_RECV_PROTOCOL can be "pop3" or "imap"
EMAIL_RECV_PROTOCOL= EMAIL_RECV_PROTOCOL=

View file

@ -2,17 +2,17 @@ import LoginForm from "@/layouts/LoginForm.jsx";
import Loading from "@/layouts/Loading.jsx"; import Loading from "@/layouts/Loading.jsx";
import MainLayout from "@/layouts/MainLayout.jsx"; import MainLayout from "@/layouts/MainLayout.jsx";
import { useSelector, useDispatch } from "react-redux"; import { useSelector, useDispatch } from "react-redux";
import { load, checkAuth } from "@/slices/authSlice.js"; import { checkAuth } from "@/slices/authSlice.js";
import { useEffect } from "react"; import { useEffect } from "react";
function App() { function App() {
const loading = useSelector((state) => state.auth.loading); const loading = useSelector((state) => state.auth.loading);
const auth = useSelector((state) => state.auth.auth); const email = useSelector((state) => state.auth.email);
const dispatch = useDispatch(); const dispatch = useDispatch();
useEffect(() => { useEffect(() => {
setTimeout(() => { setTimeout(() => {
dispatch(load); dispatch(checkAuth);
}, 500); }, 500);
const interval = setInterval(() => { const interval = setInterval(() => {
@ -24,7 +24,7 @@ function App() {
if (loading) { if (loading) {
return <Loading />; return <Loading />;
} else if (auth === null) { } else if (email === null) {
return <LoginForm />; return <LoginForm />;
} else { } else {
return <MainLayout />; return <MainLayout />;

View file

@ -20,9 +20,7 @@ import Content from "@/components/Content.jsx";
import { setView } from "@/slices/viewSlice.js"; import { setView } from "@/slices/viewSlice.js";
function LoginLayout() { function LoginLayout() {
const email = useSelector((state) => const email = useSelector((state) => state.auth.email);
state.auth.auth ? state.auth.auth.email : "Unknown"
);
const menuShown = useSelector((state) => state.menu.shown); const menuShown = useSelector((state) => state.menu.shown);
const dispatch = useDispatch(); const dispatch = useDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
@ -135,7 +133,7 @@ function LoginLayout() {
href="#" href="#"
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
dispatch(logout()); dispatch(logout);
}} }}
className="inline-block text-inherit w-8 h-8 py-1 mx-0.5 align-middle rounded-sm hover:bg-primary-foreground/30 hover:text-primary-foreground transition-colors" className="inline-block text-inherit w-8 h-8 py-1 mx-0.5 align-middle rounded-sm hover:bg-primary-foreground/30 hover:text-primary-foreground transition-colors"
> >

View file

@ -5,111 +5,49 @@ export const authSlice = createSlice({
initialState: { initialState: {
loading: true, loading: true,
error: null, error: null,
auth: null email: null
}, },
reducers: { reducers: {
load: (state, action) => {
if (state.loading) state.loading = false;
if (action.payload && action.payload.error !== undefined)
state.error = action.payload.error;
if (action.payload && action.payload.auth !== undefined) {
state.auth = action.payload.auth;
if (!localStorage.getItem("credentials"))
localStorage.setItem(
"credentials",
btoa(JSON.stringify(action.payload.auth))
);
}
},
login: (state, action) => { login: (state, action) => {
if (state.loading) state.loading = false; if (state.loading) state.loading = false;
if (action.payload && action.payload.error !== undefined) if (action.payload && action.payload.error !== undefined)
state.error = action.payload.error; state.error = action.payload.error;
if (action.payload && action.payload.auth !== undefined) { if (action.payload && action.payload.email !== undefined) {
state.auth = action.payload.auth; state.email = action.payload.email;
localStorage.setItem(
"credentials",
btoa(JSON.stringify(action.payload.auth))
);
} }
}, },
logout: (state) => { logout: (state) => {
if (state.loading) state.loading = false; if (state.loading) state.loading = false;
localStorage.removeItem("credentials"); state.email = null;
state.auth = null;
}, },
verificationFailed: (state) => { verificationFailed: (state) => {
if (state.loading) state.loading = false; if (state.loading) state.loading = false;
state.error = null; state.error = null;
state.auth = null; state.email = null;
} }
} }
}); });
export const { logout, verificationFailed } = authSlice.actions; export const { verificationFailed } = authSlice.actions;
export async function load(dispatch) {
const state = {};
let credentials = {};
try {
credentials = JSON.parse(atob(localStorage.getItem("credentials")));
// eslint-disable-next-line no-unused-vars
} catch (err) {
// Use empty credentials
}
try {
const res = await fetch("/api/check", {
method: "GET",
headers: {
Authorization:
credentials.email && credentials.password
? "BasicMERNMail " +
btoa(
credentials.email.replace(/:/g, "") + ":" + credentials.password
)
: undefined
}
});
if (res.status == 200) {
state.auth =
credentials.email && credentials.password
? { email: credentials.email, password: credentials.password }
: {};
}
} catch (err) {
state.error = err.message;
}
dispatch(authSlice.actions.load(state));
}
export function login(email, password) { export function login(email, password) {
return async (dispatch) => { return async (dispatch) => {
const state = {}; const state = {};
let credentials = {
email: email,
password: password
};
try { try {
const res = await fetch("/api/check", { const res = await fetch("/api/login", {
method: "GET", method: "POST",
headers: { headers: {
Authorization: "Content-Type": "application/json"
credentials.email && credentials.password },
? "BasicMERNMail " + body: JSON.stringify({
btoa( email: email,
credentials.email.replace(/:/g, "") + password: password
":" + }),
credentials.password credentials: "include"
)
: undefined
}
}); });
const data = await res.json(); const data = await res.json();
if (res.status == 200) { if (res.status == 200) {
state.auth = state.email = email;
credentials.email && credentials.password
? { email: credentials.email, password: credentials.password }
: {};
state.error = null; state.error = null;
} else { } else {
state.error = data.message; state.error = data.message;
@ -121,38 +59,38 @@ export function login(email, password) {
}; };
} }
export async function checkAuth(dispatch, getState) { export async function checkAuth(dispatch) {
const state = {}; const state = {};
let credentials = getState().auth.auth;
if (credentials === null) {
return;
}
try { try {
const res = await fetch("/api/check", { const res = await fetch("/api/check", {
method: "GET", method: "GET",
headers: { credentials: "include"
Authorization:
credentials.email && credentials.password
? "BasicMERNMail " +
btoa(
credentials.email.replace(/:/g, "") + ":" + credentials.password
)
: undefined
}
}); });
if (res.status == 200) { if (res.status == 200) {
state.auth = const data = await res.json();
credentials.email && credentials.password state.email = data.email;
? { email: credentials.email, password: credentials.password }
: {};
} else {
state.auth = null;
} }
} catch (err) {
state.error = err.message;
}
dispatch(authSlice.actions.login(state));
}
export async function logout(dispatch) {
try {
await fetch("/api/logout", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({}),
credentials: "include"
});
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
} catch (err) { } catch (err) {
// Don't display the message // Logout failed
} }
dispatch(authSlice.actions.load(state)); dispatch(authSlice.actions.logout());
} }
export default authSlice.reducer; export default authSlice.reducer;

View file

@ -75,22 +75,14 @@ export const { initCurrentMailbox, setCurrentMailboxFromURL } =
export async function setMailboxes(dispatch, getState) { export async function setMailboxes(dispatch, getState) {
const state = {}; const state = {};
let credentials = getState().auth.auth; let email = getState().auth.email;
if (credentials === null) { if (email === null) {
return; return;
} }
try { try {
const res = await fetch("/api/receive/mailboxes", { const res = await fetch("/api/receive/mailboxes", {
method: "GET", method: "GET",
headers: { credentials: "include"
Authorization:
credentials.email && credentials.password
? "BasicMERNMail " +
btoa(
credentials.email.replace(/:/g, "") + ":" + credentials.password
)
: undefined
}
}); });
const data = await res.json(); const data = await res.json();
if (res.status == 200) { if (res.status == 200) {

View file

@ -29,8 +29,8 @@ export const { resetLoading } = messagesSlice.actions;
export function setMessages(signal) { export function setMessages(signal) {
return async (dispatch, getState) => { return async (dispatch, getState) => {
const state = {}; const state = {};
let credentials = getState().auth.auth; let email = getState().auth.email;
if (credentials === null) { if (email === null) {
return; return;
} }
let currentMailbox = getState().mailboxes.currentMailbox; let currentMailbox = getState().mailboxes.currentMailbox;
@ -45,17 +45,7 @@ export function setMessages(signal) {
try { try {
const res = await fetch(`/api/receive/mailbox/${currentMailbox}`, { const res = await fetch(`/api/receive/mailbox/${currentMailbox}`, {
method: "GET", method: "GET",
headers: { credentials: "include",
Authorization:
credentials.email && credentials.password
? "BasicMERNMail " +
btoa(
credentials.email.replace(/:/g, "") +
":" +
credentials.password
)
: undefined
},
signal: signal signal: signal
}); });
const data = await res.json(); const data = await res.json();

323
package-lock.json generated
View file

@ -9,10 +9,14 @@
"version": "0.0.0", "version": "0.0.0",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"body-parser": "^1.20.3",
"cookie-parser": "^1.4.7",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"express": "^4.21.0", "express": "^4.21.0",
"imap-node": "^0.9.9", "imap-node": "^0.9.9",
"jsonwebtoken": "^9.0.2",
"mailparser": "^3.7.1", "mailparser": "^3.7.1",
"mongoose": "^8.7.1",
"node-pop3": "^0.9.0", "node-pop3": "^0.9.0",
"nodemailer": "^6.9.15", "nodemailer": "^6.9.15",
"serve-static": "^1.16.2" "serve-static": "^1.16.2"
@ -862,6 +866,14 @@
"node": "^12.20.0 || ^14.13.1 || >=16.0.0" "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
} }
}, },
"node_modules/@mongodb-js/saslprep": {
"version": "1.1.9",
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.9.tgz",
"integrity": "sha512-tVkljjeEaAhCqTzajSdgbQ6gE6f3oneVwa3iXR6csiEwXXOFsiC6Uh9iAjAhXPtqa/XMDHWjjeNH/77m/Yq2dw==",
"dependencies": {
"sparse-bitfield": "^3.0.3"
}
},
"node_modules/@napi-rs/nice": { "node_modules/@napi-rs/nice": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.0.1.tgz",
@ -1238,6 +1250,19 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/webidl-conversions": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
"integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA=="
},
"node_modules/@types/whatwg-url": {
"version": "11.0.5",
"resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz",
"integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==",
"dependencies": {
"@types/webidl-conversions": "*"
}
},
"node_modules/accepts": { "node_modules/accepts": {
"version": "1.3.8", "version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@ -1643,6 +1668,14 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/bson": {
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/bson/-/bson-6.8.0.tgz",
"integrity": "sha512-iOJg8pr7wq2tg/zSlCCHMi3hMm5JTOxLTagf3zxhcenHsFp+c6uOs6K7W5UE7A4QIJGtqh/ZovFNMP4mOPJynQ==",
"engines": {
"node": ">=16.20.1"
}
},
"node_modules/buffer": { "node_modules/buffer": {
"version": "5.7.1", "version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
@ -1667,6 +1700,11 @@
"ieee754": "^1.1.13" "ieee754": "^1.1.13"
} }
}, },
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
},
"node_modules/bytes": { "node_modules/bytes": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@ -2173,6 +2211,26 @@
"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==",
"dependencies": {
"cookie": "0.7.2",
"cookie-signature": "1.0.6"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/cookie-parser/node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": { "node_modules/cookie-signature": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
@ -2542,6 +2600,14 @@
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"dev": true "dev": true
}, },
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"dependencies": {
"safe-buffer": "^5.0.1"
}
},
"node_modules/ee-first": { "node_modules/ee-first": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@ -4294,6 +4360,59 @@
"node": "*" "node": "*"
} }
}, },
"node_modules/jsonwebtoken": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
"integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
"dependencies": {
"jws": "^3.2.2",
"lodash.includes": "^4.3.0",
"lodash.isboolean": "^3.0.3",
"lodash.isinteger": "^4.0.4",
"lodash.isnumber": "^3.0.3",
"lodash.isplainobject": "^4.0.6",
"lodash.isstring": "^4.0.1",
"lodash.once": "^4.0.0",
"ms": "^2.1.1",
"semver": "^7.5.4"
},
"engines": {
"node": ">=12",
"npm": ">=6"
}
},
"node_modules/jsonwebtoken/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/jwa": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
"integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
"dependencies": {
"buffer-equal-constant-time": "1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jws": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
"dependencies": {
"jwa": "^1.4.1",
"safe-buffer": "^5.0.1"
}
},
"node_modules/kareem": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz",
"integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/keyv": { "node_modules/keyv": {
"version": "4.5.4", "version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@ -4769,11 +4888,35 @@
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
"dev": true "dev": true
}, },
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="
},
"node_modules/lodash.isboolean": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="
},
"node_modules/lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="
},
"node_modules/lodash.isnumber": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="
},
"node_modules/lodash.isplainobject": { "node_modules/lodash.isplainobject": {
"version": "4.0.6", "version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="
"dev": true },
"node_modules/lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="
}, },
"node_modules/lodash.kebabcase": { "node_modules/lodash.kebabcase": {
"version": "4.1.1", "version": "4.1.1",
@ -4799,6 +4942,11 @@
"integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==",
"dev": true "dev": true
}, },
"node_modules/lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="
},
"node_modules/lodash.snakecase": { "node_modules/lodash.snakecase": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz",
@ -5090,6 +5238,11 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/memory-pager": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
"integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg=="
},
"node_modules/meow": { "node_modules/meow": {
"version": "12.1.1", "version": "12.1.1",
"resolved": "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz", "resolved": "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz",
@ -5233,6 +5386,126 @@
"node": ">=16 || 14 >=14.17" "node": ">=16 || 14 >=14.17"
} }
}, },
"node_modules/mongodb": {
"version": "6.9.0",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.9.0.tgz",
"integrity": "sha512-UMopBVx1LmEUbW/QE0Hw18u583PEDVQmUmVzzBRH0o/xtE9DBRA5ZYLOjpLIa03i8FXjzvQECJcqoMvCXftTUA==",
"dependencies": {
"@mongodb-js/saslprep": "^1.1.5",
"bson": "^6.7.0",
"mongodb-connection-string-url": "^3.0.0"
},
"engines": {
"node": ">=16.20.1"
},
"peerDependencies": {
"@aws-sdk/credential-providers": "^3.188.0",
"@mongodb-js/zstd": "^1.1.0",
"gcp-metadata": "^5.2.0",
"kerberos": "^2.0.1",
"mongodb-client-encryption": ">=6.0.0 <7",
"snappy": "^7.2.2",
"socks": "^2.7.1"
},
"peerDependenciesMeta": {
"@aws-sdk/credential-providers": {
"optional": true
},
"@mongodb-js/zstd": {
"optional": true
},
"gcp-metadata": {
"optional": true
},
"kerberos": {
"optional": true
},
"mongodb-client-encryption": {
"optional": true
},
"snappy": {
"optional": true
},
"socks": {
"optional": true
}
}
},
"node_modules/mongodb-connection-string-url": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.1.tgz",
"integrity": "sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg==",
"dependencies": {
"@types/whatwg-url": "^11.0.2",
"whatwg-url": "^13.0.0"
}
},
"node_modules/mongoose": {
"version": "8.7.1",
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.7.1.tgz",
"integrity": "sha512-RpNMyhyzLVCVbf8xTVbrf/18G3MqQzNw5pJdvOJ60fzbCa3cOZzz9L+8XpqzBXtRlgZGWv0T7MmOtvrT8ocp1Q==",
"dependencies": {
"bson": "^6.7.0",
"kareem": "2.6.3",
"mongodb": "6.9.0",
"mpath": "0.9.0",
"mquery": "5.0.0",
"ms": "2.1.3",
"sift": "17.1.3"
},
"engines": {
"node": ">=16.20.1"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mongoose"
}
},
"node_modules/mongoose/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/mpath": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz",
"integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/mquery": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz",
"integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==",
"dependencies": {
"debug": "4.x"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/mquery/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/mquery/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/ms": { "node_modules/ms": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@ -5824,7 +6097,6 @@
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"dev": true,
"engines": { "engines": {
"node": ">=6" "node": ">=6"
} }
@ -6158,7 +6430,6 @@
"version": "7.6.3", "version": "7.6.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
"dev": true,
"bin": { "bin": {
"semver": "bin/semver.js" "semver": "bin/semver.js"
}, },
@ -6311,6 +6582,11 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/sift": {
"version": "17.1.3",
"resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz",
"integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ=="
},
"node_modules/signal-exit": { "node_modules/signal-exit": {
"version": "3.0.7", "version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
@ -6411,6 +6687,14 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/sparse-bitfield": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
"integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
"dependencies": {
"memory-pager": "^1.0.2"
}
},
"node_modules/split2": { "node_modules/split2": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
@ -6694,6 +6978,17 @@
"nodetouch": "bin/nodetouch.js" "nodetouch": "bin/nodetouch.js"
} }
}, },
"node_modules/tr46": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz",
"integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==",
"dependencies": {
"punycode": "^2.3.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/tree-kill": { "node_modules/tree-kill": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
@ -6872,6 +7167,26 @@
"defaults": "^1.0.3" "defaults": "^1.0.3"
} }
}, },
"node_modules/webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
"engines": {
"node": ">=12"
}
},
"node_modules/whatwg-url": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz",
"integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==",
"dependencies": {
"tr46": "^4.1.1",
"webidl-conversions": "^7.0.0"
},
"engines": {
"node": ">=16"
}
},
"node_modules/which": { "node_modules/which": {
"version": "1.3.1", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",

View file

@ -20,10 +20,14 @@
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"dependencies": { "dependencies": {
"body-parser": "^1.20.3",
"cookie-parser": "^1.4.7",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"express": "^4.21.0", "express": "^4.21.0",
"imap-node": "^0.9.9", "imap-node": "^0.9.9",
"jsonwebtoken": "^9.0.2",
"mailparser": "^3.7.1", "mailparser": "^3.7.1",
"mongoose": "^8.7.1",
"node-pop3": "^0.9.0", "node-pop3": "^0.9.0",
"nodemailer": "^6.9.15", "nodemailer": "^6.9.15",
"serve-static": "^1.16.2" "serve-static": "^1.16.2"

View file

@ -13,14 +13,31 @@ if (!process.env.ATTACHMENTS_PATH) {
} }
} }
const express = require("express"); const express = require("express");
const mongoose = require("mongoose");
const serveStatic = require("serve-static"); const serveStatic = require("serve-static");
const authAndInitReceiveMiddleware = require("./middleware/authAndInitReceive.js"); const authAndInitReceiveMiddleware = require("./middleware/authAndInitReceive.js");
const checkRoute = require("./routes/check.js"); const checkRoute = require("./routes/check.js");
const loginRoute = require("./routes/login.js");
const logoutRoute = require("./routes/logout.js");
const receiveRoute = require("./routes/receive.js"); const receiveRoute = require("./routes/receive.js");
const bodyParser = require("body-parser");
const cookieParser = require("cookie-parser");
mongoose
.connect(process.env.MONGODB_CONNSTRING)
.then(() => console.log(`Database connected successfully`));
// Since mongoose's Promise is deprecated, we override it with Node's Promise
mongoose.Promise = global.Promise;
const app = express(); const app = express();
app.use(cookieParser());
app.use("/api", bodyParser.json());
app.use("/api/login", loginRoute);
app.use("/api", authAndInitReceiveMiddleware); app.use("/api", authAndInitReceiveMiddleware);
app.use("/api/logout", logoutRoute);
app.use("/api/check", checkRoute); app.use("/api/check", checkRoute);
app.use("/api/receive", receiveRoute); app.use("/api/receive", receiveRoute);
app.use("/api", (req, res, next) => { app.use("/api", (req, res, next) => {

View file

@ -1,23 +1,41 @@
const initReceiveDriver = require("../utils/initReceiveDriver.js"); const initReceiveDriver = require("../utils/initReceiveDriver.js");
const { decryptPassword } = require("../utils/passwordCrypto.js");
const userModel = require("../models/user.js");
const jwt = require("jsonwebtoken");
module.exports = function authAndInitReceiveMiddleware(req, res, next) { module.exports = function authAndInitReceiveMiddleware(req, res, next) {
// Custom implementation of HTTP Basic authentication jwt.verify(req.cookies.token, process.env.JWT_SECRET, (err, decoded) => {
const deny = (message) => { if (err) {
res.set("WWW-Authenticate", "BasicMERNMail"); res.status(401).json({ message: err.message });
res.status(401).json({ message: message || "Authentication required." }); return;
}; }
const email = decoded.email;
const authTPair = (req.headers.authorization || "").split(" "); userModel
if (authTPair[0] != "Basic" && authTPair[0] != "BasicMERNMail") { .findOne({ email: email })
deny(); .then((result) => {
return; if (!result) {
} res.status(401).json({ message: "User not found" });
const b64auth = authTPair[1] || ""; } else {
const authPair = Buffer.from(b64auth, "base64").toString().split(":"); const password = decryptPassword(result.encryptedPassword);
const email = authPair[0]; initReceiveDriver(email, password, (err, driver) => {
const password = authPair[1]; if (err) {
res.status(401).json({ message: err.message });
if (email && password) { } else {
req.credentials = {
email: email,
password: password
};
req.receiveDriver = driver;
next();
}
});
}
})
.catch((err) => {
res.status(500).json({ message: err.message });
});
});
/*if (email && password) {
initReceiveDriver(email, password, (err, driver) => { initReceiveDriver(email, password, (err, driver) => {
if (err) { if (err) {
deny(err.message); deny(err.message);
@ -32,5 +50,5 @@ module.exports = function authAndInitReceiveMiddleware(req, res, next) {
}); });
} else { } else {
deny(); deny();
} }*/
}; };

18
src/models/user.js Normal file
View file

@ -0,0 +1,18 @@
// models/User.js
const mongoose = require("mongoose");
const userSchema = new mongoose.Schema({
email: {
type: String,
required: true,
unique: true
},
encryptedPassword: {
type: String,
required: true
}
});
const User = mongoose.model("users", userSchema);
module.exports = User;

View file

@ -3,7 +3,7 @@ const router = express.Router();
router.get("/", (req, res) => { router.get("/", (req, res) => {
req.receiveDriver.close(); req.receiveDriver.close();
res.json({ message: "OK!" }); res.json({ email: req.credentials.email });
}); });
module.exports = router; module.exports = router;

76
src/routes/login.js Normal file
View file

@ -0,0 +1,76 @@
const express = require("express");
const jwt = require("jsonwebtoken");
const userModel = require("../models/user.js");
const initReceiveDriver = require("../utils/initReceiveDriver.js");
const { encryptPassword } = require("../utils/passwordCrypto.js");
const router = express.Router();
router.post("/", (req, res) => {
if (!req.body || !req.body.email || !req.body.password)
res.status(400).json({ message: "Email and password are required" });
initReceiveDriver(
String(req.body.email),
String(req.body.password),
(err, driver) => {
if (err) {
res.status(401).json({ message: err.message });
} else {
driver.close();
userModel
.findOne({ email: req.body.email })
.then((result) => {
const createCallback = () => {
jwt.sign(
{ email: req.body.email },
process.env.JWT_SECRET,
(err, token) => {
if (err) {
res.status(401).json({ message: err.message });
} else {
res.cookie("token", token, {
maxAge: 315360000000,
httpOnly: true,
secure: parseInt(process.env.JWT_SECURECOOKIE) > 0,
sameSite: "strict"
});
res.json({ message: "Logged in successfully" });
}
}
);
};
const encryptedPassword = encryptPassword(req.body.password);
if (!result) {
userModel
.create({
email: req.body.email,
encryptedPassword: encryptedPassword
})
.then(createCallback)
.catch((err) => {
res.status(500).json({ message: err.message });
});
} else {
userModel
.replaceOne(
{ email: req.body.email },
{
email: req.body.email,
encryptedPassword: encryptedPassword
}
)
.then(createCallback)
.catch((err) => {
res.status(500).json({ message: err.message });
});
}
})
.catch((err) => {
res.status(500).json({ message: err.message });
});
}
}
);
});
module.exports = router;

11
src/routes/logout.js Normal file
View file

@ -0,0 +1,11 @@
const express = require("express");
const router = express.Router();
// POST method to prevent logout CSRF
router.post("/", (req, res) => {
req.receiveDriver.close();
res.clearCookie("token");
res.json({ message: "Logged out successfully" });
});
module.exports = router;

View file

@ -0,0 +1,26 @@
const crypto = require("crypto");
// Decode the base64 strings to buffers
const secretKey = Buffer.from(process.env.ENCRYPTION_SECRETKEY, "base64");
const iv = Buffer.from(process.env.ENCRYPTION_IV, "base64");
// Encrypt the password
function encryptPassword(password) {
const cipher = crypto.createCipheriv("aes-256-cbc", secretKey, iv);
let encrypted = cipher.update(password, "utf8", "hex");
encrypted += cipher.final("hex");
return encrypted;
}
// Decrypt the password
function decryptPassword(encryptedPassword) {
const decipher = crypto.createDecipheriv("aes-256-cbc", secretKey, iv);
let decrypted = decipher.update(encryptedPassword, "hex", "utf8");
decrypted += decipher.final("utf8");
return decrypted;
}
module.exports = {
encryptPassword: encryptPassword,
decryptPassword: decryptPassword
};