diff --git a/lib/extensions/admin/account.html b/lib/extensions/admin/account.html new file mode 100644 index 0000000..94f5f3d --- /dev/null +++ b/lib/extensions/admin/account.html @@ -0,0 +1,90 @@ + + + Telebit Account + + +
+ + +
+ + + + + diff --git a/lib/extensions/admin/index.html b/lib/extensions/admin/index.html index d6b500c..1acb285 100644 --- a/lib/extensions/admin/index.html +++ b/lib/extensions/admin/index.html @@ -29,6 +29,9 @@

Friends enable friends to share anything, access anywhere, connect anytime.

+ Login + Create Account +

Share and Test over HTTPS

@@ -139,74 +142,6 @@ TCP
-
- - -
- - diff --git a/lib/extensions/index.js b/lib/extensions/index.js index 15a671e..5cf5e9b 100644 --- a/lib/extensions/index.js +++ b/lib/extensions/index.js @@ -6,7 +6,9 @@ var util = require('util'); var crypto = require('crypto'); var escapeHtml = require('escape-html'); var jwt = require('jsonwebtoken'); -var requestAsync = util.promisify(require('request')); +var requestAsync = util.promisify(require('@coolaj86/urequest')); +var readFileAsync = util.promisify(fs.readFile); +var mkdirpAsync = util.promisify(require('mkdirp')); var _auths = module.exports._auths = {}; var Auths = {}; @@ -77,6 +79,72 @@ Auths._clean = function () { }); }; +var sfs = require('safe-replace'); +var Accounts = {}; +Accounts._getTokenId = function (auth) { + return auth.data.sub + '@' + (auth.data.iss||'').replace(/\/|\\/g, '-'); +}; +Accounts._accPath = function (req, accId) { + return path.join(req._state.config.accountsDir, 'self', accId); +}; +Accounts._subPath = function (req, id) { + return path.join(req._state.config.accountsDir, 'oauth3', id); +}; +Accounts._setSub = function (req, id, subData) { + var subpath = Accounts._subPath(req, id); + return mkdirpAsync(subpath).then(function () { + return sfs.writeFileAsync(path.join(subpath, 'index.json'), JSON.stringify(subData)); + }); +}; +Accounts._setAcc = function (req, accId, acc) { + var accpath = Accounts._accPath(req, accId); + return mkdirpAsync(accpath).then(function () { + return sfs.writeFileAsync(path.join(accpath, 'index.json'), JSON.stringify(acc)); + }); +}; +Accounts.create = function (req) { + var id = Accounts._getTokenId(req.auth); + var acc = { + sub: crypto.randomBytes(16).toString('hex') + // TODO use something from the request to know which of the domains to use + , iss: req._state.config.webminDomain + , contacts: [] + }; + var accId = Accounts._getTokenId(acc); + acc.id = accId; + + // TODO notify any non-authorized accounts that they've been added? + return Accounts.getBySub(req).then(function (subData) { + subData.accounts.push({ type: 'self', id: accId }); + acc.contacts.push({ type: 'oauth3', id: subData.id, sub: subData.sub, iss: subData.iss }); + return Accounts._setSub(req, id, subData).then(function () { + return Accounts._setAcc(req, accId, acc).then(function () { + return acc; + }); + }); + }); +}; +/* +// TODO an owner of an asset can give permission to another entity +// but that does not mean that that owner has access to that entity's things +// Example: +// A 3rd party login's email verification cannot be trusted for auth +// Only 1st party verifications can be trusted for authorization +Accounts.link = function (req) { +}; +*/ +Accounts.getBySub = 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: [] }; + }); +}; + function sendMail(state, auth) { console.log('[DEBUG] ext auth', auth); /* @@ -137,6 +205,179 @@ function sendMail(state, auth) { }); } +// TODO replace with OAuth3 function +function oauth3Auth(req, res, next) { + var jwt = require('jsonwebtoken'); + var verifyJwt = util.promisify(jwt.verify); + var token = (req.headers.authorization||'').replace(/^bearer /i, ''); + var auth; + var authData; + + if (!token) { + res.send({ + error: { + code: "E_NOAUTH" + , message: "no authorization header" + } + }); + return; + } + + try { + authData = jwt.decode(token, { complete: true }); + } catch(e) { + authData = null; + } + + if (!authData) { + res.send({ + error: { + code: "E_PARSEAUTH" + , message: "could not parse authorization header as JWT" + } + }); + return; + } + + auth = authData.payload; + if (!auth.sub && ('*' === auth.aud || '*' === auth.azp)) { + res.send({ + error: { + code: "E_NOIMPL" + , message: "missing 'sub' and a wildcard 'azp' or 'aud' indicates that this is an exchange token," + + " however, this app has not yet implemented opaque token exchange" + } + }); + return; + } + + if ([ 'sub', 'iss' ].some(function (key) { + if ('string' !== typeof auth[key]) { + res.send({ + error: { + code: "E_PARSEAUTH" + , message: "could not read property '" + key + "' of authorization token" + } + }); + return true; + } + })) { return; } + if ([ 'kid' ].some(function (key) { + if (/\/|\\/.test(authData.header[key])) { + res.send({ + error: { + code: "E_PARSESUBJECT" + , message: "'" + key + "' `" + JSON.stringify(authData.header[key]) + "' contains invalid characters" + } + }); + return true; + } + })) { return; } + if ([ 'sub', 'kid' ].some(function (key) { + if (/\/|\\/.test(auth[key])) { + res.send({ + error: { + code: "E_PARSESUBJECT" + , message: "'" + key + "' `" + JSON.stringify(auth[key]) + "' contains invalid characters" + } + }); + return true; + } + })) { return; } + + // TODO needs to work with app:// and custom:// + function prefixHttps(str) { + return (str||'').replace(/^(https?:\/\/)?/i, 'https://'); + } + + var url = require('url'); + var discoveryUrl = url.resolve(prefixHttps(auth.iss), '_apis/oauth3.org/index.json'); + console.log('discoveryUrl: ', discoveryUrl, auth.iss); + return requestAsync({ + url: discoveryUrl + , json: true + }).then(function (resp) { + + // TODO + // it may be necessary to exchange the token, + + if (200 !== resp.statusCode || 'object' !== typeof resp.body || !resp.body.retrieve_jwk + || 'string' !== typeof resp.body.retrieve_jwk.url || 'string' !== typeof resp.body.api) { + res.send({ + error: { + code: "E_NOTFOUND" + , message: resp.statusCode + ": issuer `" + JSON.stringify(auth.iss) + + "' does not declare 'api' & 'retrieve_key' and hence the token you provided cannot be verified." + , _status: resp.statusCode + , _url: discoveryUrl + , _body: resp.body + } + }); + return; + } + var keyUrl = url.resolve( + prefixHttps(resp.body.api).replace(/:hostname/g, auth.iss) + , resp.body.retrieve_jwk.url + .replace(/:hostname/g, auth.iss) + .replace(/:sub/g, auth.sub) + // TODO + .replace(/:kid/g, authData.header.kid || auth.iss) + ); + console.log('keyUrl: ', keyUrl); + return requestAsync({ + url: keyUrl + , json: true + }).then(function (resp) { + var jwk = resp.body; + if (200 !== resp.statusCode || 'object' !== typeof resp.body) { + //headers.authorization + res.send({ + error: { + code: "E_NOTFOUND" + , message: resp.statusCode + ": did not retrieve public key from `" + JSON.stringify(auth.iss) + + "' for token validation and hence the token you provided cannot be verified." + , _status: resp.statusCode + , _url: keyUrl + , _body: resp.body + } + }); + return; + } + + var pubpem; + try { + pubpem = require('jwk-to-pem')(jwk, { private: false }); + } catch(e) { + pubpem = null; + } + return verifyJwt(token, pubpem, { + algorithms: [ 'RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512' ] + }).then(function (decoded) { + if (!decoded) { + res.send({ + error: { + code: "E_UNVERIFIED" + , message: "retrieved jwk does not verify provided token." + , _jwk: jwk + } + }); + } + req.auth = {}; + req.auth.jwt = token; + req.auth.data = auth; + next(); + }); + }); + }, function (err) { + res.send({ + error: { + code: err.code || "E_GENERIC" + , message: err.toString() + } + }); + }); +} + module.exports.pairRequest = function (opts) { console.log("It's auth'n time!"); var state = opts.state; @@ -357,9 +598,36 @@ var urls = { pairState: '/api/telebit.cloud/pair_state/:id' }; staticApp.use('/', express.static(path.join(__dirname, 'admin'))); -app.use('/api', CORS({})); +app.use('/api', CORS({ + credentials: true +, headers: [ 'Authorization', 'X-Requested-With', 'X-HTTP-Method-Override', 'Content-Type', 'Accept' ] +})); app.use('/api', bodyParser.json()); +app.use('/api/telebit.cloud/account', oauth3Auth); +app.get('/api/telebit.cloud/account', function (req, res) { + Accounts.getBySub(req).then(function (subData) { + res.send(subData); + }, function (err) { + res.send({ + error: { + code: err.code || "E_GENERIC" + , message: err.toString() + } + }); + }); +}); +app.post('/api/telebit.cloud/account', function (req, res) { + return Accounts.create(req).then(function (acc) { + res.send({ + success: true + , id: acc.id + , sub: acc.sub + , iss: acc.iss + }); + }); +}); + // From Device (which knows id, but not secret) app.post('/api/telebit.cloud/pair_request', function (req, res) { var auth = req.body; @@ -383,7 +651,7 @@ app.post('/api/telebit.cloud/pair_request', function (req, res) { 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 crypto = require('crypto'); var response = {}; diff --git a/package.json b/package.json index f3a665d..92d905f 100644 --- a/package.json +++ b/package.json @@ -37,8 +37,7 @@ }, "homepage": "https://git.coolaj86.com/coolaj86/telebit-relay.js", "dependencies": { - "@coolaj86/urequest": "^1.1.1", - "bluebird": "^3.5.1", + "@coolaj86/urequest": "^1.2.1", "body-parser": "^1.18.3", "cluster-store": "^2.0.8", "connect-cors": "^0.5.6", @@ -48,16 +47,22 @@ "greenlock": "^2.2.4", "human-readable-ids": "^1.0.4", "js-yaml": "^3.11.0", - "jsonwebtoken": "^8.2.1", + "jsonwebtoken": "^8.3.0", + "jwk-to-pem": "^2.0.0", + "mkdirp": "^0.5.1", "nowww": "^1.2.1", "proxy-packer": "^1.4.3", "recase": "^1.0.4", "redirect-https": "^1.1.5", "request": "^2.87.0", + "safe-replace": "^1.0.3", "serve-static": "^1.13.2", "sni": "^1.0.0", "ws": "^5.1.1" }, + "trulyOptionalDependencies": { + "bluebird": "^3.5.1" + }, "engineStrict": true, "engines": { "node": "10.2.1"