From 114cc53dd4db5ea75518305696d0a9e60635a58e Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 19 Jun 2018 23:40:58 +0000 Subject: [PATCH] WIP api token for accounts --- lib/extensions/index.js | 326 ++++++++++++++++++++++++++-------------- 1 file changed, 215 insertions(+), 111 deletions(-) diff --git a/lib/extensions/index.js b/lib/extensions/index.js index bc86359..3b3efe1 100644 --- a/lib/extensions/index.js +++ b/lib/extensions/index.js @@ -1,117 +1,188 @@ 'use strict'; -/* -curl -s --user 'api:YOUR_API_KEY' \ - https://api.mailgun.net/v3/YOUR_DOMAIN_NAME/messages \ - -F from='Excited User ' \ - -F to=YOU@YOUR_DOMAIN_NAME \ - -F to=bar@example.com \ - -F subject='Hello' \ - -F text='Testing some Mailgun awesomeness!' -*/ + 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 = {}; -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 text = "You tried connecting with '{{hostname}}' for the first time. Confirm to continue connecting:\n" - + '\n' - + ' https://' + state.config.webminDomain + '/login/#/magic={{id}}\n' - + '\n' - + "({{os_arch}} {{os_platform}} {{os_release}})\n" - + '\n' - ; - var html = "You tried connecting with '{{hostname}}' for the first time. Confirm to continue connecting:
" - + '
' - + '       Confirm Device
' - + '
' - + '       or copy and paste this link:
' - + '       https://' + state.config.webminDomain + '/login/#/magic={{id}}
' - + '
' - + "({{os_arch}} {{os_platform}} {{os_release}})
" - + '
' - ; - [ 'id', 'hostname', 'os_arch', 'os_platform', 'os_release' ].forEach(function (key) { - var val = escapeHtml(auth[key]); - subj = subj.replace(new RegExp('{{' + key + '}}', 'g'), val); - text = text.replace(new RegExp('{{' + key + '}}', 'g'), val); - html = html.replace(new RegExp('{{' + key + '}}', 'g'), val); - }); - return requestAsync({ - url: state.config.mailer.url - , method: 'POST' - , auth: { user: 'api', pass: state.config.mailer.apiKey } - , formData: { - from: state.config.mailer.from - , to: auth.subject - , subject: subj - , text: text - , html: html +function sendMail(state, auth) { + console.log('[DEBUG] ext auth', auth); + /* + curl -s --user 'api:YOUR_API_KEY' \ + https://api.mailgun.net/v3/YOUR_DOMAIN_NAME/messages \ + -F from='Excited User ' \ + -F to=YOU@YOUR_DOMAIN_NAME \ + -F to=bar@example.com \ + -F subject='Hello' \ + -F text='Testing some Mailgun awesomeness!' + */ + var subj = 'Confirm New Device Connection'; + var text = "You tried connecting with '{{hostname}}' for the first time. Confirm to continue connecting:\n" + + '\n' + + ' https://' + state.config.webminDomain + '/login/#/magic={{secret}}\n' + + '\n' + + "({{os_arch}} {{os_platform}} {{os_release}})\n" + + '\n' + ; + var html = "You tried connecting with '{{hostname}}' for the first time. Confirm to continue connecting:
" + + '
' + + '       Confirm Device
' + + '
' + + '       or copy and paste this link:
' + + '       https://' + state.config.webminDomain + '/login/#/magic={{secret}}
' + + '
' + + "({{os_arch}} {{os_platform}} {{os_release}})
" + + '
' + ; + [ 'id', 'secret', 'hostname', 'os_arch', 'os_platform', 'os_release' ].forEach(function (key) { + var val = escapeHtml(auth[key]); + subj = subj.replace(new RegExp('{{' + key + '}}', 'g'), val); + text = text.replace(new RegExp('{{' + key + '}}', 'g'), val); + html = html.replace(new RegExp('{{' + key + '}}', 'g'), val); + }); + return requestAsync({ + url: state.config.mailer.url + , method: 'POST' + , auth: { user: 'api', pass: state.config.mailer.apiKey } + , formData: { + from: state.config.mailer.from + , to: auth.subject + , subject: subj + , text: text + , html: html + } + }).then(function (resp) { + fs.writeFile(path.join(__dirname, 'emails', auth.subject), JSON.stringify(auth), function (err) { + if (err) { + console.error('[ERROR] in writing auth details'); + console.error(err); } - }).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) { - if (err) { - console.error('[ERROR] in writing auth details'); - console.error(err); - } - }); + }); + 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 + var t = setTimeout(function () { + console.log("[Magic Link] Timeout for '" + auth.subject + "'"); + delete _auths[auth.id]; + var err = new Error("Login Failure: Magic Link was not clicked within 5 minutes"); + err.code = 'E_LOGIN_TIMEOUT'; + reject(); + }, 2 * 60 * 60 * 1000); + + function authorize(pin) { + console.log("mighty auth'n ranger!"); + clearTimeout(t); + return module.exports.pairPin({ secret: auth.secret, pin: pin }).then(function (tokenData) { + // TODO call state object with socket info rather than resolve + resolve(tokenData); + return tokenData; + }, function (err) { + reject(err); + return state.Promise.reject(err); + }); + } + + _auths[auth.id].resolve = authorize; + _auths[auth.id].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) { - // TODO use global interval whenever the number of active links is high - var t = setTimeout(function () { - console.log("[Magic Link] Timeout for '" + auth.subject + "'"); - delete _auths[auth.id]; - var err = new Error("Login Failure: Magic Link was not clicked within 5 minutes"); - err.code = 'E_LOGIN_TIMEOUT'; - reject(); - }, 2 * 60 * 60 * 1000); - - function authorize() { - console.log("mighty auth'n ranger!"); - clearTimeout(t); - delete _auths[auth.id]; - var hri = require('human-readable-ids').hri; - 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); - 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; - } - - _auths[auth.id] = { - dt: Date.now() - , resolve: authorize - , reject: 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); }; + //var loaded = false; -var path = require('path'); var express = require('express'); var app = express(); var staticApp = express(); var nowww = require('nowww')(); var CORS = require('connect-cors'); +var bodyParser = require('body-parser'); staticApp.use('/', express.static(path.join(__dirname, 'admin'))); 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"); var tokenData; var magic = req.params.magic || req.query.magic; + var pin = req.params.pin || req.query.pin; 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"); - tokenData = _auths[magic].resolve(); + tokenData = _auths[magic].resolve(pin); console.log("DEBUG telebit.cloud magic 1c"); res.send(tokenData); } else { @@ -162,6 +265,7 @@ module.exports.webadmin = function (state, req, res) { } if ('api.' + state.config.webminDomain === host) { console.log("DEBUG going to api"); + req._state = state; app(req, res); return; }