WIP api token for accounts

This commit is contained in:
AJ ONeal 2018-06-19 23:40:58 +00:00
parent 09b1d5939e
commit 114cc53dd4
1 changed files with 215 additions and 111 deletions

View File

@ -1,4 +1,17 @@
'use strict'; 'use strict';
var fs = require('fs');
var path = require('path');
var util = require('util');
var crypto = require('crypto');
var escapeHtml = require('escape-html');
var jwt = require('jsonwebtoken');
var requestAsync = util.promisify(require('request'));
var _auths = module.exports._auths = {};
function sendMail(state, auth) {
console.log('[DEBUG] ext auth', auth);
/* /*
curl -s --user 'api:YOUR_API_KEY' \ curl -s --user 'api:YOUR_API_KEY' \
https://api.mailgun.net/v3/YOUR_DOMAIN_NAME/messages \ https://api.mailgun.net/v3/YOUR_DOMAIN_NAME/messages \
@ -8,43 +21,25 @@ curl -s --user 'api:YOUR_API_KEY' \
-F subject='Hello' \ -F subject='Hello' \
-F text='Testing some Mailgun awesomeness!' -F text='Testing some Mailgun awesomeness!'
*/ */
var fs = require('fs');
var escapeHtml = require('escape-html');
var _auths = module.exports._auths = {};
module.exports.authenticate = function (opts) {
console.log("It's auth'n time!");
var util = require('util');
var requestAsync = util.promisify(require('request'));
var state = opts.state;
var jwtoken = opts.auth;
var auth;
var crypto = require('crypto');
console.log('[DEBUG] ext auth', jwtoken);
auth = jwtoken;
if ('object' === typeof auth && /^.+@.+\..+$/.test(auth.subject)) {
console.log("[DEBUG] gonna send email");
auth.id = crypto.randomBytes(12).toString('hex');
//var id = crypto.randomBytes(16).toString('base64').replace(/\+/g,'-').replace(/\//g,'_').replace(/=/g,'');
var subj = 'Confirm New Device Connection'; var subj = 'Confirm New Device Connection';
var text = "You tried connecting with '{{hostname}}' for the first time. Confirm to continue connecting:\n" var text = "You tried connecting with '{{hostname}}' for the first time. Confirm to continue connecting:\n"
+ '\n' + '\n'
+ ' https://' + state.config.webminDomain + '/login/#/magic={{id}}\n' + ' https://' + state.config.webminDomain + '/login/#/magic={{secret}}\n'
+ '\n' + '\n'
+ "({{os_arch}} {{os_platform}} {{os_release}})\n" + "({{os_arch}} {{os_platform}} {{os_release}})\n"
+ '\n' + '\n'
; ;
var html = "You tried connecting with '{{hostname}}' for the first time. Confirm to continue connecting:<br>" var html = "You tried connecting with '{{hostname}}' for the first time. Confirm to continue connecting:<br>"
+ '<br>' + '<br>'
+ '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <a href="https://' + state.config.webminDomain + '/login/#/magic={{id}}">Confirm Device</a><br>' + '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <a href="https://' + state.config.webminDomain + '/login/#/magic={{secret}}">Confirm Device</a><br>'
+ '<br>' + '<br>'
+ '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <small>or copy and paste this link:</small><br>' + '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <small>or copy and paste this link:</small><br>'
+ '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <small>https://' + state.config.webminDomain + '/login/#/magic={{id}}</small><br>' + '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <small>https://' + state.config.webminDomain + '/login/#/magic={{secret}}</small><br>'
+ '<br>' + '<br>'
+ "({{os_arch}} {{os_platform}} {{os_release}})<br>" + "({{os_arch}} {{os_platform}} {{os_release}})<br>"
+ '<br>' + '<br>'
; ;
[ 'id', 'hostname', 'os_arch', 'os_platform', 'os_release' ].forEach(function (key) { [ 'id', 'secret', 'hostname', 'os_arch', 'os_platform', 'os_release' ].forEach(function (key) {
var val = escapeHtml(auth[key]); var val = escapeHtml(auth[key]);
subj = subj.replace(new RegExp('{{' + key + '}}', 'g'), val); subj = subj.replace(new RegExp('{{' + key + '}}', 'g'), val);
text = text.replace(new RegExp('{{' + key + '}}', 'g'), val); text = text.replace(new RegExp('{{' + key + '}}', 'g'), val);
@ -62,15 +57,94 @@ module.exports.authenticate = function (opts) {
, html: html , html: html
} }
}).then(function (resp) { }).then(function (resp) {
console.log("[DEBUG] email was sent, or so they say");
console.log(resp.body);
fs.writeFile(path.join(__dirname, 'emails', auth.subject), JSON.stringify(auth), function (err) { fs.writeFile(path.join(__dirname, 'emails', auth.subject), JSON.stringify(auth), function (err) {
if (err) { if (err) {
console.error('[ERROR] in writing auth details'); console.error('[ERROR] in writing auth details');
console.error(err); console.error(err);
} }
}); });
return new state.Promise(function (resolve, reject) { console.log("[DEBUG] email was sent, or so they say");
console.log(resp.body);
});
}
module.exports.pairRequest = function (opts) {
console.log("It's auth'n time!");
var state = opts.state;
var auth = opts.auth;
var jwt = require('jsonwebtoken');
console.log("[DEBUG] gonna send email");
auth.id = crypto.randomBytes(12).toString('hex');
auth.secret = crypto.randomBytes(12).toString('hex');
//var id = crypto.randomBytes(16).toString('base64').replace(/\+/g,'-').replace(/\//g,'_').replace(/=/g,'');
return sendMail(state, auth).then(function () {
var now = Date.now();
var authnToken = {
domains: []
, ports: []
, aud: state.config.webminDomain
, iss: Math.round(now / 1000)
, id: auth.id
, pin: auth.otp
, hostname: auth.hostname
};
_auths[auth.id] = _auths[auth.secret] = {
dt: now
, authn: jwt.sign(authnToken, state.secret)
, pin: auth.otp
, id: auth.id
, secret: auth.secret
};
authnToken.jwt = _auths[auth.id].authn;
// return empty token which will receive grants upon authorization
return authnToken;
});
};
module.exports.pairPin = function (opts) {
var state = opts.state;
return state.Promise.resolve().then(function () {
var pin = opts.pin;
var secret = opts.secret;
var auth = _auths[secret];
if (!auth || auth.secret !== opts.secret) {
throw new Error("I can't even right now - bad magic link id");
}
// XXX security, we want to check the pin if it's supported serverside,
// regardless of what the client sends. This bad logic is just for testing.
if (pin && auth.pin && pin !== auth.pin) {
throw new Error("I can't even right now - bad device pair pin");
}
delete _auths[auth.id];
var hri = require('human-readable-ids').hri;
var hrname = hri.random() + '.' + state.config.sharedDomain;
var authzToken = {
domains: [ hrname ]
, ports: [ (1024 + 1) + Math.round(Math.random() * 6300) ]
, aud: state.config.webminDomain
, iss: Math.round(Date.now() / 1000)
, id: auth.id
, hostname: auth.hostname
};
authzToken.jwt = jwt.sign(authzToken, state.secret);
fs.writeFile(path.join(__dirname, 'emails', auth.subject + '.data'), JSON.stringify(authzToken), function (err) {
if (err) {
console.error('[ERROR] in writing token details');
console.error(err);
}
});
return authzToken;
});
};
module.exports.pairState = function (opts) {
var state = opts.state;
var auth = opts.auth;
var resolve = opts.resolve;
var reject = opts.reject;
// TODO use global interval whenever the number of active links is high // TODO use global interval whenever the number of active links is high
var t = setTimeout(function () { var t = setTimeout(function () {
console.log("[Magic Link] Timeout for '" + auth.subject + "'"); console.log("[Magic Link] Timeout for '" + auth.subject + "'");
@ -80,38 +154,35 @@ module.exports.authenticate = function (opts) {
reject(); reject();
}, 2 * 60 * 60 * 1000); }, 2 * 60 * 60 * 1000);
function authorize() { function authorize(pin) {
console.log("mighty auth'n ranger!"); console.log("mighty auth'n ranger!");
clearTimeout(t); clearTimeout(t);
delete _auths[auth.id]; return module.exports.pairPin({ secret: auth.secret, pin: pin }).then(function (tokenData) {
var hri = require('human-readable-ids').hri; // TODO call state object with socket info rather than resolve
var hrname = hri.random() + '.' + state.config.sharedDomain;
var jwt = require('jsonwebtoken');
var tokenData = {
domains: [ hrname ]
, ports: [ 1024 + Math.round(Math.random() * 6300) ]
, aud: state.config.webminDomain
, iss: Math.round(Date.now() / 1000)
, id: auth.id
, hostname: auth.hostname
};
tokenData.jwt = jwt.sign(tokenData, state.secret);
resolve(tokenData); resolve(tokenData);
fs.writeFile(path.join(__dirname, 'emails', auth.subject + '.data'), JSON.stringify(tokenData), function (err) {
if (err) {
console.error('[ERROR] in writing token details');
console.error(err);
}
});
return tokenData; return tokenData;
}, function (err) {
reject(err);
return state.Promise.reject(err);
});
} }
_auths[auth.id] = { _auths[auth.id].resolve = authorize;
dt: Date.now() _auths[auth.id].reject = reject;
, resolve: authorize
, reject: reject
}; };
module.exports.authenticate = function (opts) {
var jwt = require('jsonwebtoken');
var jwtoken = opts.auth;
var auth = opts.auth;
var state = opts.state;
if ('object' === typeof auth && /^.+@.+\..+$/.test(auth.subject)) {
return module.exports.pairRequest(opts).then(function () {
return new state.Promise(function (resolve, reject) {
opts.resolve = resolve;
opts.reject = reject;
module.exports.pairState(opts);
}); });
}); });
} }
@ -126,23 +197,55 @@ module.exports.authenticate = function (opts) {
return state.defaults.authenticate(opts.auth); return state.defaults.authenticate(opts.auth);
}; };
//var loaded = false; //var loaded = false;
var path = require('path');
var express = require('express'); var express = require('express');
var app = express(); var app = express();
var staticApp = express(); var staticApp = express();
var nowww = require('nowww')(); var nowww = require('nowww')();
var CORS = require('connect-cors'); var CORS = require('connect-cors');
var bodyParser = require('body-parser');
staticApp.use('/', express.static(path.join(__dirname, 'admin'))); staticApp.use('/', express.static(path.join(__dirname, 'admin')));
app.use('/api', CORS({})); app.use('/api', CORS({}));
app.get('/api/telebit.cloud/magic/:magic', function (req, res) { app.use('/api', bodyParser.json());
// From Device
app.post('/api/telebit.cloud/pair_request', function (req, res) {
var auth = req.body;
module.exports.authenticate({ state: req._state, auth: auth }).then(function (tokenData) {
// res.send({ success: true, message: "pair request sent" });
res.send(tokenData);
}, function (err) {
res.send({ error: err });
});
});
// From Browser
app.post('/api/telebit.cloud/pair_code', function (req, res) {
var auth = req.body;
return module.exports.pairPin({ secret: auth.magic, pin: auth.pin }).then(function (tokenData) {
res.send(tokenData);
}, function (err) {
res.send({ error: err });
});
});
// From Device (polling)
app.get('/api/telebit.cloud/pair_state', function (req, res) {
// check if pair is complete
// respond immediately if so
// wait for a little bit otherwise
// respond if/when it completes
// or respond after time if it does not complete
res.send({ error: { message: "not implemented" } });
});
// From Browser
app.get('/api/telebit.cloud/magic/:magic/:pin?', function (req, res) {
console.log("DEBUG telebit.cloud magic"); console.log("DEBUG telebit.cloud magic");
var tokenData; var tokenData;
var magic = req.params.magic || req.query.magic; var magic = req.params.magic || req.query.magic;
var pin = req.params.pin || req.query.pin;
console.log("DEBUG telebit.cloud magic 1a", magic); console.log("DEBUG telebit.cloud magic 1a", magic);
if (_auths[magic]) { if (_auths[magic] && magic === _auths[magic].secret) {
console.log("DEBUG telebit.cloud magic 1b"); console.log("DEBUG telebit.cloud magic 1b");
tokenData = _auths[magic].resolve(); tokenData = _auths[magic].resolve(pin);
console.log("DEBUG telebit.cloud magic 1c"); console.log("DEBUG telebit.cloud magic 1c");
res.send(tokenData); res.send(tokenData);
} else { } else {
@ -162,6 +265,7 @@ module.exports.webadmin = function (state, req, res) {
} }
if ('api.' + state.config.webminDomain === host) { if ('api.' + state.config.webminDomain === host) {
console.log("DEBUG going to api"); console.log("DEBUG going to api");
req._state = state;
app(req, res); app(req, res);
return; return;
} }