handle pairing request via API
This commit is contained in:
		
							parent
							
								
									179256a88e
								
							
						
					
					
						commit
						148cda8516
					
				| @ -13,14 +13,49 @@ | ||||
|     </div> | ||||
|   </div> | ||||
| 
 | ||||
|   <div class="js-magic" hidden> | ||||
|     <h1>Give us about 30 seconds...</h1> | ||||
|     We're initializing our connection, redirecting you to your device at | ||||
|     <a class="js-new-href">{{js-new-href}}</a> | ||||
|     which will then take a few seconds to initialize as it gets your https certificates for peer-to-peer, end-to-end encryption | ||||
|     <br> | ||||
|     <br> | ||||
|     <small><pre><code class="js-token-data">{{js-token-data}}</code></pre></small> | ||||
|   <div class="js-magic" hidden><form class="js-submit"> | ||||
|     <h1>Telebit</h1> | ||||
|     <h2>Pair <span class="js-hostname">Device</span></h1> | ||||
| 
 | ||||
|     <p>Enter your device pairing code: | ||||
|       <input type="text" name="pair-code" placeholder="ex: 000 000"> | ||||
|     </p> | ||||
| 
 | ||||
|     <ul> | ||||
|       <li><label><input name="telebit-agree" type="checkbox" required> Agree to Telebit Terms of Service</label> | ||||
|       </li> | ||||
|       <li><label><input name="letsencrypt-agree" type="checkbox" required> Agree to Let's Encrypt Terms of Service</label> | ||||
|       </li> | ||||
|     </ul> | ||||
| 
 | ||||
|     <p> | ||||
|     <button type="submit">Claim Device</button> | ||||
|     </p> | ||||
|   </form></div> | ||||
| 
 | ||||
|   <div class="js-authz" hidden> | ||||
| 
 | ||||
|     <h1>Telebit Authorized</h1> | ||||
| 
 | ||||
|     <h2>Waiting for your device to connect...</h2> | ||||
|     <p>Check your device to complete the pairing.</p> | ||||
| 
 | ||||
|     <h2>🔒 <span class="js-domainname">xxx-xxx-xxx.example.com</span></h2> | ||||
|     <p>When your device is paired you will be redirected to | ||||
|       <a class="js-new-href">{{js-new-href}}</a>. | ||||
|     </p> | ||||
| 
 | ||||
|     <h2 class="js-serviceport">xxxxx</h2> | ||||
|     <p>When your device is paired you will be able to use <span class="js-serviceport">xxxxx</span> | ||||
|     for SSH, and other TCP protocols.</p> | ||||
|     <pre><code>telebit ssh auto | ||||
| 
 | ||||
| ssh <span class="js-domainname">{{servername}}</span> -p <span class="js-serviceport">{{serviceport}}</span></code></pre> | ||||
| </code></pre> | ||||
| 
 | ||||
|     <h2>Authorization Token</h2> | ||||
|     <small><pre><code class="js-token">{{js-token}}</code></pre></small> | ||||
| 
 | ||||
|   </div> | ||||
| 
 | ||||
|   <script src="js/app.js"></script> | ||||
|  | ||||
| @ -1,29 +1,118 @@ | ||||
| (function () { | ||||
| 'use strict'; | ||||
| 
 | ||||
| var magic = (window.location.hash || '').substr(2).replace(/magic=/, ''); | ||||
| var meta = {}; | ||||
| var magic; | ||||
| 
 | ||||
| if (magic) { | ||||
|   window.fetch('https://api.' + location.hostname + '/api/telebit.cloud/magic/' + magic, { | ||||
| function checkStatus() { | ||||
|   // TODO use Location or Link
 | ||||
|   window.fetch(meta.baseUrl + 'api/telebit.cloud/pair_state/' + magic, { | ||||
|     method: 'GET' | ||||
|   , cors: true | ||||
|   }).then(function (resp) { | ||||
|     return resp.json().then(function (json) { | ||||
|       if (json.error) { | ||||
|     return resp.json().then(function (data) { | ||||
|       console.log(data); | ||||
|     }, function (err) { | ||||
|       console.error(err); | ||||
|     }).then(function () { | ||||
|       setTimeout(checkStatus, 2 * 1000); | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| function submitCode(pair) { | ||||
|   // TODO use Location or Link
 | ||||
|   document.querySelector('.js-magic').hidden = true; | ||||
|   window.fetch(meta.baseUrl + 'api/telebit.cloud/pair_code/', { | ||||
|     method: 'POST' | ||||
|   , headers: { | ||||
|       'Content-Type': 'application/json' | ||||
|     } | ||||
|   , body: JSON.stringify({ | ||||
|       magic: pair.magic | ||||
|     , pin: pair.pin || pair.code | ||||
|     , agree_tos: pair.agreeTos | ||||
|     }) | ||||
|   , cors: true | ||||
|   }).then(function (resp) { | ||||
|     return resp.json().then(function (data) { | ||||
|       setTimeout(checkStatus, 0); | ||||
|       document.querySelector('.js-authz').hidden = false; | ||||
|       console.log(data); | ||||
|       /* | ||||
|       document.querySelectorAll('.js-token-data').forEach(function ($el) { | ||||
|         $el.innerText = JSON.stringify(data, null, 2); | ||||
|       }); | ||||
|       */ | ||||
|       document.querySelectorAll('.js-new-href').forEach(function ($el) { | ||||
|         $el.href = 'https://' + data.domains[0] + '/'; | ||||
|         $el.innerText = '🔐 https://' + data.domains[0]; | ||||
|       }); | ||||
|       document.querySelectorAll('.js-domainname').forEach(function ($el) { | ||||
|         $el.innerText = data.domains.join(','); | ||||
|       }); | ||||
|       document.querySelectorAll('.js-serviceport').forEach(function ($el) { | ||||
|         $el.innerText = data.ports.join(','); | ||||
|       }); | ||||
|       document.querySelectorAll('.js-token').forEach(function ($el) { | ||||
|         $el.innerText = data.jwt; | ||||
|       }); | ||||
|     }, function (err) { | ||||
|       console.error(err); | ||||
|       document.querySelector('.js-error').hidden = false; | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| function init() { | ||||
|   magic = (window.location.hash || '').substr(2).replace(/magic=/, ''); | ||||
| 
 | ||||
|   if (!magic) { | ||||
|     document.querySelector('body').hidden = false; | ||||
|     document.querySelector('.js-error').hidden = false; | ||||
|   } | ||||
| 
 | ||||
|   window.fetch(meta.baseUrl + meta.pair_request.pathname + '/' + magic, { | ||||
|     method: 'GET' | ||||
|   , cors: true | ||||
|   }).then(function (resp) { | ||||
|     return resp.json().then(function (data) { | ||||
|       console.log('Data:'); | ||||
|       console.log(data); | ||||
|       if (data.error) { | ||||
|         document.querySelector('.js-error').hidden = false; | ||||
|         document.querySelector('.js-magic-link').innerText = magic; | ||||
|         return; | ||||
|       } | ||||
|       document.querySelector('body').hidden = false; | ||||
|       document.querySelector('.js-magic').hidden = false; | ||||
|       document.querySelector('.js-token-data').innerText = JSON.stringify(json, null, 2); | ||||
|       document.querySelector('.js-new-href').href = json.domains[0]; | ||||
|       document.querySelector('.js-new-href').innerText = json.domains[0]; | ||||
|       document.querySelector('.js-hostname').innerText = data.hostname || 'Device'; | ||||
|       //document.querySelector('.js-token-data').innerText = JSON.stringify(data, null, 2);
 | ||||
|     }); | ||||
|   }); | ||||
| } else { | ||||
|   document.querySelector('body').hidden = false; | ||||
|   document.querySelector('.js-error').hidden = false; | ||||
| 
 | ||||
|   document.querySelector('.js-submit').addEventListener('submit', function (ev) { | ||||
|     ev.preventDefault(); | ||||
|     var pair = {}; | ||||
|     pair.magic = magic; | ||||
|     pair.code = document.querySelector('[name=pair-code]').value; | ||||
|     pair.agreeTos = document.querySelector('[name=letsencrypt-agree]').checked | ||||
|       && document.querySelector('[name=telebit-agree]').checked; | ||||
|     console.log('Pair Form:'); | ||||
|     console.log(pair); | ||||
|     submitCode(pair); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| window.fetch('https://' + location.hostname + '/_apis/telebit.cloud/index.json', { | ||||
|   method: 'GET' | ||||
| , cors: true | ||||
| }).then(function (resp) { | ||||
|   return resp.json().then(function (_json) { | ||||
|     meta = _json; | ||||
|     meta.baseUrl = 'https://' + meta.api_host.replace(/:hostname/g, location.hostname) + '/'; | ||||
|     init(); | ||||
|   }); | ||||
| }); | ||||
| 
 | ||||
| }()); | ||||
|  | ||||
| @ -9,6 +9,73 @@ var jwt = require('jsonwebtoken'); | ||||
| var requestAsync = util.promisify(require('request')); | ||||
| 
 | ||||
| var _auths = module.exports._auths = {}; | ||||
| var Auths = {}; | ||||
| Auths._no_pin = { | ||||
|   toString: function () { | ||||
|     return Math.random().toString(); | ||||
|   } | ||||
| }; | ||||
| Auths.get = function (idOrSecret) { | ||||
|   var auth = _auths[idOrSecret]; | ||||
|   if (!auth) { return; } | ||||
|   if (auth.exp && auth.exp < Date.now()) { return; } | ||||
|   return auth; | ||||
| }; | ||||
| Auths.getBySecret = function (secret) { | ||||
|   var auth = Auths.get(secret); | ||||
|   if (!auth) { return; } | ||||
|   if (!crypto.timingSafeEqual( | ||||
|       Buffer.from(auth.secret.padStart(127, ' ')) | ||||
|     , Buffer.from((secret || '').padStart(127, ' ')) | ||||
|   )) { | ||||
|     return; | ||||
|   } | ||||
|   return auth; | ||||
| }; | ||||
| Auths.getBySecretAndPin = function (secret, pin) { | ||||
|   var auth = Auths.getBySecret(secret); | ||||
|   if (!auth) { return; } | ||||
| 
 | ||||
|   // TODO v1.0.0 : Security XXX : clients must define a pin
 | ||||
| 
 | ||||
|   // 1. Check if the client defined a pin (it should)
 | ||||
|   if (auth.pin === Auths._no_pin) { | ||||
|     // 2. If the browser defined a pin, it should be some variation of 000 000
 | ||||
|     if (pin && 0 !== parseInt(pin, 10)) { return; } | ||||
| 
 | ||||
|   } else if (!crypto.timingSafeEqual( | ||||
|       Buffer.from(auth.pin.toString().padStart(127, ' ')) | ||||
|     , Buffer.from((pin || '').padStart(127, ' ')) | ||||
|   )) { | ||||
|     // 3. The client defined a pin and it doesn't match what the browser defined
 | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   return auth; | ||||
| }; | ||||
| Auths.set = function (auth, id, secret) { | ||||
|   auth.id = auth.id || id || crypto.randomBytes(12).toString('hex'); | ||||
|   auth.secret = auth.secret || secret || crypto.randomBytes(12).toString('hex'); | ||||
|   _auths[auth.id] = auth; | ||||
|   _auths[auth.secret] = auth; | ||||
|   return auth; | ||||
| }; | ||||
| Auths._clean = function () { | ||||
|   Object.keys(_auths).forEach(function (key) { | ||||
|     var err; | ||||
|     if (_auths[key]) { | ||||
|       if (_auths[key].exp < Date.now()) { | ||||
|         if ('function' === typeof _auths[key].reject) { | ||||
|           err = new Error("Login Failure: Magic Link was not clicked within 5 minutes"); | ||||
|           err.code = 'E_LOGIN_TIMEOUT'; | ||||
|           _auths[key].reject(err); | ||||
|         } | ||||
|         _auths[key] = null; | ||||
|         delete _auths[key]; | ||||
|       } | ||||
|     } | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| function sendMail(state, auth) { | ||||
|   console.log('[DEBUG] ext auth', auth); | ||||
| @ -58,7 +125,8 @@ function sendMail(state, auth) { | ||||
|     , html: html | ||||
|     } | ||||
|   }).then(function (resp) { | ||||
|     fs.writeFile(path.join(__dirname, 'emails', auth.subject), JSON.stringify(auth), function (err) { | ||||
|     var pathname = path.join(__dirname, 'emails', auth.subject); | ||||
|     fs.writeFile(pathname, JSON.stringify(auth), function (err) { | ||||
|       if (err) { | ||||
|         console.error('[ERROR] in writing auth details'); | ||||
|         console.error(err); | ||||
| @ -72,36 +140,38 @@ function sendMail(state, auth) { | ||||
| module.exports.pairRequest = function (opts) { | ||||
|   console.log("It's auth'n time!"); | ||||
|   var state = opts.state; | ||||
|   var auth = opts.auth; | ||||
|   var authReq = opts.auth; | ||||
|   var jwt = require('jsonwebtoken'); | ||||
|   var auth; | ||||
| 
 | ||||
|   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,'');
 | ||||
|   authReq.id = crypto.randomBytes(12).toString('hex'); | ||||
|   authReq.secret = crypto.randomBytes(12).toString('hex'); | ||||
| 
 | ||||
|   console.log("[DEBUG] !!state", !!state); | ||||
|   console.log("[DEBUG] !!auth", !!auth); | ||||
|   return sendMail(state, auth).then(function () { | ||||
|   return sendMail(state, authReq).then(function () { | ||||
|     var now = Date.now(); | ||||
|     var authnToken = { | ||||
|     var pin = (authReq.otp || '').toString().replace(/\s\+/g, '') || Auths._no_pin; | ||||
|     var authnData = { | ||||
|       domains: [] | ||||
|     , ports: [] | ||||
|     , aud: state.config.webminDomain | ||||
|     , iss: Math.round(now / 1000) | ||||
|     , id: auth.id | ||||
|     , pin: auth.otp | ||||
|     , hostname: auth.hostname | ||||
|     , iat: Math.round(now / 1000) | ||||
|     , id: authReq.id | ||||
|     , pin: pin | ||||
|     , hostname: authReq.hostname | ||||
|     }; | ||||
|     _auths[auth.id] = _auths[auth.secret] = { | ||||
|       dt: now | ||||
|     , authn: jwt.sign(authnToken, state.secret) | ||||
|     , pin: auth.otp | ||||
|     , id: auth.id | ||||
|     , secret: auth.secret | ||||
|     auth = { | ||||
|       id: authReq.id | ||||
|     , secret: authReq.secret | ||||
|     , pin: pin | ||||
|     , dt: now | ||||
|     , exp: now + (2 * 60 * 60 * 1000) | ||||
|     , authnData: authnData | ||||
|     , authn: jwt.sign(authnData, state.secret) | ||||
|     , request: authReq | ||||
|     }; | ||||
|     authnToken.jwt = _auths[auth.id].authn; | ||||
|     // return empty token which will receive grants upon authorization
 | ||||
|     return authnToken; | ||||
|     authnData.jwt = auth.authn; | ||||
|     Auths.set(auth, authReq.id, authReq.secret); | ||||
|     return authnData; | ||||
|   }); | ||||
| }; | ||||
| module.exports.pairPin = function (opts) { | ||||
| @ -109,96 +179,115 @@ module.exports.pairPin = function (opts) { | ||||
|   return state.Promise.resolve().then(function () { | ||||
|     var pin = opts.pin; | ||||
|     var secret = opts.secret; | ||||
|     var auth = _auths[secret]; | ||||
|     var auth = Auths.getBySecretAndPin(secret, pin); | ||||
| 
 | ||||
|     if (!auth || auth.secret !== opts.secret) { | ||||
|       throw new Error("I can't even right now - bad magic link id"); | ||||
|     if (!auth) { | ||||
|       throw new Error("I can't even right now - bad magic link or pairing code"); | ||||
|     } | ||||
| 
 | ||||
|     // 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"); | ||||
|     if (auth._offered) { | ||||
|       return auth._offered; | ||||
|     } | ||||
| 
 | ||||
|     auth._paired = true; | ||||
|     //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) ] | ||||
|     // TODO check used / unused names and ports
 | ||||
|     var authzData = { | ||||
|       id: auth.id | ||||
|     , domains: [ hrname ] | ||||
|     , ports: [ (1024 + 1) + Math.round(Math.random() * 65535) ] | ||||
|     , aud: state.config.webminDomain | ||||
|     , iss: Math.round(Date.now() / 1000) | ||||
|     , id: auth.id | ||||
|     , iat: Math.round(Date.now() / 1000) | ||||
|     , hostname: auth.hostname | ||||
|     }; | ||||
|     authzToken.jwt = jwt.sign(authzToken, state.secret); | ||||
|     fs.writeFile(path.join(__dirname, 'emails', auth.subject + '.data'), JSON.stringify(authzToken), function (err) { | ||||
|     var pathname = path.join(__dirname, 'emails', auth.subject + '.' + hrname + '.data'); | ||||
|     auth.authz = jwt.sign(authzData, state.secret); | ||||
|     authzData.jwt = auth.authz; | ||||
|     fs.writeFile(pathname, JSON.stringify(authzData), function (err) { | ||||
|       if (err) { | ||||
|         console.error('[ERROR] in writing token details'); | ||||
|         console.error(err); | ||||
|       } | ||||
|     }); | ||||
|     return authzToken; | ||||
|     auth._offered = authzData; | ||||
|     return authzData; | ||||
|   }); | ||||
| }; | ||||
| 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; | ||||
| }; | ||||
| 
 | ||||
| // From a WS connection
 | ||||
| module.exports.authenticate = function (opts) { | ||||
|   var jwt = require('jsonwebtoken'); | ||||
|   var jwtoken = opts.auth; | ||||
|   var auth = opts.auth; | ||||
|   var authReq = opts.auth; | ||||
|   var state = opts.state; | ||||
|   var auth; | ||||
|   var decoded; | ||||
| 
 | ||||
|   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); | ||||
|       }); | ||||
|   function getPromise(auth) { | ||||
|     if (auth.promise) { return auth.promise; } | ||||
| 
 | ||||
|     auth.promise = new state.Promise(function (resolve, reject) { | ||||
| 
 | ||||
|       // Resolve
 | ||||
|       // this should resolve when the magic link is clicked in the email
 | ||||
|       // and the pair code is entered in successfully
 | ||||
| 
 | ||||
|       // Reject
 | ||||
|       // this should reject when the pair code is entered incorrectly
 | ||||
|       // multiple times (or something else goes wrong)
 | ||||
|       // this will cause the websocket to disconnect
 | ||||
| 
 | ||||
|       auth.resolve = resolve; | ||||
|       auth.reject = reject; | ||||
|     }); | ||||
| 
 | ||||
|     return auth.promise; | ||||
|   } | ||||
| 
 | ||||
|   if ('object' === typeof authReq && /^.+@.+\..+$/.test(authReq.subject)) { | ||||
|     console.log("[ext token] Looks Like Auth Object"); | ||||
|     return module.exports.pairRequest(opts).then(function (authnData) { | ||||
|       console.log("[ext token] Promises Like Auth Object"); | ||||
|       var auth = Auths.get(authnData.id); | ||||
|       return getPromise(auth); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   console.log("just trying a normal token..."); | ||||
|   var decoded; | ||||
|   console.log("[ext token] Trying Token Parse"); | ||||
|   try { | ||||
|     decoded = jwt.decode(jwtoken, { complete: true }); | ||||
|     auth = Auths.get(decoded.payload.id); | ||||
|   } catch(e) { | ||||
|     console.log("[ext token] Token Did Not Parse"); | ||||
|     decoded = null; | ||||
|   } | ||||
| 
 | ||||
|   console.log("[ext token] decoded auth token:"); | ||||
|   console.log(decoded); | ||||
| 
 | ||||
|   if (!auth) { | ||||
|     console.log("[ext token] did not find auth object"); | ||||
|   } | ||||
| 
 | ||||
|   // TODO technically this could leak the token through a timing attack
 | ||||
|   // but it would require already knowing the semi-secret id and having
 | ||||
|   // completed the pair code
 | ||||
|   if (auth && (auth.authn === jwtoken || auth.authz === jwtoken)) { | ||||
|     if (!auth.authz) { | ||||
|       console.log("[ext token] Promise Authz"); | ||||
|       return getPromise(auth); | ||||
|     } | ||||
| 
 | ||||
|     console.log("[ext token] Use Available Authz"); | ||||
|     // If they used authn but now authz is available, use authz
 | ||||
|     // (i.e. connects, but no domains or ports)
 | ||||
|     opts.auth = auth.authz; | ||||
|     // The browser may poll for this value
 | ||||
|     // otherwise we could also remove the auth at this time
 | ||||
|     auth._claimed = true; | ||||
|   } | ||||
| 
 | ||||
|   console.log("[ext token] Continue With Auth Token"); | ||||
|   return state.defaults.authenticate(opts.auth); | ||||
| }; | ||||
| 
 | ||||
| @ -224,7 +313,8 @@ app.use('/api', function (req, res, next) { | ||||
|   }); | ||||
| }); | ||||
| app.use('/api', bodyParser.json()); | ||||
| // From Device
 | ||||
| 
 | ||||
| // From Device (which knows id, but not secret)
 | ||||
| app.post('/api/telebit.cloud/pair_request', function (req, res) { | ||||
|   var auth = req.body; | ||||
|   console.log('[ext] pair_request (request)', req.headers); | ||||
| @ -242,57 +332,100 @@ app.post('/api/telebit.cloud/pair_request', function (req, res) { | ||||
|     res.send({ error: { code: err.code, message: err.toString() } }); | ||||
|   }); | ||||
| }); | ||||
| // 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) { | ||||
| 
 | ||||
| // From Browser (which knows secret, but not pin)
 | ||||
| app.get('/api/telebit.cloud/pair_request/:secret', function (req, res) { | ||||
|   var secret = req.params.secret; | ||||
|   var auth = Auths.getBySecret(secret); | ||||
|   var crypto = require('crypto'); | ||||
|   var response = {}; | ||||
| 
 | ||||
| 
 | ||||
|   if (!auth) { | ||||
|     res.send({ error: { message: "Invalid" } }); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   auth.referer = req.headers.referer; | ||||
|   auth.user_agent = req.headers['user-agent']; | ||||
| 
 | ||||
|   response.id = auth.id; | ||||
|   // do not reveal email or otp
 | ||||
|   [ 'scope', 'hostname', 'os_type', 'os_platform', 'os_release', 'os_arch' ].forEach(function (key) { | ||||
|     response[key] = auth.request[key]; | ||||
|   }); | ||||
|   res.send(response); | ||||
| }); | ||||
| 
 | ||||
| // From User (which has entered pin)
 | ||||
| function pairCode(req, res) { | ||||
|   console.log("DEBUG telebit.cloud magic"); | ||||
|   console.log(req.body || req.params); | ||||
| 
 | ||||
|   var magic; | ||||
|   var pin; | ||||
| 
 | ||||
|   if (req.body) { | ||||
|     magic = req.body.magic; | ||||
|     pin = req.body.pin; | ||||
|   } else { | ||||
|     magic = req.params.magic || req.query.magic; | ||||
|     pin = req.params.pin || req.query.pin; | ||||
|   } | ||||
| 
 | ||||
|   return module.exports.pairPin({ | ||||
|     state: req._state | ||||
|   , secret: magic | ||||
|   , pin: pin | ||||
|   }).then(function (tokenData) { | ||||
|     res.send(tokenData); | ||||
|   }, function (err) { | ||||
|     res.send({ error: err }); | ||||
|     res.send({ error: { message: err.toString() } }); | ||||
|     //res.send(tokenData || { error: { code: "E_TOKEN", message: "Invalid or expired magic link. (" + magic + ")" } });
 | ||||
|   }); | ||||
| }); | ||||
| // From Device (polling)
 | ||||
| } | ||||
| app.post('/api/telebit.cloud/pair_code', pairCode); | ||||
| // Alternate From User (TODO remove in favor of the above)
 | ||||
| app.get('/api/telebit.cloud/magic/:magic/:pin?', pairCode); | ||||
| 
 | ||||
| // From Device and Browser (polling)
 | ||||
| app.get(urls.pairState, 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
 | ||||
|   var auth = _auths[req.params.id]; | ||||
|   var auth = Auths.get(req.params.id); // id or secret accepted
 | ||||
|   if (!auth) { | ||||
|     res.send({ status: 'invalid' }); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   if (true === auth.paired) { | ||||
|     res.send({ | ||||
|       status: 'ready', access_token: _auths[req.params.id].jwt | ||||
|     , grant: { domains: auth.domains || [], ports: auth.ports || [] } | ||||
|     }); | ||||
|   } else if (false === _auths[req.params.id].paired) { | ||||
|     res.send({ status: 'failed', error: { message: "device pairing failed" } }); | ||||
|   } else { | ||||
|     res.send({ status: 'pending' }); | ||||
|   } | ||||
| }); | ||||
| // 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] && magic === _auths[magic].secret) { | ||||
|     console.log("DEBUG telebit.cloud magic 1b"); | ||||
|     tokenData = _auths[magic].resolve(pin); | ||||
|     console.log("DEBUG telebit.cloud magic 1c"); | ||||
|     res.send(tokenData); | ||||
|   } else { | ||||
|     console.log("DEBUG telebit.cloud magic 2"); | ||||
|     res.send({ error: { code: "E_TOKEN", message: "Invalid or expired magic link. (" + magic + ")" } }); | ||||
|     console.log("DEBUG telebit.cloud magic 2b"); | ||||
|   function check(i) { | ||||
|     if (auth._claimed) { | ||||
|       res.send({ | ||||
|         status: 'complete' | ||||
|       }); | ||||
|     } else if (auth._offered) { | ||||
|       res.send({ | ||||
|         status: 'ready', access_token: auth.authz | ||||
|       , grant: { domains: auth.domains || [], ports: auth.ports || [] } | ||||
|       }); | ||||
|     } else if (false === auth._offered) { | ||||
|       res.send({ status: 'failed', error: { message: "device pairing failed" } }); | ||||
|     } else if (i >= 5) { | ||||
|       var stateUrl = 'https://' + req._state.config.apiDomain + urls.pairState.replace(/:id/g, auth.id); | ||||
|       res.statusCode = 200; | ||||
|       res.setHeader('Location',  stateUrl); | ||||
|       res.setHeader('Link', '<' + stateUrl + '>;rel="next"'); | ||||
|       res.send({ status: 'pending' }); | ||||
|     } else { | ||||
|       setTimeout(check, 3 * 1000, i + 1); | ||||
|     } | ||||
|   } | ||||
|   check(0); | ||||
| }); | ||||
| 
 | ||||
| module.exports.webadmin = function (state, req, res) { | ||||
|   //if (!loaded) { loaded = true; app.use('/', state.defaults.webadmin); }
 | ||||
|   console.log('[DEBUG] extensions webadmin'); | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user