diff --git a/lib/extensions/admin/account.html b/lib/extensions/admin/account.html
index 1fd078e..a0405a6 100644
--- a/lib/extensions/admin/account.html
+++ b/lib/extensions/admin/account.html
@@ -16,16 +16,25 @@
Account
-
-
+
Claims
+
+ -
+ {{ claim.value }}
+ TXT _claim-challenge.{{ claim.value }}: {{ claim.challenge }}
+
+
+
+
Domains
-
{{ domain }}
diff --git a/lib/extensions/admin/js/account.js b/lib/extensions/admin/js/account.js
index 805d421..a11a38c 100644
--- a/lib/extensions/admin/js/account.js
+++ b/lib/extensions/admin/js/account.js
@@ -1,3 +1,4 @@
+/*global Vue*/
(function () {
'use strict';
var OAUTH3 = window.OAUTH3;
@@ -5,33 +6,7 @@
host: window.location.host
, pathname: window.location.pathname.replace(/\/[^\/]*$/, '/')
});
- var $ = function () { return document.querySelector.apply(document, arguments); }
- var vueData = {
- domains: []
- , newDomain: null
- , newEmail: null
- , hasAccount: false
- , token: null
- };
- var app = new Vue({
- el: '.v-app'
- , data: vueData
- , methods: {
- challengeDns: function () {
- console.log("A new (DNS) challenger!", vueData);
- }
- , challengeEmail: function () {
- console.log("A new (Email) challenger!", vueData);
- }
- }
- });
-
- function listStuff(data) {
- //window.alert("TODO: show authorized devices, domains, and connectivity information");
- vueData.hasAccount = true;
- vueData.domains = data.domains;
- }
-
+ var $ = function () { return document.querySelector.apply(document, arguments); };
var sessionStr = localStorage.getItem('session');
var session;
if (sessionStr) {
@@ -42,6 +17,49 @@
}
}
+ var vueData = {
+ claims: []
+ , domains: []
+ , newDomain: null
+ , newDomainWildcard: false
+ , newEmail: null
+ , hasAccount: false
+ , token: null
+ };
+ var app = new Vue({
+ el: '.v-app'
+ , data: vueData
+ , methods: {
+ challengeDns: function () {
+ return oauth3.request({
+ url: 'https://api.' + location.hostname + '/api/telebit.cloud/account/authorizations/new'
+ , method: 'POST'
+ , session: session
+ , data: { type: 'dns', value: vueData.newDomain, wildcard: vueData.newDomainWildcard }
+ });
+ }
+ , checkDns: function (claim) {
+ return oauth3.request({
+ url: 'https://api.' + location.hostname + '/api/telebit.cloud/account/authorizations/new/:value/:challenge'
+ .replace(/:value/g, claim.value)
+ .replace(/:challenge/g, claim.challenge)
+ , method: 'POST'
+ , session: session
+ });
+ }
+ , challengeEmail: function () {
+ console.log("A new (Email) challenger!", vueData);
+ }
+ }
+ });
+ app = null;
+
+ function listStuff(data) {
+ //window.alert("TODO: show authorized devices, domains, and connectivity information");
+ vueData.hasAccount = true;
+ vueData.domains = data.domains;
+ vueData.claims = data.claims;
+ }
function loadAccount(session) {
return oauth3.request({
url: 'https://api.' + location.hostname + '/api/telebit.cloud/account'
@@ -64,7 +82,7 @@
, method: 'POST'
, session: session
, body: {
- email: email
+ email: vueData.newEmail
}
}).then(function (resp) {
listStuff(resp);
@@ -124,7 +142,7 @@
console.log(resp.data);
localStorage.setItem('session', JSON.stringify(session));
- loadAccount(session)
+ loadAccount(session);
});
});
@@ -138,7 +156,7 @@
$('body form.js-auth-form').addEventListener('submit', onClickLogin);
onChangeProvider('oauth3.org');
if (session) {
- vueData.token = session.access_token
+ vueData.token = session.access_token;
loadAccount(session);
}
}());
diff --git a/lib/extensions/db.js b/lib/extensions/db.js
index de72d8f..40cff60 100644
--- a/lib/extensions/db.js
+++ b/lib/extensions/db.js
@@ -29,7 +29,12 @@ DB._load = function () {
DB._byId = {};
DB._grants = {};
DB._grantsMap = {};
+ DB._authz = {};
DB._perms.forEach(function (acc) {
+ if ('authz' === acc.type) {
+ DB._authz[acc.id] = acc;
+ return;
+ }
if (acc.id) {
// if account has an id
DB._byId[acc.id] = acc;
@@ -129,6 +134,80 @@ DB.accounts.add = function (obj) {
}
});
};
+DB.authorizations = {};
+DB.authorizations.create = function (acc, claim) {
+ if (!acc.id || !claim.type || !claim.value) { throw new Error("requires account id"); }
+ var crypto = require('crypto');
+ var authz = DB._authz[acc.id];
+ if (!authz) {
+ authz = {
+ id: acc.id
+ , type: 'authz'
+ , claims: []
+ };
+ DB._authz[acc.id] = authz;
+ DB._perms.push(authz);
+ }
+ // TODO check for unique type:value pairing in claims
+ claim.challenge = crypto.randomBytes(16).toString('hex');
+ claim.createdAt = Date.now();
+ claim.verifiedAt = 0;
+ authz.claims.push(claim);
+ DB.save();
+ return JSON.parse(JSON.stringify(claim));
+};
+DB.authorizations.check = function (acc, claim) {
+ var authz = DB._authz[acc.id];
+ var vclaim = null;
+ if (!authz) {
+ return vclaim;
+ }
+
+ authz.claims.some(function (c) {
+ console.log('authz.check', c);
+ if (claim.challenge) {
+ if (c.challenge === claim.challenge) {
+ vclaim = JSON.parse(JSON.stringify(c));
+ return true;
+ }
+ } else if (claim.value === c.value) {
+ vclaim = JSON.parse(JSON.stringify(c));
+ return true;
+ }
+ });
+ return vclaim;
+};
+DB.authorizations.checkAll = function (acc) {
+ var authz = DB._authz[acc.id];
+ if (!authz) {
+ return [];
+ }
+
+ return authz.claims.map(function (claim) {
+ return JSON.parse(JSON.stringify(claim));
+ });
+};
+DB.authorizations.verify = function (acc, claim) {
+ var scmp = require('scmp');
+ var authz = DB._authz[acc.id];
+ var vclaim;
+ if (!authz) { return false; }
+
+ authz.claims.some(function (c) {
+ if (scmp(c.challenge, claim.challenge)) {
+ vclaim = c;
+ c.verifiedAt = Date.now();
+ return true;
+ }
+ });
+
+ if (vclaim) {
+ DB.save();
+ return true;
+ }
+
+ return false;
+};
DB.domains = {};
DB.domains.available = function (name) {
return PromiseA.resolve().then(function () {
@@ -154,7 +233,7 @@ DB.domains._add = function (acc, opts) {
parts.shift();
parts.pop();
if (parts.some(function (part) {
- if (DB._byDomain[part]) {
+ if (DB._byDomain[part] && DB._byDomain[part].wildcard) {
pdomain = part;
return true;
}
@@ -175,6 +254,7 @@ DB.domains._add = function (acc, opts) {
, domain: domain
};
acc.domains.push(domain);
+ DB.save();
});
};
DB.ports = {};
diff --git a/lib/extensions/index.js b/lib/extensions/index.js
index a4cfec4..0e27f6e 100644
--- a/lib/extensions/index.js
+++ b/lib/extensions/index.js
@@ -19,6 +19,15 @@ var requestAsync = util.promisify(require('@coolaj86/urequest'));
var mkdirpAsync = util.promisify(require('mkdirp'));
var TRUSTED_ISSUERS = [ 'oauth3.org' ];
var DB = require('./db.js');
+var Claims = {};
+Claims.publicize = function publicizeClaim(claim) {
+ if (!claim) {
+ return null;
+ }
+ var result = { type: claim.type, value: claim.value, verifiedAt: claim.verifiedAt, createdAt: claim.createdAt };
+ if ('dns' === claim.type) { result.challenge = claim.challenge; }
+ return result;
+};
var _auths = module.exports._auths = {};
var Auths = {};
@@ -763,6 +772,7 @@ app.get('/api/telebit.cloud/account', function (req, res) {
//console.log(grants);
var domainsMap = {};
var portsMap = {};
+ var claimsMap = {};
var result = JSON.parse(JSON.stringify(acc));
result.domains.length = 0;
result.ports.length = 0;
@@ -775,6 +785,11 @@ app.get('/api/telebit.cloud/account', function (req, res) {
account.ports.forEach(function (p) {
portsMap[p.number] = p;
});
+ DB.authorizations.checkAll({ id: account.id }).filter(function (claim) {
+ return !claim.verifiedAt;
+ }).forEach(function (claim) {
+ claimsMap[claim.challenge] = claim;
+ });
});
result.domains = Object.keys(domainsMap).map(function (k) {
return domainsMap[k];
@@ -782,6 +797,9 @@ app.get('/api/telebit.cloud/account', function (req, res) {
result.ports = Object.keys(portsMap).map(function (k) {
return portsMap[k];
});
+ result.claims = Object.keys(claimsMap).map(function (k) {
+ return Claims.publicize(claimsMap[k]);
+ });
return result;
});
}
@@ -821,24 +839,121 @@ app.post('/api/telebit.cloud/account', function (req, res) {
// Challenge Nodes / Email, Domains / DNS
app.post('/api/telebit.cloud/account/authorizations/new', function (req, res) {
// Send email via SMTP, confirm client's chosen pin
- res.statusCode = 500;
- res.send({ error: { code: "E_NO_IMPL", message: "not implemented" } });
+ var accId = Accounts._getTokenId(req.auth);
+ var typ = req.body.type;
+ var val = req.body.value;
+ var wild = req.body.wildcard;
+ var claim;
+
+ if ('dns' === typ && /^[a-z0-9\-\.]+.[a-z]+$/i.test(val)) {
+ claim = DB.authorizations.create({ id: accId }, { type: typ, value: val, wildcard: wild });
+ // MUST RETURN PUBLIC VALUES ONLY!
+ // (challenge is public with dns because the verification is internal)
+ res.send({ success: true, claim: claim });
+ } else if ('email' === typ) {
+ //claim = DB.authorizations.create({ type: dns, claim: claim });
+ // MUST RETURN PUBLIC VALUES ONLY!
+ // (challenge is private with email because the verification is external)
+ //claim.challenge = undefined;;
+ // TODO send email
+ res.statusCode = 501;
+ res.send({ error: { code: "E_NO_IMPL", message: "authz '" + typ + "' understood but not implemented" } });
+ } else {
+ res.statusCode = 501;
+ res.send({ error: { code: "E_NO_IMPL", message: "unknown authz type '" + typ + "'" } });
+ }
});
-app.get('/api/telebit.cloud/account/authorizations/status/:id', function (req, res) {
+app.get('/api/telebit.cloud/account/authorizations/status/:value?', function (req, res) {
// For client to check on status
- res.statusCode = 500;
- res.send({ error: { code: "E_NO_IMPL", message: "not implemented" } });
+ var accId = Accounts._getTokenId(req.auth);
+ var val = req.params.value;
+ var result;
+
+ if (val) {
+ result = Claims.publicize(DB.authorizations.check({ id: accId }, { value: val }));
+ // MUST RETURN PUBLIC VALUES ONLY!
+ res.send({ success: true, claim: result });
+ } else {
+ result = DB.authorizations.checkAll({ id: accId }).map(Claims.publicize);
+ // MUST RETURN PUBLIC VALUES ONLY!
+ res.send({ success: true, claims: result });
+ }
});
app.get('/api/telebit.cloud/account/authorizations/meta/:secret', function (req, res) {
// For agent to retrieve metadata
res.statusCode = 500;
res.send({ error: { code: "E_NO_IMPL", message: "not implemented" } });
});
-app.post('/api/telebit.cloud/account/authorizations/new/:magic/:pin', function (req, res) {
+app.post('/api/telebit.cloud/account/authorizations/verify/:magic/:pin', function (req, res) {
// For agent to confirm user's intent
res.statusCode = 500;
res.send({ error: { code: "E_NO_IMPL", message: "not implemented" } });
});
+app.post('/api/telebit.cloud/account/authorizations/new/:value/:challenge?', function (req, res) {
+ // For agent to confirm user's intent
+ var dns = require('dns');
+ var accId = Accounts._getTokenId(req.auth);
+ var val = req.params.value;
+ var ch = req.params.challenge;
+ var claim = DB.authorizations.check({ id: accId }, { challenge: ch, value: val });
+
+ function notFound() {
+ res.send({ error: {
+ code: "E_PENDING"
+ , message: "Did not find '" + claim.challenge + "' among records at '_claim-challenge." + claim.value + "'"
+ } });
+ }
+
+ function grantDnsClaim() {
+ return Accounts.getOrCreate(req).then(function (acc) {
+ return DB.domains._add(acc, { domain: claim.value, wildcard: claim.wildcard }).then(function (result) {
+ if (!DB.authorizations.verify({ id: accId }, claim)) {
+ var err = new Error("'_claim-challenge." + claim.value + "' matched, but final verification failed");
+ err.code = "E_UNKNOWN";
+ return PromiseA.reject(err);
+ }
+ return result;
+ });
+ });
+ }
+
+ function checkDns() {
+ dns.resolveTxt('_claim-challenge.' + claim.value, function (err, records) {
+ if (err) {
+ notFound();
+ return;
+ }
+
+ if (!records.some(function (txts) {
+ return txts.some(function (txt) {
+ console.log('TXT', txt);
+ return claim.challenge === txt;
+ });
+ })) {
+ notFound();
+ return;
+ }
+
+ grantDnsClaim().then(function () {
+ res.send({ success: true });
+ }).catch(function (err) {
+ res.send({ error: { code: err.code, message: err.toString(), _stack: err.stack } });
+ });
+ });
+ }
+
+ console.log('claim', claim);
+
+ if ('dns' === claim.type) {
+ checkDns();
+ } else if ('email' === claim.type) {
+ res.statusCode = 500;
+ res.send({ error: { code: "E_NO_IMPL", message: "'" + claim.type + "' not implemented yet" } });
+ } else {
+ res.statusCode = 500;
+ res.send({ error: { code: "E_NO_IMPL", message: "'" + claim.type + "' not understood" } });
+ }
+});
// From Device (which knows id, but not secret)
diff --git a/lib/extensions/package.json b/lib/extensions/package.json
index 6c43a8d..7921cae 100644
--- a/lib/extensions/package.json
+++ b/lib/extensions/package.json
@@ -11,6 +11,7 @@
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"jwk-to-pem": "^2.0.0",
- "oauth3.js": "^1.2.5"
+ "oauth3.js": "^1.2.5",
+ "scmp": "^1.0.2"
}
}