shims for jwt and jws authentication
This commit is contained in:
parent
1fda5b15d0
commit
58dab177da
|
@ -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;
|
||||
|
|
149
bin/telebitd.js
149
bin/telebitd.js
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue