WIP implementing semi-proper accounts
This commit is contained in:
parent
1099a75509
commit
64281e4c93
|
@ -3,8 +3,9 @@
|
|||
<title>Telebit Account</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Login</h1>
|
||||
<form class="js-auth-form">
|
||||
<input class="js-auth-subject" type="email"/>
|
||||
<input class="js-auth-subject" placeholder="email" type="email"/>
|
||||
<button class="js-auth-submit" type="submit">Login</button>
|
||||
</form>
|
||||
|
||||
|
@ -30,9 +31,12 @@
|
|||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
var email = $('.js-auth-subject').value;
|
||||
|
||||
// TODO check subject for provider viability
|
||||
return oauth3.authenticate({
|
||||
subject: $('.js-auth-subject').value
|
||||
subject: email
|
||||
, scope: 'email@oauth3.org'
|
||||
}).then(function (session) {
|
||||
|
||||
console.info('Authentication was Successful:');
|
||||
|
@ -44,6 +48,10 @@
|
|||
//
|
||||
console.info('Secure PPID (aka subject):', session.token.sub);
|
||||
|
||||
function listStuff() {
|
||||
window.alert("TODO: show authorized devices, domains, and connectivity information");
|
||||
}
|
||||
|
||||
return oauth3.request({
|
||||
url: 'https://api.oauth3.org/api/issuer@oauth3.org/jwks/:sub/:kid.json'
|
||||
.replace(/:sub/g, session.token.sub)
|
||||
|
@ -62,13 +70,32 @@
|
|||
console.log(resp.data);
|
||||
|
||||
return oauth3.request({
|
||||
url: 'https://api.telebit.cloud/api/telebit.cloud/account'
|
||||
url: 'https://api.' + location.hostname + '/api/telebit.cloud/account'
|
||||
, session: session
|
||||
}).then(function (resp) {
|
||||
|
||||
console.info("Telebit Account:");
|
||||
console.log(resp.data);
|
||||
|
||||
if (1 === resp.data.accounts.length) {
|
||||
listStuff(resp);
|
||||
} else if (0 === resp.data.accounts.length) {
|
||||
return oauth3.request({
|
||||
url: 'https://api.' + location.hostname + 'api/telebit.cloud/account'
|
||||
, method: 'POST'
|
||||
, session: session
|
||||
, body: {
|
||||
email: email
|
||||
}
|
||||
}).then(function (resp) {
|
||||
listStuff(resp);
|
||||
});
|
||||
} if (resp.data.accounts.length > 2) {
|
||||
window.alert("Multiple accounts.");
|
||||
} else {
|
||||
window.alert("Bad response.");
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,14 @@
|
|||
'use strict';
|
||||
|
||||
var PromiseA;
|
||||
try {
|
||||
PromiseA = require('bluebird');
|
||||
} catch(e) {
|
||||
PromiseA = global.Promise;
|
||||
}
|
||||
|
||||
var fs = require('fs');
|
||||
var sfs = require('safe-replace').create({ tmp: 'tmp', bak: 'bak' });
|
||||
var path = require('path');
|
||||
var util = require('util');
|
||||
var crypto = require('crypto');
|
||||
|
@ -9,13 +17,206 @@ var jwt = require('jsonwebtoken');
|
|||
var requestAsync = util.promisify(require('@coolaj86/urequest'));
|
||||
var readFileAsync = util.promisify(fs.readFile);
|
||||
var mkdirpAsync = util.promisify(require('mkdirp'));
|
||||
|
||||
var PromiseA;
|
||||
var TRUSTED_ISSUERS = [ 'oauth3.org' ];
|
||||
var DB = {};
|
||||
DB._load = function () {
|
||||
try {
|
||||
PromiseA = require('bluebird');
|
||||
DB._perms = require('./permissions.json');
|
||||
} catch(e) {
|
||||
PromiseA = global.Promise;
|
||||
try {
|
||||
DB._perms = require('./permissions.json.bak');
|
||||
} catch(e) {
|
||||
DB._perms = [];
|
||||
}
|
||||
}
|
||||
DB._byDomain = {};
|
||||
DB._byPort = {};
|
||||
DB._byEmail = {};
|
||||
DB._byPpid = {};
|
||||
DB._byId = {};
|
||||
DB._grants = {};
|
||||
DB._perms.forEach(function (acc) {
|
||||
if (acc.id) {
|
||||
DB._byId[acc.id] = acc;
|
||||
if (!DB._grants[acc.id]) {
|
||||
DB._grants[acc.id] = [];
|
||||
}
|
||||
acc.domains.forEach(function (d) {
|
||||
DB._grants[d.name + '|id|' + acc.id] = true
|
||||
DB._grants[acc.id].push(d);
|
||||
});
|
||||
acc.ports.forEach(function (p) {
|
||||
DB._grants[p.number + '|id|' + acc.id] = true
|
||||
DB._grants[acc.id].push(p);
|
||||
});
|
||||
}
|
||||
acc.nodes.forEach(function (node) {
|
||||
if ('mailto' === node.scheme || 'email' === node.type) {
|
||||
if (!DB._grants[node.email]) {
|
||||
DB._grants[node.email] = [];
|
||||
}
|
||||
acc.domains.forEach(function (d) {
|
||||
DB._grants[d.name + '|' + (node.scheme||node.type) + '|' + node.name] = true
|
||||
DB._grants[node.email].push(d);
|
||||
});
|
||||
acc.ports.forEach(function (d) {
|
||||
DB._grants[d.name + '|' + (node.scheme||node.type) + '|' + node.name] = true
|
||||
DB._grants[node.email].push(p);
|
||||
});
|
||||
DB._byEmail[node.name] = {
|
||||
account: acc
|
||||
, node: node
|
||||
}
|
||||
}
|
||||
});
|
||||
acc.ppids.forEach(function (node) {
|
||||
DB._byPpid[node.name] = {
|
||||
account: acc
|
||||
, node: node
|
||||
}
|
||||
});
|
||||
acc.domains.forEach(function (domain) {
|
||||
if (DB._byDomain[domain.name]) {
|
||||
console.warn("duplicate domain '" + domain.name + "'");
|
||||
console.warn("::existing account '" + acc.nodes.map(function (node) { return node.name; }) + "'");
|
||||
console.warn("::new account '" + DB._byDomain[domain.name].account.nodes.map(function (node) { return node.name; }) + "'");
|
||||
}
|
||||
DB._byDomain[domain.name] = {
|
||||
account: acc
|
||||
, domain: domain
|
||||
};
|
||||
});
|
||||
acc.ports.forEach(function (port) {
|
||||
if (DB._byPort[port.number]) {
|
||||
console.warn("duplicate port '" + domain.number + "'");
|
||||
console.warn("::existing account '" + acc.nodes.map(function (node) { return node.name; }) + "'");
|
||||
console.warn("::new account '" + DB._byPort[port.number].account.nodes.map(function (node) { return node.name; }) + "'");
|
||||
}
|
||||
DB._byPort[domain.name] = {
|
||||
account: acc
|
||||
, port: port
|
||||
};
|
||||
});
|
||||
});
|
||||
};
|
||||
DB._load();
|
||||
DB.accounts = {};
|
||||
DB.accounts.get = function (obj) {
|
||||
return PromiseA.resolve().then(function () {
|
||||
return DB._byId[obj.name] || (DB._byEmail[obj.name] || {}).acc || null;
|
||||
});
|
||||
};
|
||||
DB.accounts.add = function (obj) {
|
||||
return PromiseA.resolve().then(function () {
|
||||
if (obj.id) {
|
||||
// TODO more checks
|
||||
DB._perms.push(obj);
|
||||
} else if (obj.email) {
|
||||
obj.email = undefined;
|
||||
DB._perms.push(obj);
|
||||
}
|
||||
});
|
||||
};
|
||||
DB.domains = {};
|
||||
DB.domains.available = function (name) {
|
||||
return PromiseA.resolve().then(function () {
|
||||
return !DB._byDomain[name];
|
||||
});
|
||||
};
|
||||
DB.domains._add = function (acc, name) {
|
||||
// TODO verifications to change ownership of a domain
|
||||
return PromiseA.resolve().then(function () {
|
||||
var err;
|
||||
//var acc = DB._byId[aid];
|
||||
var domain = {
|
||||
name: name
|
||||
, createdAt: new Date().toISOString()
|
||||
, wildcard: true
|
||||
};
|
||||
var pdomain;
|
||||
var parts = name.split('.').map(function (el, i) {
|
||||
return arr.slice(i).join('.');
|
||||
}).reverse();
|
||||
parts.shift();
|
||||
parts.pop();
|
||||
if (parts.some(function (part) {
|
||||
if (DB._byDomain[part]) {
|
||||
pdomain = part;
|
||||
return true;
|
||||
}
|
||||
})) {
|
||||
err = new Error("'" + name + "' exists as '" + pdomain + "' and therefore requires an admin to review and approve");
|
||||
err.code = "E_REQ_ADMIN";
|
||||
throw err;
|
||||
}
|
||||
if (DB._byDomain[name]) {
|
||||
if (acc !== DB._byDomain[name].account) {
|
||||
throw new Error("domain '" + name + "' exists");
|
||||
}
|
||||
// happily ignore non-change
|
||||
return;
|
||||
}
|
||||
DB._byDomain[name] = {
|
||||
account: acc
|
||||
, domain: domain
|
||||
};
|
||||
acc.domains.push(domain);
|
||||
});
|
||||
};
|
||||
DB.ports = {};
|
||||
DB.ports.available = function (number) {
|
||||
return PromiseA.resolve().then(function () {
|
||||
return !DB._byPort[number];
|
||||
});
|
||||
};
|
||||
DB.ports._add = function (acc, number) {
|
||||
return PromiseA.resolve().then(function () {
|
||||
//var acc = DB._byId[aid];
|
||||
var port = {
|
||||
number: number
|
||||
, createdAt: new Date().toISOString()
|
||||
};
|
||||
if (DB._byPort[number]) {
|
||||
// TODO verifications
|
||||
throw new Error("port '" + number + "' exists");
|
||||
}
|
||||
DB._byPort[number] = {
|
||||
account: acc
|
||||
, domain: domain
|
||||
};
|
||||
acc.domains.push(domain);
|
||||
});
|
||||
};
|
||||
DB._save = function () {
|
||||
return sfs.writeAsync('./accounts.json', JSON.stringify(DB._perms));
|
||||
};
|
||||
DB._saveToken = null;
|
||||
DB._savePromises = [];
|
||||
DB._savePromise = PromiseA.resolve();
|
||||
DB.save = function () {
|
||||
cancelTimeout(DB._saveToken);
|
||||
return new Promise(function (resolve, reject) {
|
||||
function doSave() {
|
||||
DB._savePromise = DB._savePromise.then(function () {
|
||||
return DB._save().then(function (yep) {
|
||||
DB._savePromises.forEach(function (p) {
|
||||
p.resolve(yep);
|
||||
});
|
||||
DB._savePromises.length = 1;
|
||||
}, function (err) {
|
||||
DB._savePromises.forEach(function (p) {
|
||||
p.reject(err);
|
||||
});
|
||||
DB._savePromises.length = 1;
|
||||
});
|
||||
});
|
||||
return DB._savePromise;
|
||||
}
|
||||
|
||||
DB._saveToken = setTimeout(doSave, 2500);
|
||||
DB.savePromises.push({ resolve: resolve, reject: reject });
|
||||
});
|
||||
};
|
||||
|
||||
var _auths = module.exports._auths = {};
|
||||
var Auths = {};
|
||||
|
@ -140,15 +341,22 @@ Accounts.create = function (req) {
|
|||
Accounts.link = function (req) {
|
||||
};
|
||||
*/
|
||||
Accounts.getBySub = function (req) {
|
||||
|
||||
Accounts.getOrCreate = function (req) {
|
||||
var id = Accounts._getTokenId(req.auth);
|
||||
var subpath = Accounts._subPath(req, id);
|
||||
return readFileAsync(path.join(subpath, 'index.json'), 'utf8').then(function (text) {
|
||||
return JSON.parse(text);
|
||||
}, function (/*err*/) {
|
||||
return null;
|
||||
}).then(function (links) {
|
||||
return links || { id: id, sub: req.auth.sub, iss: req.auth.iss, accounts: [] };
|
||||
var idNode = { type: 'ppid', name: id };
|
||||
|
||||
return DB.accounts.get(idNode).then(function (acc) {
|
||||
if (acc) { return _acc; }
|
||||
acc = { id: id, sub: req.auth.sub, iss: req.auth.iss, domains: [], ports: [], nodes: [ idNode ] };
|
||||
return DB.accounts.add(acc).then(function () {
|
||||
// intentionally not returned to the promise chain
|
||||
DB.save().catch(function (err) {
|
||||
console.error('DB.save() failed:');
|
||||
console.error(err);
|
||||
});
|
||||
return acc;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -343,6 +551,7 @@ function oauth3Auth(req, res, next) {
|
|||
, json: true
|
||||
}).then(function (resp) {
|
||||
var jwk = resp.body;
|
||||
console.log('Retrieved token\'s JWK: ', resp.body);
|
||||
if (200 !== resp.statusCode || 'object' !== typeof resp.body) {
|
||||
//headers.authorization
|
||||
res.send({
|
||||
|
@ -362,6 +571,7 @@ function oauth3Auth(req, res, next) {
|
|||
try {
|
||||
pubpem = require('jwk-to-pem')(jwk, { private: false });
|
||||
} catch(e) {
|
||||
console.error("jwk-to-pem", e);
|
||||
pubpem = null;
|
||||
}
|
||||
return verifyJwt(token, pubpem, {
|
||||
|
@ -382,7 +592,7 @@ function oauth3Auth(req, res, next) {
|
|||
next();
|
||||
});
|
||||
});
|
||||
}, function (err) {
|
||||
}).catch(function (err) {
|
||||
res.send({
|
||||
error: {
|
||||
code: err.code || "E_GENERIC"
|
||||
|
@ -391,6 +601,13 @@ function oauth3Auth(req, res, next) {
|
|||
});
|
||||
});
|
||||
}
|
||||
var OAUTH3 = require('oauth3.js').create({ pathname: process.cwd() });
|
||||
/*
|
||||
// TODO all of the above should be replace with the official lib
|
||||
return OAUTH3.jwk.verifyToken(req.auth.jwt).then(function (token) {
|
||||
}).catch(function (err) {
|
||||
});
|
||||
*/
|
||||
|
||||
module.exports.pairRequest = function (opts) {
|
||||
console.log("It's auth'n time!");
|
||||
|
@ -433,6 +650,50 @@ module.exports.pairRequest = function (opts) {
|
|||
return authnData;
|
||||
});
|
||||
};
|
||||
DB.getDomainAndPort = function (state) {
|
||||
var domainCount = 0;
|
||||
var portCount = 0;
|
||||
|
||||
function chooseDomain() {
|
||||
var err;
|
||||
if (domainCount >= 3) {
|
||||
err = new Error("there too few unallocated domains left");
|
||||
err.code = "E_DOMAINS_EXHAUSTED";
|
||||
return PromiseA.reject(err);
|
||||
}
|
||||
domainCount += 1;
|
||||
var hri = require('human-readable-ids').hri;
|
||||
var i = Math.floor(Math.random() * state.config.sharedDomains.length);
|
||||
var hrname = hri.random() + '.' + state.config.sharedDomains[i];
|
||||
return DB.domains.available(hrname).then(function (available) {
|
||||
if (!available) { return chooseDomain(); }
|
||||
return hrname;
|
||||
});
|
||||
}
|
||||
function choosePort() {
|
||||
var err;
|
||||
if (portCount >= 3) {
|
||||
err = new Error("there too few unallocated ports left");
|
||||
err.code = "E_PORTS_EXHAUSTED";
|
||||
return PromiseA.reject(err);
|
||||
}
|
||||
portCount += 1;
|
||||
var portnumber = (1024 + 1) + Math.round(Math.random() * 65535);
|
||||
return DB.ports.available(portnumber).then(function (available) {
|
||||
if (!available) { return portDomain(); }
|
||||
return portnumber;
|
||||
});
|
||||
}
|
||||
return Promise.all([
|
||||
chooseDomain()
|
||||
, choosePort()
|
||||
]).then(function (two) {
|
||||
return {
|
||||
domain: two[0]
|
||||
, port: two[1]
|
||||
};
|
||||
});
|
||||
};
|
||||
module.exports.pairPin = function (opts) {
|
||||
var state = opts.state;
|
||||
return state.Promise.resolve().then(function () {
|
||||
|
@ -455,19 +716,28 @@ module.exports.pairPin = function (opts) {
|
|||
}
|
||||
|
||||
console.log('[pairPin] generating offer');
|
||||
var hri = require('human-readable-ids').hri;
|
||||
var i = Math.floor(Math.random() * state.config.sharedDomains.length);
|
||||
var hrname = hri.random() + '.' + state.config.sharedDomains[i];
|
||||
// TODO check used / unused names and ports
|
||||
return DB.getDomainAndPort(state);
|
||||
}).then(function (grantable) {
|
||||
var emailNode = { scheme: 'mailto', type: 'email', name: auth.subject };
|
||||
|
||||
return DB.accounts.get(emailNode).then(function (_acc) {
|
||||
var acc = _acc;
|
||||
if (!acc) {
|
||||
acc = { email: true, domains: [], ports: [], nodes: [ emailNode ] };
|
||||
}
|
||||
return PromiseA.all([
|
||||
DB.domains._add(acc, opts.domain)
|
||||
, DB.ports._add(acc, opts.port)
|
||||
]).then(function () {
|
||||
var authzData = {
|
||||
id: auth.id
|
||||
, domains: [ hrname ]
|
||||
, ports: [ (1024 + 1) + Math.round(Math.random() * 65535) ]
|
||||
, domains: [ grantable.domain ]
|
||||
, ports: [ grantable.port ]
|
||||
, aud: state.config.webminDomain
|
||||
, iat: Math.round(Date.now() / 1000)
|
||||
// of the client's computer
|
||||
, hostname: auth.hostname
|
||||
};
|
||||
var pathname = path.join(__dirname, 'emails', auth.subject + '.' + hrname + '.data');
|
||||
auth.authz = jwt.sign(authzData, state.secret);
|
||||
auth.authzData = authzData;
|
||||
authzData.jwt = auth.authz;
|
||||
|
@ -478,13 +748,31 @@ module.exports.pairPin = function (opts) {
|
|||
} else {
|
||||
console.log('[pairPin] not resolvable');
|
||||
}
|
||||
|
||||
if (!_acc) {
|
||||
return DB.accounts.add(acc).then(function () {
|
||||
// intentionally not returned to the promise chain
|
||||
DB.save().catch(function (err) {
|
||||
console.error('DB.save() failed:');
|
||||
console.error(err);
|
||||
});
|
||||
return authzData;
|
||||
});
|
||||
} else {
|
||||
return authzData;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
var pathname = path.join(__dirname, 'emails', auth.subject + '.' + hrname + '.data');
|
||||
fs.writeFile(pathname, JSON.stringify(authzData), function (err) {
|
||||
if (err) {
|
||||
console.error('[ERROR] in writing token details');
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
return authzData;
|
||||
*/
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -620,11 +908,81 @@ app.use('/api', CORS({
|
|||
app.use('/api', bodyParser.json());
|
||||
|
||||
app.use('/api/telebit.cloud/account', oauth3Auth);
|
||||
Accounts._associateEmails = function (req) {
|
||||
if (-1 === (req._state.config.trustedIssuers||TRUSTED_ISSUERS).indexOf(req.auth.data.iss)) {
|
||||
// again, make sure that untrusted issuers do not get
|
||||
return null;
|
||||
}
|
||||
|
||||
// oauth3.org, issuer@oauth3.org, profile
|
||||
return OAUTH3.request({
|
||||
url: "https://api." + req.auth.data.iss + "/api/issuer@oauth3.org/acl/profile"
|
||||
, session: { accessToken: req.auth.jwt }
|
||||
}).then(function (resp) {
|
||||
var email;
|
||||
var err;
|
||||
(resp.data.nodes||[]).some(function (node) {
|
||||
// TODO use verified email addresses
|
||||
return true
|
||||
});
|
||||
// back-compat for current way email is stored
|
||||
if (!email && /@/.test(resp.data.username)) {
|
||||
email = resp.data.username;
|
||||
}
|
||||
if (!email) {
|
||||
err = new Error ("could not find a verified email address in profile settings");
|
||||
err.code = "E_NO_EMAIL"
|
||||
return PromiseA.reject(err);
|
||||
}
|
||||
|
||||
return [ { scheme: 'mailto', type: 'email', name: email } ];
|
||||
});
|
||||
};
|
||||
app.get('/api/telebit.cloud/account', function (req, res) {
|
||||
Accounts.getBySub(req).then(function (subData) {
|
||||
res.send(subData);
|
||||
}, function (err) {
|
||||
res.send({
|
||||
return Accounts.getOrCreate(req).then(function (acc) {
|
||||
var hasEmail = subData.nodes.some(function (node) {
|
||||
return 'email' === node.type;
|
||||
});
|
||||
function getAllGrants() {
|
||||
return PromiseA.all(acc.nodes.map(function (node) {
|
||||
return DB.accounts.get(node);
|
||||
})).then(function (grants) {
|
||||
var domainsMap = {};
|
||||
var portsMap = {};
|
||||
var result = JSON.parse(JSON.stringify(acc));
|
||||
result.domains.length = 0;
|
||||
result.ports.length = 0;
|
||||
grants.forEach(function (account) {
|
||||
account.domains.forEach(function (d) {
|
||||
domainsMap[d.name] = d;
|
||||
});
|
||||
account.ports.forEach(function (p) {
|
||||
portsMap[p.number] = p;
|
||||
});
|
||||
});
|
||||
result.domains = Object.keys(domainsMap).map(function (k) {
|
||||
return domainsMap[k];
|
||||
});
|
||||
result.ports = Object.keys(portsMap).map(function (k) {
|
||||
return portsMap[k];
|
||||
});
|
||||
return result;
|
||||
});
|
||||
}
|
||||
if (!hasEmail) {
|
||||
return Accounts._associateEmails(req).then(function (nodes) {
|
||||
nodes.forEach(function (node) {
|
||||
acc.nodes.push(node);
|
||||
});
|
||||
return getAllGrants();
|
||||
});
|
||||
} else {
|
||||
return getAllGrants();
|
||||
}
|
||||
}).then(function (result) {
|
||||
res.send(result);
|
||||
}).catch(function (err) {
|
||||
return res.send({
|
||||
error: {
|
||||
code: err.code || "E_GENERIC"
|
||||
, message: err.toString()
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"name": "telebit.commercial",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "Commercial node.js APIs for Telebit",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"dependencies": {
|
||||
"jwk-to-pem": "^2.0.0",
|
||||
"oauth3.js": "^1.2.5"
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue