Merge pull request #254 from H2-invent/master

add JWT board name verification
This commit is contained in:
Ophir LOJKINE 2022-09-05 22:00:47 +02:00 committed by GitHub
commit 27c79074ed
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 157 additions and 23 deletions

View file

@ -94,9 +94,34 @@ Within the payload, you can declare the user's roles as an array. Currently the
"roles": ["moderator"]
}
```
Moderators have access to the Clear tool, which will wipe all content from the board.
## Board name verification in the JWT
WBO supports verification of the board with a JWT.
The `AUTH_SECRET_KEY` variable in [`configuration.js`](./server/configuration.js) should be filled with the secret key for the JWT.
To check for a valid board name just add the board name to the role with a ":". With this you can set a moderator for a specific board.
```
{
....
"roles": ["moderator:<boardName1>","moderator:<boardName2>","editor:<boardName3>","editor:<boardName4>"] }
}
```
eg, `http://myboard.com/boards/mySecretBoardName?token={token}`
```
{
"iat": 1516239022,
"exp": 1516298489,
"roles": ["moderator:mySecretBoardName"]
}
```
You can now be sure that only users who have the correct token have access to the board with the specific name.
## Configuration
When you start a WBO server, it loads its configuration from several environment variables.

View file

@ -57,4 +57,5 @@ module.exports = {
/** Secret key for jwt */
AUTH_SECRET_KEY: (process.env["AUTH_SECRET_KEY"] || ""),
};

View file

@ -0,0 +1,99 @@
/**
* WHITEBOPHIR
*********************************************************
* @licstart The following is the entire license notice for the
* JavaScript code in this page.
*
* Copyright (C) 2013 Ophir LOJKINE
*
*
* The JavaScript code in this page is free software: you can
* redistribute it and/or modify it under the terms of the GNU
* General Public License (GNU GPL) as published by the Free Software
* Foundation, either version 3 of the License, or (at your option)
* any later version. The code is distributed WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
*
* As additional permission under GNU GPL version 3 section 7, you
* may distribute non-source (e.g., minimized or compacted) forms of
* that code without the copy of the GNU GPL normally required by
* section 4, provided you include this license notice and a URL
* through which recipients can access the Corresponding Source.
*
* @licend
*/
config = require("./configuration.js"),
jsonwebtoken = require("jsonwebtoken");
/**
* This function checks if a board name is set in the roles claim.
* Returns true of the board name is set in the JWT and the board name matches the board name in the URL
* @param {string} url
* @param {string} boardNameIn
@returns {boolean} - True if user does not have the role forbidden false if the user hase the role forbidden
@throws {Error} - If no boardname match
*/
function checkBoardnameInToken(url, boardNameIn) {
var token = url.searchParams.get("token");
if (roleInBoard(token, boardNameIn) === 'forbidden') {
throw new Error("Acess Forbidden");
}
}
function parse_role(role) {
let [_, role_name, board_name] = role.match(/^([^:]*):?(.*)$/);
return {role_name, board_name}
}
/**
* This function checks if a oard name is set in the roles claim.
* Returns string depending on the role in the board
* @param {string} token
* @param {string} board
@returns {string} "moderator"|"editor"|"forbidden"
*/
function roleInBoard(token, board = null) {
if (config.AUTH_SECRET_KEY != "") {
if (!token) {
throw new Error("No token provided");
}
var payload = jsonwebtoken.verify(token, config.AUTH_SECRET_KEY);
var roles = payload.roles;
var oneHasBoardName = false;
var oneHasModerator = false;
if (roles) {
for (var line of roles) {
var role = parse_role(line);
if (role.board_name !== '') {
oneHasBoardName = true;
}
if (role.role_name === "moderator") {
oneHasModerator = true;
}
if (role.board_name === board) {
return role.role_name;
}
}
if ((!board && oneHasModerator) || !oneHasBoardName) {
if (oneHasModerator) {
return "moderator";
} else {
return "editor";
}
}
return "forbidden";
} else {
return "editor";
}
} else {
return "editor";
}
}
module.exports = {checkBoardnameInToken, roleInBoard};

View file

@ -24,9 +24,10 @@
* @licend
*/
config = require("./configuration.js"),
jsonwebtoken = require("jsonwebtoken");
const {roleInBoard} = require("./jwtBoardnameAuth");
/**
* Validates jwt and returns whether user is a moderator
* @param {URL} url
@ -38,7 +39,7 @@ function checkUserPermission(url) {
if (config.AUTH_SECRET_KEY != "") {
var token = url.searchParams.get("token");
if (token) {
isModerator = checkIfModerator(token);
isModerator = roleInBoard(token) === "moderator";
} else {
// Error out as no token provided
throw new Error("No token provided");
@ -47,22 +48,5 @@ function checkUserPermission(url) {
return isModerator;
}
/**
* Check if user is a moderator
* @param {string} token
*/
function checkIfModerator(token) {
if(config.AUTH_SECRET_KEY != "") {
var payload = jsonwebtoken.verify(token, config.AUTH_SECRET_KEY);
var roles = payload.roles;
if(roles) {
return roles.includes("moderator");
} else {
return false;
}
} else {
return false;
}
}
module.exports = { checkUserPermission, checkIfModerator };
module.exports = { checkUserPermission };

View file

@ -11,6 +11,7 @@ var app = require("http").createServer(handler),
polyfillLibrary = require("polyfill-library"),
check_output_directory = require("./check_output_directory.js"),
jwtauth = require("./jwtauth.js");
jwtBoardName = require("./jwtBoardnameAuth.js");
var MIN_NODE_VERSION = 10.0;
@ -120,11 +121,13 @@ function handleRequest(request, response) {
if (parts.length === 1) {
// '/boards?board=...' This allows html forms to point to boards
var boardName = parsedUrl.searchParams.get("board") || "anonymous";
jwtBoardName.checkBoardnameInToken(parsedUrl, boardName);
var headers = { Location: "boards/" + encodeURIComponent(boardName) };
response.writeHead(301, headers);
response.end();
} else if (parts.length === 2 && parsedUrl.pathname.indexOf(".") === -1) {
validateBoardName(parts[1]);
var boardName = validateBoardName(parts[1]);
jwtBoardName.checkBoardnameInToken(parsedUrl, boardName);
boardTemplate.serve(request, response, isModerator);
// If there is no dot and no directory, parts[1] is the board name
} else {
@ -139,6 +142,7 @@ function handleRequest(request, response) {
config.HISTORY_DIR,
"board-" + boardName + ".json"
);
jwtBoardName.checkBoardnameInToken(parsedUrl, boardName);
if (parts.length > 2 && /^[0-9A-Za-z.\-]+$/.test(parts[2])) {
history_file += "." + parts[2] + ".bak";
}
@ -161,6 +165,7 @@ function handleRequest(request, response) {
config.HISTORY_DIR,
"board-" + boardName + ".json"
);
jwtBoardName.checkBoardnameInToken(parsedUrl, boardName);
response.writeHead(200, {
"Content-Type": "image/svg+xml",
"Content-Security-Policy": CSP,

View file

@ -105,7 +105,27 @@ function testBoard(browser) {
// test hideMenu
browser.url(SERVER + '/boards/anonymous?lang=fr&hideMenu=true&' + tokenQuery).waitForElementNotVisible('#menu');
browser.url(SERVER + '/boards/anonymous?lang=fr&hideMenu=false&' + tokenQuery).waitForElementVisible('#menu');
if(browser.globals.token) {
//has moderator jwt and no board name
browser.url(SERVER + '/boards/testboard?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJyb2xlcyI6WyJtb2RlcmF0b3IiXX0.PqYHmV0loeKwyLLYZ1a1eIXBCCaa3t5lYUTu_P_-i14').waitForElementVisible('#toolID-Clear');
//has moderator JWT and other board name
browser.url(SERVER + '/boards/testboard123?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJyb2xlcyI6WyJtb2RlcmF0b3IiXX0.PqYHmV0loeKwyLLYZ1a1eIXBCCaa3t5lYUTu_P_-i14').waitForElementVisible('#toolID-Clear');
//has moderator JWT and board name match board name in url
browser.url(SERVER + '/boards/testboard?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJyb2xlcyI6WyJtb2RlcmF0b3I6dGVzdGJvYXJkIl19.UVf6awGEChVxcWBbt6dYoNH0Scq7cVD_xfQn-U8A1lw').waitForElementVisible('#toolID-Clear');
//has moderator JWT and board name NOT match board name in url
browser.url(SERVER + '/boards/testboard123?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJyb2xlcyI6WyJtb2RlcmF0b3I6dGVzdGJvYXJkIl19.UVf6awGEChVxcWBbt6dYoNH0Scq7cVD_xfQn-U8A1lw').waitForElementNotPresent('#menu');
//has editor JWT and no boardname provided
browser.url(SERVER + '/boards/testboard?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJyb2xlcyI6WyJlZGl0b3IiXX0.IJehwM8tPVQFzJ2fZMBHveii1DRChVtzo7PEnSmmFt8').waitForElementNotPresent('#toolID-Clear');
browser.url(SERVER + '/boards/testboard?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJyb2xlcyI6WyJlZGl0b3IiXX0.IJehwM8tPVQFzJ2fZMBHveii1DRChVtzo7PEnSmmFt8').waitForElementVisible('#menu')
//has editor JWT and boardname provided and match to the board in the url
browser.url(SERVER + '/boards/testboard?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJyb2xlcyI6WyJlZGl0bzp0ZXN0Ym9hcmQiXX0.-P6gjYlPP5I2zgSoVTlADdesVPfSXV-JXZQK5uh3Xwo').waitForElementVisible('#menu');
browser.url(SERVER + '/boards/testboard?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJyb2xlcyI6WyJlZGl0bzp0ZXN0Ym9hcmQiXX0.-P6gjYlPP5I2zgSoVTlADdesVPfSXV-JXZQK5uh3Xwo').waitForElementNotPresent('#toolID-Clear');
//has editor JWT and boardname provided and and not match to the board in the url
browser.url(SERVER + '/boards/testboard123?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJyb2xlcyI6WyJlZGl0bzp0ZXN0Ym9hcmQiXX0.-P6gjYlPP5I2zgSoVTlADdesVPfSXV-JXZQK5uh3Xwo').waitForElementNotPresent('#menu');
//is moderator and boardname contains ":"
browser.url(SERVER + '/boards/test:board?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJyb2xlcyI6WyJtb2RlcmF0b3I6dGVzdDpib2FyZCJdfQ.LKYcDccheD2oXAMAemxSekDeowGsMl29CFkgJgwbkGE').waitForElementNotPresent('#menu');
browser.url(SERVER + '/boards/testboard?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJyb2xlcyI6WyJtb2RlcmF0b3I6dGVzdDpib2FyZCJdfQ.LKYcDccheD2oXAMAemxSekDeowGsMl29CFkgJgwbkGE').waitForElementNotPresent('#menu');
}
page.end();
}