handle accounts

This commit is contained in:
AJ ONeal 2018-07-07 09:45:33 +00:00
parent c045e4c712
commit 4e459ea617
4 changed files with 372 additions and 74 deletions

View File

@ -0,0 +1,90 @@
<html>
<head>
<title>Telebit Account</title>
</head>
<body>
<form class="js-auth-form">
<input class="js-auth-subject" type="email"/>
<button class="js-auth-submit" type="submit">Login</button>
</form>
<script src="assets/oauth3.org/oauth3.core.js"></script>
<script>
(function () {
'use strict';
var OAUTH3 = window.OAUTH3;
var oauth3 = OAUTH3.create({
host: window.location.host
, pathname: window.location.pathname.replace(/\/[^\/]*$/, '/')
});
var $ = function () { return document.querySelector.apply(document, arguments); }
function onChangeProvider(providerUri) {
// example https://oauth3.org
return oauth3.setIdentityProvider(providerUri);
}
// This opens up the login window for the specified provider
//
function onClickLogin(ev) {
ev.preventDefault();
ev.stopPropagation();
// TODO check subject for provider viability
return oauth3.authenticate({
subject: $('.js-auth-subject').value
}).then(function (session) {
console.info('Authentication was Successful:');
console.log(session);
// You can use the PPID (or preferably a hash of it) as the login for your app
// (it securely functions as both username and password which is known only by your app)
// If you use a hash of it as an ID, you can also use the PPID itself as a decryption key
//
console.info('Secure PPID (aka subject):', session.token.sub);
return oauth3.request({
url: 'https://api.oauth3.org/api/issuer@oauth3.org/jwks/:sub/:kid.json'
.replace(/:sub/g, session.token.sub)
.replace(/:kid/g, session.token.iss)
, session: session
}).then(function (resp) {
console.info("Public Key:");
console.log(resp.data);
return oauth3.request({
url: 'https://api.oauth3.org/api/issuer@oauth3.org/acl/profile'
, session: session
}).then(function (resp) {
console.info("Inspect Token:");
console.log(resp.data);
return oauth3.request({
url: 'https://api.telebit.cloud/api/telebit.cloud/account'
, session: session
}).then(function (resp) {
console.info("Telebit Account:");
console.log(resp.data);
});
});
});
}, function (err) {
console.error('Authentication Failed:');
console.log(err);
});
}
$('body form.js-auth-form').addEventListener('submit', onClickLogin);
onChangeProvider('oauth3.org');
}());
</script>
</body>
</html>

View File

@ -29,6 +29,9 @@
<p>Friends enable friends to share anything, access anywhere, connect anytime.</p> <p>Friends enable friends to share anything, access anywhere, connect anytime.</p>
</center> </center>
<a href="account.html#/login">Login</a>
<a href="account.html#/create_account">Create Account</a>
<div style="width: 800px; margin: auto;"> <div style="width: 800px; margin: auto;">
<div> <div>
<h2>Share and Test over HTTPS</h2> <h2>Share and Test over HTTPS</h2>
@ -139,74 +142,6 @@ TCP
</div> </div>
<form class="js-auth-form">
<input class="js-auth-subject" type="email"/>
<button class="js-auth-submit" type="submit">Login</button>
</form>
<script src="assets/oauth3.org/oauth3.core.js"></script>
<script>
(function () {
'use strict';
var OAUTH3 = window.OAUTH3;
var oauth3 = OAUTH3.create(window.location);
var $ = function () { return document.querySelector.apply(document, arguments); }
function onChangeProvider(providerUri) {
// example https://oauth3.org
return oauth3.setIdentityProvider(providerUri);
}
// This opens up the login window for the specified provider
//
function onClickLogin(ev) {
ev.preventDefault();
ev.stopPropagation();
// TODO check subject for provider viability
return oauth3.authenticate({
subject: $('.js-auth-subject').value
}).then(function (session) {
console.info('Authentication was Successful:');
console.log(session);
// You can use the PPID (or preferably a hash of it) as the login for your app
// (it securely functions as both username and password which is known only by your app)
// If you use a hash of it as an ID, you can also use the PPID itself as a decryption key
//
console.info('Secure PPID (aka subject):', session.token.sub);
return oauth3.request({
url: 'https://api.oauth3.org/api/issuer@oauth3.org/jwks/:sub/:kid.json'
.replace(/:sub/g, session.token.sub)
.replace(/:kid/g, session.token.iss)
, session: session
}).then(function (resp) {
console.info("Public Key:");
console.log(resp.data);
return oauth3.request({
url: 'https://api.oauth3.org/api/issuer@oauth3.org/acl/profile'
, session: session
}).then(function (resp) {
console.info("Inspect Token:");
console.log(resp.data);
});
});
}, function (err) {
console.error('Authentication Failed:');
console.log(err);
});
}
$('body form.js-auth-form').addEventListener('submit', onClickLogin);
onChangeProvider('oauth3.org');
}());
</script>
<script src="js/app.js"></script> <script src="js/app.js"></script>
</body> </body>
</html> </html>

View File

@ -6,7 +6,9 @@ var util = require('util');
var crypto = require('crypto'); var crypto = require('crypto');
var escapeHtml = require('escape-html'); var escapeHtml = require('escape-html');
var jwt = require('jsonwebtoken'); 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 = module.exports._auths = {};
var 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) { function sendMail(state, auth) {
console.log('[DEBUG] ext auth', 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) { 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;
@ -357,9 +598,36 @@ var urls = {
pairState: '/api/telebit.cloud/pair_state/:id' pairState: '/api/telebit.cloud/pair_state/:id'
}; };
staticApp.use('/', express.static(path.join(__dirname, 'admin'))); 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', 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) // 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;
@ -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) { app.get('/api/telebit.cloud/pair_request/:secret', function (req, res) {
var secret = req.params.secret; var secret = req.params.secret;
var auth = Auths.getBySecret(secret); var auth = Auths.getBySecret(secret);
var crypto = require('crypto'); //var crypto = require('crypto');
var response = {}; var response = {};

View File

@ -37,8 +37,7 @@
}, },
"homepage": "https://git.coolaj86.com/coolaj86/telebit-relay.js", "homepage": "https://git.coolaj86.com/coolaj86/telebit-relay.js",
"dependencies": { "dependencies": {
"@coolaj86/urequest": "^1.1.1", "@coolaj86/urequest": "^1.2.1",
"bluebird": "^3.5.1",
"body-parser": "^1.18.3", "body-parser": "^1.18.3",
"cluster-store": "^2.0.8", "cluster-store": "^2.0.8",
"connect-cors": "^0.5.6", "connect-cors": "^0.5.6",
@ -48,16 +47,22 @@
"greenlock": "^2.2.4", "greenlock": "^2.2.4",
"human-readable-ids": "^1.0.4", "human-readable-ids": "^1.0.4",
"js-yaml": "^3.11.0", "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", "nowww": "^1.2.1",
"proxy-packer": "^1.4.3", "proxy-packer": "^1.4.3",
"recase": "^1.0.4", "recase": "^1.0.4",
"redirect-https": "^1.1.5", "redirect-https": "^1.1.5",
"request": "^2.87.0", "request": "^2.87.0",
"safe-replace": "^1.0.3",
"serve-static": "^1.13.2", "serve-static": "^1.13.2",
"sni": "^1.0.0", "sni": "^1.0.0",
"ws": "^5.1.1" "ws": "^5.1.1"
}, },
"trulyOptionalDependencies": {
"bluebird": "^3.5.1"
},
"engineStrict": true, "engineStrict": true,
"engines": { "engines": {
"node": "10.2.1" "node": "10.2.1"