MAJOR: Updates for Authenticated Web UI and CLI #30

Open
coolaj86 wants to merge 77 commits from next into master
6 changed files with 163 additions and 51 deletions
Showing only changes of commit 58dab177da - Show all commits

View File

@ -13,6 +13,7 @@ var YAML = require('js-yaml');
var TOML = require('toml'); var TOML = require('toml');
var TPLS = TOML.parse(fs.readFileSync(path.join(__dirname, "../lib/en-us.toml"), 'utf8')); var TPLS = TOML.parse(fs.readFileSync(path.join(__dirname, "../lib/en-us.toml"), 'utf8'));
var JWT = require('../lib/jwt.js'); var JWT = require('../lib/jwt.js');
var keypairs = require('keypairs');
/* /*
if ('function' !== typeof TOML.stringify) { if ('function' !== typeof TOML.stringify) {
@ -766,17 +767,18 @@ var keyname = 'telebit-remote';
state.keystore = keystore; state.keystore = keystore;
state.keystoreSecure = !keystore.insecure; state.keystoreSecure = !keystore.insecure;
keystore.get(keyname).then(function (key) { keystore.get(keyname).then(function (key) {
if (key && key.kty) { if (key && key.kty && key.kid) {
state.key = key; state.key = key;
state.pub = keypairs.neuter({ jwk: key });
fs.readFile(confpath, 'utf8', parseConfig); fs.readFile(confpath, 'utf8', parseConfig);
return; return;
} }
var keypairs = require('keypairs');
return keypairs.generate().then(function (pair) { return keypairs.generate().then(function (pair) {
var jwk = pair.private; var jwk = pair.private;
return keystore.set(keyname, jwk).then(function () { return keypairs.thumbprint({ jwk: pair.public }).then(function (kid) {
return keypairs.thumbprint({ jwk: pair.public }).then(function (kid) { jwk.kid = kid;
return keystore.set(keyname, jwk).then(function () {
var size = (jwk.crv || Buffer.from(jwk.n, 'base64').byteLength * 8); var size = (jwk.crv || Buffer.from(jwk.n, 'base64').byteLength * 8);
console.info("Generated new %s %s private key with thumbprint %s", jwk.kty, size, kid); console.info("Generated new %s %s private key with thumbprint %s", jwk.kty, size, kid);
state.key = jwk; state.key = jwk;

View File

@ -11,7 +11,7 @@ try {
var pkg = require('../package.json'); var pkg = require('../package.json');
var url = require('url'); //var url = require('url');
var path = require('path'); var path = require('path');
var os = require('os'); var os = require('os');
var fs = require('fs'); var fs = require('fs');
@ -374,46 +374,123 @@ controllers.relay = function (req, res) {
}); });
}; };
function jsonEggspress(req, res, next) {
/*
var opts = url.parse(req.url, true);
if (false && opts.query._body) {
try {
req.body = JSON.parse(decodeURIComponent(opts.query._body, true));
} catch(e) {
res.statusCode = 500;
res.end('{"error":{"message":"?_body={{bad_format}}"}}');
return;
}
}
*/
var hasLength = req.headers['content-length'] > 0;
if (!hasLength && !req.headers['content-type']) {
next();
return;
}
var body = '';
req.on('readable', function () {
var data;
while (true) {
data = req.read();
if (!data) { break; }
body += data.toString();
}
});
req.on('end', function () {
try {
req.body = JSON.parse(body);
} catch(e) {
res.statusCode = 400;
res.end('{"error":{"message":"POST body is not valid json"}}');
return;
}
next();
});
}
function decodeJwt(jwt) {
var parts = jwt.split('.');
var jws = {
protected: parts[0]
, payload: parts[0]
, signature: parts[2] //Buffer.from(parts[2], 'base64')
};
jws.header = JSON.parse(Buffer.from(jws.protected, 'base64'));
jws.claims = JSON.parse(Buffer.from(jws.payload, 'base64'));
return jws;
}
function jwtEggspress(req, res, next) {
var jwt = (req.headers.authorization||'').replace(/Bearer /i, '');
if (!jwt) { next(); return; }
try {
req.jwt = decodeJwt(jwt);
} catch(e) {
// ignore
}
// TODO verify if possible
next();
}
function verifyJws(jwk, jws) {
return require('keypairs').export({ jwk: jwk }).then(function (pem) {
var alg = 'RSA-SHA' + jws.header.alg.replace(/[^\d]+/i, '');
// XXX
// TODO check for public key in keytar
// XXX
return require('crypto')
.createVerify(alg)
.update(jws.protected + '.' + jws.payload)
.verify(pem, jws.signature, 'base64');
});
}
function jwsEggspress(req, res, next) {
// TODO check header application/jose+json ??
if (!req.body || !(req.body.protected && req.body.payload && req.body.signature)) {
next();
return;
}
req.jws = req.body;
req.jws.header = JSON.parse(Buffer.from(req.jws.protected, 'base64'));
req.body = Buffer.from(req.jws.payload, 'base64');
if ('{'.charCodeAt(0) === req.body[0] || '['.charCodeAt(0) === req.body[0]) {
req.body = JSON.parse(req.body);
}
if (req.jws.header.jwk) {
verifyJws(req.jws.header.jwk, req.jws).then(function (verified) {
req.jws.selfVerified = verified;
next();
});
return;
}
// TODO verify if possible
next();
}
function handleApi() { function handleApi() {
var app = eggspress(); var app = eggspress();
app.use('/', jwtEggspress);
app.use('/', jsonEggspress);
app.use('/', jwsEggspress);
app.use('/', function (req, res, next) { app.use('/', function (req, res, next) {
var opts = url.parse(req.url, true); if (req.jwt) {
if (false && opts.query._body) { console.log('jwt', req.jwt);
try { } else if (req.jws) {
req.body = JSON.parse(decodeURIComponent(opts.query._body, true)); console.log('jws', req.jws);
} catch(e) { console.log('body', req.body);
res.statusCode = 500;
res.end('{"error":{"message":"?_body={{bad_format}}"}}');
return;
}
} }
next();
var hasLength = req.headers['content-length'] > 0;
if (!hasLength && !req.headers['content-type']) {
next();
return;
}
var body = '';
req.on('readable', function () {
var data;
while (true) {
data = req.read();
if (!data) { break; }
body += data.toString();
}
});
req.on('end', function () {
try {
req.body = JSON.parse(body);
} catch(e) {
res.statusCode = 400;
res.end('{"error":{"message":"POST body is not valid json"}}');
return;
}
next();
});
}); });
function listSuccess(req, res) { function listSuccess(req, res) {

View File

@ -5,7 +5,11 @@ module.exports = function eggspress() {
var allPatterns = []; var allPatterns = [];
var app = function (req, res) { var app = function (req, res) {
var patterns = allPatterns.slice(0).reverse(); var patterns = allPatterns.slice(0).reverse();
function next() { function next(err) {
if (err) {
req.end(err.message);
return;
}
var todo = patterns.pop(); var todo = patterns.pop();
if (!todo) { if (!todo) {
console.log('[eggspress] Did not match any patterns', req.url); console.log('[eggspress] Did not match any patterns', req.url);

View File

@ -3,9 +3,11 @@
var os = require('os'); var os = require('os');
var path = require('path'); var path = require('path');
var http = require('http'); var http = require('http');
var keypairs = require('keypairs');
var common = require('../cli-common.js'); var common = require('../cli-common.js');
/*
function packConfig(config) { function packConfig(config) {
return Object.keys(config).map(function (key) { return Object.keys(config).map(function (key) {
var val = config[key]; var val = config[key];
@ -22,6 +24,7 @@ function packConfig(config) {
return key + ':' + val; // converts arrays to strings with , return key + ':' + val; // converts arrays to strings with ,
}); });
} }
*/
module.exports.create = function (state) { module.exports.create = function (state) {
common._init( common._init(
@ -72,16 +75,20 @@ module.exports.create = function (state) {
RC.request = function request(opts, fn) { RC.request = function request(opts, fn) {
if (!opts) { opts = {}; } if (!opts) { opts = {}; }
var service = opts.service || 'config'; var service = opts.service || 'config';
/*
var args = opts.data; var args = opts.data;
if (args && 'control' === service) { if (args && 'control' === service) {
args = packConfig(args); args = packConfig(args);
} }
var json = JSON.stringify(args); var json = JSON.stringify(opts.data);
*/
var url = '/rpc/' + service; var url = '/rpc/' + service;
/*
if (json) { if (json) {
url += ('?_body=' + encodeURIComponent(json)); url += ('?_body=' + encodeURIComponent(json));
} }
var method = opts.method || (args && 'POST') || 'GET'; */
var method = opts.method || (opts.data && 'POST') || 'GET';
var reqOpts = { var reqOpts = {
method: method method: method
, path: url , path: url
@ -124,11 +131,33 @@ module.exports.create = function (state) {
fn(err); fn(err);
}); });
if ('POST' === method && opts.data) {
req.setHeader("content-type", 'application/json'); // Simple GET
req.write(json || opts.data); if ('POST' !== method || !opts.data) {
return keypairs.signJwt({
jwk: state.key
, claims: { iss: false, exp: Math.round(Date.now()/1000) + (15 * 60) }
//TODO , exp: '15m'
}).then(function (jwt) {
req.setHeader("authorization", 'bearer ' + jwt);
req.end();
});
} }
req.end();
return keypairs.signJws({
jwk: state.key
, protected: {
// alg will be filled out automatically
jwk: state.pub
, nonce: require('crypto').randomBytes(16).toString('hex') // TODO get from server
, url: 'https://' + reqOpts.host + reqOpts.path
}
, payload: JSON.stringify(opts.data)
}).then(function (jws) {
req.setHeader("content-type", 'application/json');
req.write(JSON.stringify(jws));
req.end();
});
}; };
return RC; return RC;
}; };

6
package-lock.json generated
View File

@ -430,9 +430,9 @@
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
}, },
"keypairs": { "keypairs": {
"version": "1.2.5", "version": "1.2.6",
"resolved": "https://registry.npmjs.org/keypairs/-/keypairs-1.2.5.tgz", "resolved": "https://registry.npmjs.org/keypairs/-/keypairs-1.2.6.tgz",
"integrity": "sha512-VKUxQ4iQB5LvVMtObOzNmZRfgXLTr5GMr+wg9A2BnILArBLrtg/DIuWRJQpDNRRfAGRQjHXxSVOW+7xpzIAY1Q==", "integrity": "sha512-sJDaZvJqHWUawJjrOGKJvKGLfPh0eo2WV7td4RSL88w3BjPYCYI9PkqBn0hLqc6uw0HFSqZMikhGn/jgPpcWnQ==",
"requires": { "requires": {
"eckles": "^1.4.1", "eckles": "^1.4.1",
"rasha": "^1.2.4" "rasha": "^1.2.4"

View File

@ -57,7 +57,7 @@
"finalhandler": "^1.1.1", "finalhandler": "^1.1.1",
"greenlock": "^2.6.7", "greenlock": "^2.6.7",
"js-yaml": "^3.11.0", "js-yaml": "^3.11.0",
"keypairs": "^1.2.5", "keypairs": "^1.2.6",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"proxy-packer": "^2.0.2", "proxy-packer": "^2.0.2",
"ps-list": "^5.0.0", "ps-list": "^5.0.0",