seitime-frappe/realtime/middlewares/authenticate.js
Aarol D'Souza 5bb1dffab5
fix!: use secret for auth. between servers (#36778)
* fix: use secret for auth. between servers

* fix(security): use redis for server auth.

* fix: use socket.io directly to fetch secret from redis

* refactor: Socket secret can be bench specific

- No need to keep it site specific.

* fix: don't return anything if secrets dont match

* test: rewrite test to factor in server-to-server communication only

---------

Co-authored-by: Ankush Menat <ankush@frappe.io>
2026-02-17 11:55:00 +05:30

115 lines
3.3 KiB
JavaScript

const cookie = require("cookie");
const { get_conf, get_redis_subscriber } = require("../../node_utils");
const { get_url } = require("../utils");
const conf = get_conf();
const redisClient = get_redis_subscriber("redis_queue");
async function getSecretFromRedis() {
if (!redisClient.isOpen) await redisClient.connect();
const val = await redisClient.get("socketio_auth_secret");
return val;
}
function authenticate_with_frappe(socket, next) {
let namespace = socket.nsp.name;
namespace = namespace.slice(1, namespace.length); // remove leading `/`
if (namespace != get_site_name(socket)) {
next(new Error("Invalid namespace"));
}
if (get_hostname(socket.request.headers.host) != get_hostname(socket.request.headers.origin)) {
next(new Error("Invalid origin"));
return;
}
if (!socket.request.headers.cookie && !socket.request.headers.authorization) {
next(
new Error(
"Missing cookie and authorization header. Either one needed for authentication."
)
);
return;
}
let cookies = cookie.parse(socket.request.headers.cookie || "");
let authorization_header = socket.request.headers.authorization;
if (!cookies.sid && !authorization_header) {
next(new Error("No authentication method used. Use cookie or authorization header."));
return;
}
socket.sid = cookies.sid;
socket.authorization_header = authorization_header;
socket.frappe_request = async (path, args = {}, opts = {}) => {
let query_args = new URLSearchParams(args);
if (query_args.toString()) {
path = path + "?" + query_args.toString();
}
let headers = {};
if (socket.authorization_header) {
headers["Authorization"] = socket.authorization_header;
} else if (socket.sid) {
headers["Cookie"] = `sid=${socket.sid}`;
}
const secret = await getSecretFromRedis();
if (secret) {
headers["X-Frappe-Socket-Secret"] = secret;
}
return fetch(get_url(socket, path), {
...opts,
headers,
});
};
socket
.frappe_request("/api/method/frappe.realtime.get_user_info")
.then((res) => res.json())
.then(async ({ message }) => {
if (socket.user !== "Guest" && !message.installed_apps) {
const retry_res = await socket.frappe_request(
"/api/method/frappe.realtime.get_user_info"
);
const retry_data = await retry_res.json();
message = retry_data.message;
}
socket.user = message.user;
socket.user_type = message.user_type;
socket.installed_apps = message.installed_apps || [];
next();
})
.catch((e) => {
next(new Error(`Unauthorized: ${e}`));
});
}
function get_site_name(socket) {
if (socket.site_name) {
return socket.site_name;
} else if (socket.request.headers["x-frappe-site-name"]) {
socket.site_name = get_hostname(socket.request.headers["x-frappe-site-name"]);
} else if (
conf.default_site &&
["localhost", "127.0.0.1"].indexOf(get_hostname(socket.request.headers.host)) !== -1
) {
socket.site_name = conf.default_site;
} else if (socket.request.headers.origin) {
socket.site_name = get_hostname(socket.request.headers.origin);
} else {
socket.site_name = get_hostname(socket.request.headers.host);
}
return socket.site_name;
}
function get_hostname(url) {
if (!url) return undefined;
if (url.indexOf("://") > -1) {
url = url.split("/")[2];
}
return url.match(/:/g) ? url.slice(0, url.indexOf(":")) : url;
}
module.exports = authenticate_with_frappe;