diff --git a/README.md b/README.md index a6411f8..30e1f66 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,12 @@ See instructions on our Wiki about [how to setup a reverse proxy for WBO](https: WBO is available in multiple languages. The translations are stored in [`server/translations.json`](./server/translations.json). If you feel like contributing to this collaborative project, you can [translate WBO into your own language](https://github.com/lovasoa/whitebophir/wiki/How-to-translate-WBO-into-your-own-language). +## Authentication + +WBO supports authentication with a JWT. This should be passed in as a query with the key `token`, eg, `http://myboard.com/boards/test?token={token}` + +The `AUTH_SECRET_KEY` variable in [`configuration.js`](./server/configuration.js) should filled with the secret key for the JWT. + ## Configuration When you start a WBO server, it loads its configuration from several environment variables. diff --git a/client-data/js/board.js b/client-data/js/board.js index bde3601..7ea94f3 100644 --- a/client-data/js/board.js +++ b/client-data/js/board.js @@ -62,8 +62,12 @@ Tools.connect = function () { self.socket = null; } + var url = new URL(window.location); + var params = new URLSearchParams(url.search); + var token = params.get("token"); this.socket = io.connect('', { + "query": "token=" + token, "path": window.location.pathname.split("/boards/")[0] + "/socket.io", "reconnection": true, "reconnectionDelay": 100, //Make the xhr connections as fast as possible diff --git a/nightwatch.conf.js b/nightwatch.conf.js index 647e40e..3b29d0a 100644 --- a/nightwatch.conf.js +++ b/nightwatch.conf.js @@ -24,7 +24,11 @@ module.exports = { "args": ["-headless"] } } - + } + }, + "jwt": { + "globals": { + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.5mhBHqs5_DTLdINd9p5m7ZJ6XD0Xc55kIaCRY5r6HRA" } } } diff --git a/package.json b/package.json index ae282d4..e9610c7 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ }, "scripts": { "start": "node ./server/server.js", - "test": "nightwatch tests" + "test": "nightwatch tests && nightwatch tests --env jwt" }, "main": "./server/server.js", "repository": { diff --git a/server/server.js b/server/server.js index e627565..27d1e61 100644 --- a/server/server.js +++ b/server/server.js @@ -104,13 +104,9 @@ function validateBoardName(boardName) { */ function userHasPermission(url) { if(config.AUTH_SECRET_KEY != "") { - if(url.searchParams.get("token")) { - var token = url.searchParams.get("token"); - try { - jsonwebtoken.verify(token, config.AUTH_SECRET_KEY); - } catch(error) { // Token not valid - throw new Error(error) - } + var token = url.searchParams.get("token"); + if(token) { + jsonwebtoken.verify(token, config.AUTH_SECRET_KEY); } else { // Error out as no token provided throw new Error("No token provided"); } diff --git a/server/sockets.js b/server/sockets.js index aa619c7..fb154aa 100644 --- a/server/sockets.js +++ b/server/sockets.js @@ -1,7 +1,8 @@ var iolib = require("socket.io"), { log, gauge, monitorFunction } = require("./log.js"), BoardData = require("./boardData.js").BoardData, - config = require("./configuration"); + config = require("./configuration"), + jsonwebtoken = require("jsonwebtoken"); /** Map from name to *promises* of BoardData @type {{[boardName: string]: Promise}} @@ -29,6 +30,20 @@ function noFail(fn) { function startIO(app) { io = iolib(app); + if (config.AUTH_SECRET_KEY) { + // Middleware to check for valid jwt + io.use(function(socket, next) { + if(socket.handshake.query && socket.handshake.query.token) { + jsonwebtoken.verify(socket.handshake.query.token, config.AUTH_SECRET_KEY, function(err, decoded) { + if(err) return next(new Error("Authentication error: Invalid JWT")); + socket.decoded = decoded; + next(); + }) + } else { + next(new Error("Authentication error: No jwt provided")); + } + }); + } io.on("connection", noFail(handleSocketConnection)); return io; } diff --git a/tests/integration.js b/tests/integration.js index 9d1eb32..8c3619a 100644 --- a/tests/integration.js +++ b/tests/integration.js @@ -5,12 +5,16 @@ const path = require("path"); const PORT = 8487 const SERVER = 'http://localhost:' + PORT; -let wbo, data_path; +let wbo, data_path, tokenQuery; async function beforeEach(browser, done) { data_path = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'wbo-test-data-')); process.env["PORT"] = PORT; process.env["WBO_HISTORY_DIR"] = data_path; + if(browser.globals.token) { + process.env["AUTH_SECRET_KEY"] = "test"; + tokenQuery = "token=" + browser.globals.token; + } console.log("Launching WBO in " + data_path); wbo = require("../server/server.js"); done(); @@ -51,7 +55,7 @@ function testPencil(browser) { .refresh() .waitForElementVisible("path[d='M 100 200 L 100 200 C 100 200 300 400 300 400'][stroke='#123456']") .assert.visible("path[d='M 0 0 L 0 0 C 0 0 40 120 90 120 C 140 120 180 0 180 0'][stroke='#abcdef']") - .url(SERVER + '/preview/anonymous') + .url(SERVER + '/preview/anonymous?' + tokenQuery) .waitForElementVisible("path[d='M 100 200 L 100 200 C 100 200 300 400 300 400'][stroke='#123456']") .assert.visible("path[d='M 0 0 L 0 0 C 0 0 40 120 90 120 C 140 120 180 0 180 0'][stroke='#abcdef']") .back() @@ -92,15 +96,15 @@ function testCursor(browser) { } function testBoard(browser) { - var page = browser.url(SERVER + '/boards/anonymous?lang=fr') + var page = browser.url(SERVER + '/boards/anonymous?lang=fr&' + tokenQuery) .waitForElementVisible('.tool[title ~= Crayon]') // pencil page = testPencil(page); page = testCircle(page); page = testCursor(page); // test hideMenu - browser.url(SERVER + '/boards/anonymous?lang=fr&hideMenu=true').waitForElementNotVisible('#menu'); - browser.url(SERVER + '/boards/anonymous?lang=fr&hideMenu=false').waitForElementVisible('#menu'); + browser.url(SERVER + '/boards/anonymous?lang=fr&hideMenu=true&' + tokenQuery).waitForElementNotVisible('#menu'); + browser.url(SERVER + '/boards/anonymous?lang=fr&hideMenu=false&' + tokenQuery).waitForElementVisible('#menu'); page.end(); }