handle pairing request via API
This commit is contained in:
		
							parent
							
								
									179256a88e
								
							
						
					
					
						commit
						148cda8516
					
				@ -13,14 +13,49 @@
 | 
				
			|||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <div class="js-magic" hidden>
 | 
					  <div class="js-magic" hidden><form class="js-submit">
 | 
				
			||||||
    <h1>Give us about 30 seconds...</h1>
 | 
					    <h1>Telebit</h1>
 | 
				
			||||||
    We're initializing our connection, redirecting you to your device at
 | 
					    <h2>Pair <span class="js-hostname">Device</span></h1>
 | 
				
			||||||
    <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
 | 
					    <p>Enter your device pairing code:
 | 
				
			||||||
    <br>
 | 
					      <input type="text" name="pair-code" placeholder="ex: 000 000">
 | 
				
			||||||
    <br>
 | 
					    </p>
 | 
				
			||||||
    <small><pre><code class="js-token-data">{{js-token-data}}</code></pre></small>
 | 
					
 | 
				
			||||||
 | 
					    <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>
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <script src="js/app.js"></script>
 | 
					  <script src="js/app.js"></script>
 | 
				
			||||||
 | 
				
			|||||||
@ -1,29 +1,118 @@
 | 
				
			|||||||
(function () {
 | 
					(function () {
 | 
				
			||||||
'use strict';
 | 
					'use strict';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var magic = (window.location.hash || '').substr(2).replace(/magic=/, '');
 | 
					var meta = {};
 | 
				
			||||||
 | 
					var magic;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if (magic) {
 | 
					function checkStatus() {
 | 
				
			||||||
  window.fetch('https://api.' + location.hostname + '/api/telebit.cloud/magic/' + magic, {
 | 
					  // TODO use Location or Link
 | 
				
			||||||
 | 
					  window.fetch(meta.baseUrl + 'api/telebit.cloud/pair_state/' + magic, {
 | 
				
			||||||
    method: 'GET'
 | 
					    method: 'GET'
 | 
				
			||||||
  , cors: true
 | 
					  , cors: true
 | 
				
			||||||
  }).then(function (resp) {
 | 
					  }).then(function (resp) {
 | 
				
			||||||
    return resp.json().then(function (json) {
 | 
					    return resp.json().then(function (data) {
 | 
				
			||||||
      if (json.error) {
 | 
					      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-error').hidden = false;
 | 
				
			||||||
        document.querySelector('.js-magic-link').innerText = magic;
 | 
					        document.querySelector('.js-magic-link').innerText = magic;
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      document.querySelector('body').hidden = false;
 | 
					      document.querySelector('body').hidden = false;
 | 
				
			||||||
      document.querySelector('.js-magic').hidden = false;
 | 
					      document.querySelector('.js-magic').hidden = false;
 | 
				
			||||||
      document.querySelector('.js-token-data').innerText = JSON.stringify(json, null, 2);
 | 
					      document.querySelector('.js-hostname').innerText = data.hostname || 'Device';
 | 
				
			||||||
      document.querySelector('.js-new-href').href = json.domains[0];
 | 
					      //document.querySelector('.js-token-data').innerText = JSON.stringify(data, null, 2);
 | 
				
			||||||
      document.querySelector('.js-new-href').innerText = json.domains[0];
 | 
					 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
} else {
 | 
					
 | 
				
			||||||
  document.querySelector('body').hidden = false;
 | 
					  document.querySelector('.js-submit').addEventListener('submit', function (ev) {
 | 
				
			||||||
  document.querySelector('.js-error').hidden = false;
 | 
					    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 requestAsync = util.promisify(require('request'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var _auths = module.exports._auths = {};
 | 
					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) {
 | 
					function sendMail(state, auth) {
 | 
				
			||||||
  console.log('[DEBUG] ext auth', auth);
 | 
					  console.log('[DEBUG] ext auth', auth);
 | 
				
			||||||
@ -58,7 +125,8 @@ function sendMail(state, auth) {
 | 
				
			|||||||
    , html: html
 | 
					    , html: html
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }).then(function (resp) {
 | 
					  }).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) {
 | 
					      if (err) {
 | 
				
			||||||
        console.error('[ERROR] in writing auth details');
 | 
					        console.error('[ERROR] in writing auth details');
 | 
				
			||||||
        console.error(err);
 | 
					        console.error(err);
 | 
				
			||||||
@ -72,36 +140,38 @@ function sendMail(state, auth) {
 | 
				
			|||||||
module.exports.pairRequest = function (opts) {
 | 
					module.exports.pairRequest = function (opts) {
 | 
				
			||||||
  console.log("It's auth'n time!");
 | 
					  console.log("It's auth'n time!");
 | 
				
			||||||
  var state = opts.state;
 | 
					  var state = opts.state;
 | 
				
			||||||
  var auth = opts.auth;
 | 
					  var authReq = opts.auth;
 | 
				
			||||||
  var jwt = require('jsonwebtoken');
 | 
					  var jwt = require('jsonwebtoken');
 | 
				
			||||||
 | 
					  var auth;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  auth.id = crypto.randomBytes(12).toString('hex');
 | 
					  authReq.id = crypto.randomBytes(12).toString('hex');
 | 
				
			||||||
  auth.secret = crypto.randomBytes(12).toString('hex');
 | 
					  authReq.secret = crypto.randomBytes(12).toString('hex');
 | 
				
			||||||
  //var id = crypto.randomBytes(16).toString('base64').replace(/\+/g,'-').replace(/\//g,'_').replace(/=/g,'');
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  console.log("[DEBUG] !!state", !!state);
 | 
					  return sendMail(state, authReq).then(function () {
 | 
				
			||||||
  console.log("[DEBUG] !!auth", !!auth);
 | 
					 | 
				
			||||||
  return sendMail(state, auth).then(function () {
 | 
					 | 
				
			||||||
    var now = Date.now();
 | 
					    var now = Date.now();
 | 
				
			||||||
    var authnToken = {
 | 
					    var pin = (authReq.otp || '').toString().replace(/\s\+/g, '') || Auths._no_pin;
 | 
				
			||||||
 | 
					    var authnData = {
 | 
				
			||||||
      domains: []
 | 
					      domains: []
 | 
				
			||||||
    , ports: []
 | 
					    , ports: []
 | 
				
			||||||
    , aud: state.config.webminDomain
 | 
					    , aud: state.config.webminDomain
 | 
				
			||||||
    , iss: Math.round(now / 1000)
 | 
					    , iat: Math.round(now / 1000)
 | 
				
			||||||
    , id: auth.id
 | 
					    , id: authReq.id
 | 
				
			||||||
    , pin: auth.otp
 | 
					    , pin: pin
 | 
				
			||||||
    , hostname: auth.hostname
 | 
					    , hostname: authReq.hostname
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
    _auths[auth.id] = _auths[auth.secret] = {
 | 
					    auth = {
 | 
				
			||||||
      dt: now
 | 
					      id: authReq.id
 | 
				
			||||||
    , authn: jwt.sign(authnToken, state.secret)
 | 
					    , secret: authReq.secret
 | 
				
			||||||
    , pin: auth.otp
 | 
					    , pin: pin
 | 
				
			||||||
    , id: auth.id
 | 
					    , dt: now
 | 
				
			||||||
    , secret: auth.secret
 | 
					    , exp: now + (2 * 60 * 60 * 1000)
 | 
				
			||||||
 | 
					    , authnData: authnData
 | 
				
			||||||
 | 
					    , authn: jwt.sign(authnData, state.secret)
 | 
				
			||||||
 | 
					    , request: authReq
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
    authnToken.jwt = _auths[auth.id].authn;
 | 
					    authnData.jwt = auth.authn;
 | 
				
			||||||
    // return empty token which will receive grants upon authorization
 | 
					    Auths.set(auth, authReq.id, authReq.secret);
 | 
				
			||||||
    return authnToken;
 | 
					    return authnData;
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
module.exports.pairPin = function (opts) {
 | 
					module.exports.pairPin = function (opts) {
 | 
				
			||||||
@ -109,96 +179,115 @@ module.exports.pairPin = function (opts) {
 | 
				
			|||||||
  return state.Promise.resolve().then(function () {
 | 
					  return state.Promise.resolve().then(function () {
 | 
				
			||||||
    var pin = opts.pin;
 | 
					    var pin = opts.pin;
 | 
				
			||||||
    var secret = opts.secret;
 | 
					    var secret = opts.secret;
 | 
				
			||||||
    var auth = _auths[secret];
 | 
					    var auth = Auths.getBySecretAndPin(secret, pin);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!auth || auth.secret !== opts.secret) {
 | 
					    if (!auth) {
 | 
				
			||||||
      throw new Error("I can't even right now - bad magic link id");
 | 
					      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,
 | 
					    if (auth._offered) {
 | 
				
			||||||
    // regardless of what the client sends. This bad logic is just for testing.
 | 
					      return auth._offered;
 | 
				
			||||||
    if (pin && auth.pin && pin !== auth.pin) {
 | 
					 | 
				
			||||||
      throw new Error("I can't even right now - bad device pair pin");
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    auth._paired = true;
 | 
					 | 
				
			||||||
    //delete _auths[auth.id];
 | 
					 | 
				
			||||||
    var hri = require('human-readable-ids').hri;
 | 
					    var hri = require('human-readable-ids').hri;
 | 
				
			||||||
    var hrname = hri.random() + '.' + state.config.sharedDomain;
 | 
					    var hrname = hri.random() + '.' + state.config.sharedDomain;
 | 
				
			||||||
    var authzToken = {
 | 
					    // TODO check used / unused names and ports
 | 
				
			||||||
      domains: [ hrname ]
 | 
					    var authzData = {
 | 
				
			||||||
    , ports: [ (1024 + 1) + Math.round(Math.random() * 6300) ]
 | 
					      id: auth.id
 | 
				
			||||||
 | 
					    , domains: [ hrname ]
 | 
				
			||||||
 | 
					    , ports: [ (1024 + 1) + Math.round(Math.random() * 65535) ]
 | 
				
			||||||
    , aud: state.config.webminDomain
 | 
					    , aud: state.config.webminDomain
 | 
				
			||||||
    , iss: Math.round(Date.now() / 1000)
 | 
					    , iat: Math.round(Date.now() / 1000)
 | 
				
			||||||
    , id: auth.id
 | 
					 | 
				
			||||||
    , hostname: auth.hostname
 | 
					    , hostname: auth.hostname
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
    authzToken.jwt = jwt.sign(authzToken, state.secret);
 | 
					    var pathname = path.join(__dirname, 'emails', auth.subject + '.' + hrname + '.data');
 | 
				
			||||||
    fs.writeFile(path.join(__dirname, 'emails', auth.subject + '.data'), JSON.stringify(authzToken), function (err) {
 | 
					    auth.authz = jwt.sign(authzData, state.secret);
 | 
				
			||||||
 | 
					    authzData.jwt = auth.authz;
 | 
				
			||||||
 | 
					    fs.writeFile(pathname, JSON.stringify(authzData), function (err) {
 | 
				
			||||||
      if (err) {
 | 
					      if (err) {
 | 
				
			||||||
        console.error('[ERROR] in writing token details');
 | 
					        console.error('[ERROR] in writing token details');
 | 
				
			||||||
        console.error(err);
 | 
					        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) {
 | 
					module.exports.authenticate = function (opts) {
 | 
				
			||||||
  var jwt = require('jsonwebtoken');
 | 
					  var jwt = require('jsonwebtoken');
 | 
				
			||||||
  var jwtoken = opts.auth;
 | 
					  var jwtoken = opts.auth;
 | 
				
			||||||
  var auth = opts.auth;
 | 
					  var authReq = opts.auth;
 | 
				
			||||||
  var state = opts.state;
 | 
					  var state = opts.state;
 | 
				
			||||||
 | 
					  var auth;
 | 
				
			||||||
 | 
					  var decoded;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if ('object' === typeof auth && /^.+@.+\..+$/.test(auth.subject)) {
 | 
					  function getPromise(auth) {
 | 
				
			||||||
    return module.exports.pairRequest(opts).then(function () {
 | 
					    if (auth.promise) { return auth.promise; }
 | 
				
			||||||
      return new state.Promise(function (resolve, reject) {
 | 
					
 | 
				
			||||||
        opts.resolve = resolve;
 | 
					    auth.promise = new state.Promise(function (resolve, reject) {
 | 
				
			||||||
        opts.reject = reject;
 | 
					
 | 
				
			||||||
        module.exports.pairState(opts);
 | 
					      // 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...");
 | 
					  console.log("[ext token] Trying Token Parse");
 | 
				
			||||||
  var decoded;
 | 
					 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    decoded = jwt.decode(jwtoken, { complete: true });
 | 
					    decoded = jwt.decode(jwtoken, { complete: true });
 | 
				
			||||||
 | 
					    auth = Auths.get(decoded.payload.id);
 | 
				
			||||||
  } catch(e) {
 | 
					  } catch(e) {
 | 
				
			||||||
 | 
					    console.log("[ext token] Token Did Not Parse");
 | 
				
			||||||
    decoded = null;
 | 
					    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);
 | 
					  return state.defaults.authenticate(opts.auth);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -224,7 +313,8 @@ app.use('/api', function (req, res, next) {
 | 
				
			|||||||
  });
 | 
					  });
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
app.use('/api', bodyParser.json());
 | 
					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) {
 | 
					app.post('/api/telebit.cloud/pair_request', function (req, res) {
 | 
				
			||||||
  var auth = req.body;
 | 
					  var auth = req.body;
 | 
				
			||||||
  console.log('[ext] pair_request (request)', req.headers);
 | 
					  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() } });
 | 
					    res.send({ error: { code: err.code, message: err.toString() } });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
// From Browser
 | 
					
 | 
				
			||||||
app.post('/api/telebit.cloud/pair_code', function (req, res) {
 | 
					// From Browser (which knows secret, but not pin)
 | 
				
			||||||
  var auth = req.body;
 | 
					app.get('/api/telebit.cloud/pair_request/:secret', function (req, res) {
 | 
				
			||||||
  return module.exports.pairPin({ secret: auth.magic, pin: auth.pin }).then(function (tokenData) {
 | 
					  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);
 | 
					    res.send(tokenData);
 | 
				
			||||||
  }, function (err) {
 | 
					  }, 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) {
 | 
					app.get(urls.pairState, function (req, res) {
 | 
				
			||||||
  // check if pair is complete
 | 
					  // check if pair is complete
 | 
				
			||||||
  // respond immediately if so
 | 
					  // respond immediately if so
 | 
				
			||||||
  // wait for a little bit otherwise
 | 
					  // wait for a little bit otherwise
 | 
				
			||||||
  // respond if/when it completes
 | 
					  // respond if/when it completes
 | 
				
			||||||
  // or respond after time if it does not complete
 | 
					  // 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) {
 | 
					  if (!auth) {
 | 
				
			||||||
    res.send({ status: 'invalid' });
 | 
					    res.send({ status: 'invalid' });
 | 
				
			||||||
    return;
 | 
					    return;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (true === auth.paired) {
 | 
					  function check(i) {
 | 
				
			||||||
 | 
					    if (auth._claimed) {
 | 
				
			||||||
      res.send({
 | 
					      res.send({
 | 
				
			||||||
      status: 'ready', access_token: _auths[req.params.id].jwt
 | 
					        status: 'complete'
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    } else if (auth._offered) {
 | 
				
			||||||
 | 
					      res.send({
 | 
				
			||||||
 | 
					        status: 'ready', access_token: auth.authz
 | 
				
			||||||
      , grant: { domains: auth.domains || [], ports: auth.ports || [] }
 | 
					      , grant: { domains: auth.domains || [], ports: auth.ports || [] }
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
  } else if (false === _auths[req.params.id].paired) {
 | 
					    } else if (false === auth._offered) {
 | 
				
			||||||
      res.send({ status: 'failed', error: { message: "device pairing failed" } });
 | 
					      res.send({ status: 'failed', error: { message: "device pairing failed" } });
 | 
				
			||||||
  } else {
 | 
					    } 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' });
 | 
					      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 {
 | 
					    } else {
 | 
				
			||||||
    console.log("DEBUG telebit.cloud magic 2");
 | 
					      setTimeout(check, 3 * 1000, i + 1);
 | 
				
			||||||
    res.send({ error: { code: "E_TOKEN", message: "Invalid or expired magic link. (" + magic + ")" } });
 | 
					 | 
				
			||||||
    console.log("DEBUG telebit.cloud magic 2b");
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  check(0);
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports.webadmin = function (state, req, res) {
 | 
					module.exports.webadmin = function (state, req, res) {
 | 
				
			||||||
  //if (!loaded) { loaded = true; app.use('/', state.defaults.webadmin); }
 | 
					  //if (!loaded) { loaded = true; app.use('/', state.defaults.webadmin); }
 | 
				
			||||||
  console.log('[DEBUG] extensions webadmin');
 | 
					  console.log('[DEBUG] extensions webadmin');
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user