Merge branch '61-user-authentication' into devel

This commit is contained in:
D. Berge
2020-10-23 15:14:09 +02:00
24 changed files with 503 additions and 30 deletions

View File

@@ -62,8 +62,22 @@ const allMeta = (key, value) => {
return { all: [ meta(key, value) ] };
};
// These routes do not require authentication
app.map({
'*': { all: [ meta() ] }, // Create the req.meta object
'/login': {
post: [ mw.user.login ]
},
'/logout': {
get: [ mw.user.logout ],
post: [ mw.user.logout ]
}
});
app.use(mw.auth.authentify);
// We must be authenticated before we can access these
app.map({
'/project': {
get: [ mw.project.list ], // Get list of projects
},
@@ -93,7 +107,7 @@ app.map({
},
'/project/:project/line/:line': {
// get: [ mw.line.get ],
patch: [ mw.line.patch ],
patch: [ mw.auth.access.write, mw.line.patch ],
},
'/project/:project/sequence/': {
@@ -101,30 +115,30 @@ app.map({
},
'/project/:project/sequence/:sequence': {
// get: [ mw.sequence.get ],
patch: [ mw.sequence.patch ],
patch: [ mw.auth.access.write, mw.sequence.patch ],
},
'/project/:project/plan/': {
get: [ mw.plan.list ],
put: [ mw.plan.put ],
post: [ mw.plan.post ]
put: [ mw.auth.access.write, mw.plan.put ],
post: [ mw.auth.access.write, mw.plan.post ]
},
'/project/:project/plan/:sequence': {
// get: [ mw.plan.get ],
patch: [ mw.plan.patch ],
delete: [ mw.plan.delete ]
patch: [ mw.auth.access.write, mw.plan.patch ],
delete: [ mw.auth.access.write, mw.plan.delete ]
},
//
'/project/:project/event/': {
get: [ mw.event.cache.get, mw.event.list, mw.event.cache.save ],
post: [ mw.event.post ],
put: [ mw.event.put ],
delete: [ mw.event.delete ],
post: [ mw.auth.access.write, mw.event.post ],
put: [ mw.auth.access.write, mw.event.put ],
delete: [ mw.auth.access.write, mw.event.delete ],
':type/': {
':id/': {
// get: [ mw.event.get ],
put: [ mw.event.put ],
delete: [ mw.event.delete ]
put: [ mw.auth.access.write, mw.event.put ],
delete: [mw.auth.access.write, mw.event.delete ]
}
},
},
@@ -134,7 +148,7 @@ app.map({
},
'/project/:project/configuration/:path(*)?': {
get: [ mw.configuration.get ],
// post: [ mw.label.post ],
// post: [ mw.auth.access.admin, mw.label.post ],
},
'/project/:project/info/:path(*)': {
get: [ mw.info.get ],
@@ -165,7 +179,7 @@ app.map({
'gis/:featuretype(line|point)': {
get: [ mw.gis.navdata.get ]
}
}
},
//
// '/user': {
// get: [ mw.user.get ],
@@ -177,12 +191,6 @@ app.map({
// // delete: [ mw.user.delete ]
// },
//
// '/login': {
// post: [ mw.user.login ]
// },
// '/logout': {
// post: [ mw.user.logout ]
// }
});
// Generic error handler. Stops stack dumps

View File

@@ -0,0 +1,31 @@
async function read (req, res, next) {
if (req.user) {
next();
} else {
next({status: 403, message: "Access denied"});
}
}
async function write (req, res, next) {
if (req.user && (req.user.role == "user" || req.user.role == "admin")) {
next();
} else {
next({status: 403, message: "Access denied"});
}
}
async function admin (req, res, next) {
if (req.user && req.user.role == "admin") {
next();
} else {
next({status: 403, message: "Access denied"});
}
}
module.exports = {
read,
write,
admin
};

View File

@@ -0,0 +1,92 @@
const dns = require('dns');
const { Netmask } = require('netmask');
const cfg = require('../../../lib/config');
const jwt = require('../../../lib/jwt');
async function authorisedIP (req, res) {
const validIPs = cfg._("global.users.login.ip") || {};
for (const key in validIPs) {
const block = new Netmask(key);
if (block.contains(req.ip)) {
const payload = Object.assign({
ip: req.ip,
autologin: true
}, validIPs[key]);
jwt.issue(payload, req, res);
return true;
}
}
return false;
}
async function authorisedHost (req, res) {
const validHosts = cfg._("global.users.login.host") || {};
for (const key in validHosts) {
try {
const ip = await dns.promises.resolve(key);
if (ip == req.ip) {
const payload = Object.assign({
ip: req.ip,
host: key,
autologin: true
}, validHosts[key]);
jwt.issue(payload, req, res);
return true;
}
} catch (err) {
if (err.code != "ENODATA") {
console.error(err);
}
}
}
return false;
}
async function auth (req, res, next) {
if (res.headersSent) {
// Nothing to do, this request must have been
// handled already by another middleware.
return;
}
// Check for a valid JWT (already decoded by a previous
// middleware).
if (req.user) {
if (!req.user.autologin) {
// If this is not an automatic login, check if the token is in the
// second half of its lifetime. If so, reissue a new one, valid for
// another cfg.jwt.options.expiresIn seconds.
if (req.user.exp) {
const ttl = req.user.exp - Date.now()/1000;
if (ttl < cfg.jwt.options.expiresIn/2) {
const credentials = cfg._("global.users.login.user").find(i => i.name == req.user.name && i.role == req.user.role);
if (credentials) {
// Refresh token
payload = Object.assign({}, credentials);
delete payload.hash;
jwt.issue(Object.assign({}, credentials), req, res);
}
}
}
}
next();
return;
}
// Check if the IP is known to us
if (await authorisedIP(req, res)) {
next();
return;
}
// Check if the hostname is known to us
if (await authorisedHost(req, res)) {
next();
return;
}
next({status: 401, message: "Not authorised"});
}
module.exports = auth;

View File

@@ -1,3 +1,4 @@
exports.jwt = require('./jwt');
// exports.access = require('./access');
exports.authentify = require('./authentify');
exports.access = require('./access');

View File

@@ -0,0 +1,3 @@
exports.login = require('./login');
exports.logout = require('./logout');

View File

@@ -0,0 +1,28 @@
const crypto = require('crypto');
const cfg = require('../../../lib/config');
const jwt = require('../../../lib/jwt');
async function login (req, res, next) {
if (req.body) {
const {user, password} = req.body;
if (user && password) {
const hash = crypto
.pbkdf2Sync(password, 'Dougal'+user, 10712, 48, 'sha512')
.toString('base64');
for (const credentials of cfg._("global.users.login.user") || []) {
if (credentials.name == user && credentials.hash == hash) {
const payload = Object.assign({}, credentials);
delete payload.hash;
jwt.issue(payload, req, res);
res.status(204).send();
next();
return;
}
}
next({status: 401, message: "Unauthorised"});
}
}
next({status: 400, message: "Bad request"});
}
module.exports = login;

View File

@@ -0,0 +1,8 @@
async function logout (req, res, next) {
res.clearCookie("JWT");
res.status(204).send();
next();
}
module.exports = logout;