shims for jwt and jws authentication

This commit is contained in:
AJ ONeal 2019-03-07 01:38:21 -07:00
parent 1fda5b15d0
commit 58dab177da
6 changed files with 163 additions and 51 deletions

View File

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

View File

@ -11,7 +11,7 @@ try {
var pkg = require('../package.json');
var url = require('url');
//var url = require('url');
var path = require('path');
var os = require('os');
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() {
var app = eggspress();
app.use('/', jwtEggspress);
app.use('/', jsonEggspress);
app.use('/', jwsEggspress);
app.use('/', function (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;
}
if (req.jwt) {
console.log('jwt', req.jwt);
} else if (req.jws) {
console.log('jws', req.jws);
console.log('body', req.body);
}
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();
});
next();
});
function listSuccess(req, res) {

View File

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

View File

@ -3,9 +3,11 @@
var os = require('os');
var path = require('path');
var http = require('http');
var keypairs = require('keypairs');
var common = require('../cli-common.js');
/*
function packConfig(config) {
return Object.keys(config).map(function (key) {
var val = config[key];
@ -22,6 +24,7 @@ function packConfig(config) {
return key + ':' + val; // converts arrays to strings with ,
});
}
*/
module.exports.create = function (state) {
common._init(
@ -72,16 +75,20 @@ module.exports.create = function (state) {
RC.request = function request(opts, fn) {
if (!opts) { opts = {}; }
var service = opts.service || 'config';
/*
var args = opts.data;
if (args && 'control' === service) {
args = packConfig(args);
}
var json = JSON.stringify(args);
var json = JSON.stringify(opts.data);
*/
var url = '/rpc/' + service;
/*
if (json) {
url += ('?_body=' + encodeURIComponent(json));
}
var method = opts.method || (args && 'POST') || 'GET';
*/
var method = opts.method || (opts.data && 'POST') || 'GET';
var reqOpts = {
method: method
, path: url
@ -124,11 +131,33 @@ module.exports.create = function (state) {
fn(err);
});
if ('POST' === method && opts.data) {
req.setHeader("content-type", 'application/json');
req.write(json || opts.data);
// Simple GET
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;
};

6
package-lock.json generated
View File

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

View File

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