WIP api token for accounts
This commit is contained in:
		
							parent
							
								
									09b1d5939e
								
							
						
					
					
						commit
						114cc53dd4
					
				| @ -1,117 +1,188 @@ | |||||||
| 'use strict'; | 'use strict'; | ||||||
| /* | 
 | ||||||
| curl -s --user 'api:YOUR_API_KEY' \ |  | ||||||
|     https://api.mailgun.net/v3/YOUR_DOMAIN_NAME/messages \
 |  | ||||||
|     -F from='Excited User <mailgun@YOUR_DOMAIN_NAME>' \ |  | ||||||
|     -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 fs = require('fs'); | ||||||
|  | var path = require('path'); | ||||||
|  | var util = require('util'); | ||||||
|  | var crypto = require('crypto'); | ||||||
| var escapeHtml = require('escape-html'); | var escapeHtml = require('escape-html'); | ||||||
|  | var jwt = require('jsonwebtoken'); | ||||||
|  | var requestAsync = util.promisify(require('request')); | ||||||
|  | 
 | ||||||
| var _auths = module.exports._auths = {}; | 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); | function sendMail(state, auth) { | ||||||
|   auth = jwtoken; |   console.log('[DEBUG] ext auth', auth); | ||||||
|   if ('object' === typeof auth && /^.+@.+\..+$/.test(auth.subject)) { |   /* | ||||||
|     console.log("[DEBUG] gonna send email"); |   curl -s --user 'api:YOUR_API_KEY' \ | ||||||
|     auth.id = crypto.randomBytes(12).toString('hex'); |       https://api.mailgun.net/v3/YOUR_DOMAIN_NAME/messages \
 | ||||||
|     //var id = crypto.randomBytes(16).toString('base64').replace(/\+/g,'-').replace(/\//g,'_').replace(/=/g,'');
 |       -F from='Excited User <mailgun@YOUR_DOMAIN_NAME>' \ | ||||||
|     var subj = 'Confirm New Device Connection'; |       -F to=YOU@YOUR_DOMAIN_NAME \ | ||||||
|     var text = "You tried connecting with '{{hostname}}' for the first time. Confirm to continue connecting:\n" |       -F to=bar@example.com \ | ||||||
|           + '\n' |       -F subject='Hello' \ | ||||||
|           + '    https://' + state.config.webminDomain + '/login/#/magic={{id}}\n' |       -F text='Testing some Mailgun awesomeness!' | ||||||
|           + '\n' |   */ | ||||||
|           + "({{os_arch}} {{os_platform}} {{os_release}})\n" |   var subj = 'Confirm New Device Connection'; | ||||||
|           + '\n' |   var text = "You tried connecting with '{{hostname}}' for the first time. Confirm to continue connecting:\n" | ||||||
|           ; |         + '\n' | ||||||
|     var html = "You tried connecting with '{{hostname}}' for the first time. Confirm to continue connecting:<br>" |         + '    https://' + state.config.webminDomain + '/login/#/magic={{secret}}\n' | ||||||
|           + '<br>' |         + '\n' | ||||||
|           + '          <a href="https://' + state.config.webminDomain + '/login/#/magic={{id}}">Confirm Device</a><br>' |         + "({{os_arch}} {{os_platform}} {{os_release}})\n" | ||||||
|           + '<br>' |         + '\n' | ||||||
|           + '          <small>or copy and paste this link:</small><br>' |         ; | ||||||
|           + '          <small>https://' + state.config.webminDomain + '/login/#/magic={{id}}</small><br>' |   var html = "You tried connecting with '{{hostname}}' for the first time. Confirm to continue connecting:<br>" | ||||||
|           + '<br>' |         + '<br>' | ||||||
|           + "({{os_arch}} {{os_platform}} {{os_release}})<br>" |         + '          <a href="https://' + state.config.webminDomain + '/login/#/magic={{secret}}">Confirm Device</a><br>' | ||||||
|           + '<br>' |         + '<br>' | ||||||
|           ; |         + '          <small>or copy and paste this link:</small><br>' | ||||||
|     [ 'id', 'hostname', 'os_arch', 'os_platform', 'os_release' ].forEach(function (key) { |         + '          <small>https://' + state.config.webminDomain + '/login/#/magic={{secret}}</small><br>' | ||||||
|       var val = escapeHtml(auth[key]); |         + '<br>' | ||||||
|       subj = subj.replace(new RegExp('{{' + key + '}}', 'g'), val); |         + "({{os_arch}} {{os_platform}} {{os_release}})<br>" | ||||||
|       text = text.replace(new RegExp('{{' + key + '}}', 'g'), val); |         + '<br>' | ||||||
|       html = html.replace(new RegExp('{{' + key + '}}', 'g'), val); |         ; | ||||||
|     }); |   [ 'id', 'secret', 'hostname', 'os_arch', 'os_platform', 'os_release' ].forEach(function (key) { | ||||||
|     return requestAsync({ |     var val = escapeHtml(auth[key]); | ||||||
|       url: state.config.mailer.url |     subj = subj.replace(new RegExp('{{' + key + '}}', 'g'), val); | ||||||
|     , method: 'POST' |     text = text.replace(new RegExp('{{' + key + '}}', 'g'), val); | ||||||
|     , auth: { user: 'api', pass: state.config.mailer.apiKey } |     html = html.replace(new RegExp('{{' + key + '}}', 'g'), val); | ||||||
|     , formData: { |   }); | ||||||
|         from: state.config.mailer.from  |   return requestAsync({ | ||||||
|       , to: auth.subject |     url: state.config.mailer.url | ||||||
|       , subject: subj  |   , method: 'POST' | ||||||
|       , text: text |   , auth: { user: 'api', pass: state.config.mailer.apiKey } | ||||||
|       , html: html |   , 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("[DEBUG] email was sent, or so they say"); | ||||||
|       console.log(resp.body); |     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); | 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) { |       return new state.Promise(function (resolve, reject) { | ||||||
|         // TODO use global interval whenever the number of active links is high
 |         opts.resolve = resolve; | ||||||
|         var t = setTimeout(function () { |         opts.reject = reject; | ||||||
|           console.log("[Magic Link] Timeout for '" + auth.subject + "'"); |         module.exports.pairState(opts); | ||||||
|           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 |  | ||||||
|         }; |  | ||||||
| 
 |  | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
| @ -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; | ||||||
|   } |   } | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user