aedes icon indicating copy to clipboard operation
aedes copied to clipboard

[feat] New example showing websocket authentication via HTTP form

Open faceless2 opened this issue 2 years ago • 3 comments

Not exactly a feature request, but: the supplied examples are very simple and didn't cover a use case I wanted, which was to allow a WebSocket connection to pre-log-in with an HTTP form and store the session details in a cookie, rather than by using the username/password in MQTT CONNECT. This way the login session can survive a page reload.

I've trimmed this down to the absolute basics and am posting here for posterity - if it forms the basis of an official example, great, but if not perhaps it will be useful for someone searching the issues.

Either way I don't require any action so please feel free to close this issue.

const aedes = require("aedes")();
const aedespd = require("aedes-protocol-decoder");
const ws = require("ws");
const http = require("http");
const uuid = require("uuid");
const cookie = require("cookie");

const httpServer = http.createServer((req, res) => {
    let data = "";
    req.on("data", chunk => {
        data += chunk;
    });
    req.on("end", () => {
        try {
            if (req.url == "/login") {
                data = JSON.parse(data);
                let username = data.username;
                let password = data.password;
                aedes.authenticate(null, username, password, (error, success) => {
                    if (success) {
                        let session = { username: username, id: uuid.v4() };
                        if (!aedes._websessions) {
                            aedes._websessions = {};
                        }
                        aedes._websessions[session.id] = session;
                        res.setHeader("Set-Cookie", cookie.serialize("session", session.id));
                        res.writeHead(204);
                        res.end();
                    } else {
                        // Login failed: if user was already logged in, invalidate their session
                        let id = cookie.parse(req.headers.cookie || "").session;
                        if (id && aedes._websessions) {
                            delete aedes._websessions[id];
                        }
                        res.writeHead(401);
                        res.end("Unauthorized", "UTF-8");
                    }
                });
            } else if (req.url == "/logout") {
                let id = cookie.parse(req.headers.cookie || "").session;
                if (id && aedes._websessions) {
                    delete aedes._websessions[id];
                    res.writeHead(204);
                    res.end();
                }
            } else {
                // Serve static files here, or... 
                res.writeHead(404);
                res.end("Not found", "UTF-8");
            }
        } catch (e) {
            res.writeHead(500);
            res.end("Error: "+e, "UTF-8");
        }
    });
});

aedes.authenticate = function(client, username, password, callback) {
    // If called from HTTP connection, client is null
    if (client && client.req) {
        let id = cookie.parse(client.req.headers.cookie || "").session;
        let session = client.broker._websessions ? client.broker._websessions[id] : null;
        if (session) {
            // client already logged in via HTTP.
            client.username = session.username;
            client.session = session;
            callback(null, true);
        } else {
            callback(new Error("Unauthorized"), false);
        }
    } else if (username == "admin" && password == "admin") {
        callback(null, true);
    } else {
        callback(new Error("Unauthorized"), false);
    }
}

const wss = new ws.Server({ server: httpServer });
wss.on("connection", (conn, req) => {
  const stream = ws.createWebSocketStream(conn)
  stream._socket = conn._socket
  req.connDetails = aedespd.extractSocketDetails(stream.socket || stream)
  aedes.handle(stream, req)
});

const mqttServer = require("net").createServer(aedes.handle);
mqttServer.listen(1883, function() {
    console.log("listen on 1883");
});

httpServer.listen(8080, function() {
    console.log("listen on 8080");
});

faceless2 avatar Aug 18 '21 11:08 faceless2

Feel free to open a PR to add the example

robertsLando avatar Aug 23 '21 07:08 robertsLando

I like this - creative thinking. I'm in the process of updating a mosca production system to aedes. The very fact that we can adapt so easily to different scenarios is such a bonus - I can do far more in 40 lines of JS with aedes that I could ever do with mosquito or any commercial equivalent subpub solution.

I use JWTs to authenticate clients, but also then capture the decoded token into the client to authorize subscribe and publish. But it did occur to me that I have JWTs for web authentication against http endpoints. A solid example of how to use http authentication to refuse a ws: at the point of UPGRADE would be a useful addition to aedes examples - so that the client did not even get near MQTT if they failed the first test at establishing a ws: connection. Of course, I've not looked at client code to see if this is easy at that end....

btsimonh avatar Feb 27 '24 17:02 btsimonh

@btsimonh Maybe checking how aedes-server-factory works could help: https://github.com/moscajs/aedes-server-factory/blob/main/index.js#L61

robertsLando avatar Feb 28 '24 07:02 robertsLando