(still) half-baked oauth3 layer
This commit is contained in:
parent
2b432cb36d
commit
75b9a0e2b3
|
@ -1,9 +1,155 @@
|
|||
'use strict';
|
||||
|
||||
var PromiseA = require('bluebird');
|
||||
var scoper = require('app-scoped-ids');
|
||||
|
||||
module.exports.inject = function (app) {
|
||||
module.exports.inject = function (conf, app, pkgConf, pkgDeps) {
|
||||
var scoper = require('app-scoped-ids');
|
||||
var inProcessCache = {};
|
||||
var createClientFactory = require('sqlite3-cluster/client').createClientFactory;
|
||||
var dir = [
|
||||
{ tablename: 'codes'
|
||||
, idname: 'uuid'
|
||||
, indices: ['createdAt']
|
||||
}
|
||||
, { tablename: 'logins' // coolaj86, coolaj86@gmail.com, +1-317-426-6525
|
||||
, idname: 'hashId'
|
||||
//, relations: [{ tablename: 'secrets', id: 'hashid', fk: 'loginId' }]
|
||||
, indices: ['createdAt', 'type', 'node']
|
||||
//, immutable: false
|
||||
}
|
||||
, { tablename: 'verifications'
|
||||
, idname: 'hashId' // hash(date + node)
|
||||
//, relations: [{ tablename: 'secrets', id: 'hashid', fk: 'loginId' }]
|
||||
, indices: ['createdAt', 'nodeId']
|
||||
//, immutable: true
|
||||
}
|
||||
, { tablename: 'secrets'
|
||||
, idname: 'hashId' // hash(node + secret)
|
||||
, indices: ['createdAt']
|
||||
//, immutable: true
|
||||
}
|
||||
, { tablename: 'recoveryNodes' // just for 1st-party logins
|
||||
, idname: 'hashId' //
|
||||
// TODO how transmit that something should be deleted / disabled?
|
||||
, indices: ['createdAt', 'updatedAt', 'loginHash', 'recoveryNode', 'deleted']
|
||||
}
|
||||
|
||||
//
|
||||
// Accounts
|
||||
//
|
||||
, { tablename: 'accounts_logins'
|
||||
, idname: 'id' // hash(accountId + loginId)
|
||||
, indices: ['createdAt', 'revokedAt', 'loginId', 'accountId']
|
||||
}
|
||||
, { tablename: 'accounts'
|
||||
, idname: 'id' // crypto random id? or hash(name) ?
|
||||
, unique: ['name']
|
||||
, indices: ['createdAt', 'updatedAt', 'deletedAt', 'name', 'displayName']
|
||||
}
|
||||
|
||||
//
|
||||
// OAuth3
|
||||
//
|
||||
, { tablename: 'private_key'
|
||||
, idname: 'id'
|
||||
, indices: ['createdAt']
|
||||
}
|
||||
, { tablename: 'oauth_clients'
|
||||
, idname: 'id'
|
||||
, indices: ['createdAt', 'updatedAt', 'accountId']
|
||||
, hasMany: ['apiKeys'] // TODO
|
||||
, belongsTo: ['account']
|
||||
, schema: function () {
|
||||
return {
|
||||
test: true
|
||||
, insecure: true
|
||||
};
|
||||
}
|
||||
}
|
||||
, { tablename: 'api_keys'
|
||||
, idname: 'id'
|
||||
, indices: ['createdAt', 'updatedAt', 'oauthClientId']
|
||||
, belongsTo: ['oauthClient'] // TODO pluralization
|
||||
, schema: function () {
|
||||
return {
|
||||
test: true
|
||||
, insecure: true
|
||||
};
|
||||
}
|
||||
}
|
||||
, { tablename: 'tokens' // note that a token functions as a session
|
||||
, idname: 'id'
|
||||
, indices: ['createdAt', 'updatedAt', 'expiresAt', 'revokedAt', 'oauthClientId', 'loginId', 'accountId']
|
||||
}
|
||||
, { tablename: 'grants'
|
||||
, idname: 'id' // sha256(scope + oauthClientId + (accountId || loginId))
|
||||
, indices: ['createdAt', 'updatedAt', 'oauthClientId', 'loginId', 'accountId']
|
||||
}
|
||||
];
|
||||
|
||||
function getAppScopedControllers(experienceId) {
|
||||
if (inProcessCache[experienceId]) {
|
||||
return PromiseA.resolve(inProcessCache[experienceId]);
|
||||
}
|
||||
|
||||
var mq = require('masterquest');
|
||||
var path = require('path');
|
||||
// TODO how can we encrypt this?
|
||||
var systemFactory = createClientFactory({
|
||||
// TODO only complain if the values are different
|
||||
algorithm: 'aes'
|
||||
, bits: 128
|
||||
, mode: 'cbc'
|
||||
, dirname: path.join(__dirname, '..', '..', 'var') // TODO info.conf
|
||||
//, prefix: appname.replace(/\//g, ':') // 'com.example.'
|
||||
//, dbname: 'cluster'
|
||||
, suffix: ''
|
||||
, ext: '.sqlcipher'
|
||||
, sock: conf.sqlite3Sock
|
||||
, ipcKey: conf.ipcKey
|
||||
});
|
||||
var clientFactory = createClientFactory({
|
||||
// TODO only complain if the values are different
|
||||
dirname: path.join(__dirname, '..', '..', 'var') // TODO info.conf
|
||||
, prefix: 'com.oauth3' // 'com.example.'
|
||||
//, dbname: 'config'
|
||||
, suffix: ''
|
||||
, ext: '.sqlite3'
|
||||
, sock: conf.sqlite3Sock
|
||||
, ipcKey: conf.ipcKey
|
||||
});
|
||||
|
||||
inProcessCache[experienceId] = systemFactory.create({
|
||||
init: true
|
||||
//, key: '00000000000000000000000000000000'
|
||||
, dbname: experienceId // 'com.example.'
|
||||
}).then(function (sqlStore) {
|
||||
//var db = factory.
|
||||
return mq.wrap(sqlStore, dir).then(function (models) {
|
||||
return require('./oauthclient-microservice/lib/sign-token').create(models.PrivateKey).init().then(function (signer) {
|
||||
var CodesCtrl = require('authcodes').create(models.Codes);
|
||||
/* models = { Logins, Verifications } */
|
||||
var LoginsCtrl = require('./authentication-microservice/lib/logins').create({}, CodesCtrl, models);
|
||||
/* models = { ApiKeys, OauthClients } */
|
||||
var ClientsCtrl = require('./oauthclient-microservice/lib/oauthclients').createController({}, models, signer);
|
||||
|
||||
return {
|
||||
Codes: CodesCtrl
|
||||
, Logins: LoginsCtrl
|
||||
, Clients: ClientsCtrl
|
||||
, SqlFactory: clientFactory
|
||||
, models: models
|
||||
};
|
||||
});
|
||||
});
|
||||
}).then(function (ctrls) {
|
||||
inProcessCache[experienceId] = ctrls;
|
||||
return ctrls;
|
||||
});
|
||||
|
||||
return inProcessCache[experienceId];
|
||||
}
|
||||
|
||||
//var jwsUtils = require('./lib/jws-utils').create(signer);
|
||||
var CORS = require('connect-cors');
|
||||
var cors = CORS({ credentials: true, headers: [
|
||||
|
@ -67,38 +213,7 @@ module.exports.inject = function (app) {
|
|||
return PromiseA.resolve(token);
|
||||
}
|
||||
|
||||
function getToken(req, res, next) {
|
||||
req.oauth3 = {};
|
||||
|
||||
parseAccessToken(req).then(function (token) {
|
||||
if (!token) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
var jwt = require('jsonwebtoken');
|
||||
var data = jwt.decode(token);
|
||||
var err;
|
||||
|
||||
if (!data) {
|
||||
err = new Error('not a json web token');
|
||||
err.code = 'E_NOT_JWT';
|
||||
res.send({
|
||||
error: err.code
|
||||
, error_description: err.message
|
||||
, error_url: 'https://oauth3.org/docs/errors#' + (err.code || 'E_UNKNOWN_EXCEPTION')
|
||||
});
|
||||
// PromiseA.reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
req.oauth3.token = token;
|
||||
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
function getClient(req, token, priv) {
|
||||
function getClient(req, token, priv, Controllers) {
|
||||
if (!token) {
|
||||
token = req.oauth3.token;
|
||||
}
|
||||
|
@ -110,7 +225,7 @@ module.exports.inject = function (app) {
|
|||
}
|
||||
|
||||
// TODO could get client directly by token.app (id of client)
|
||||
priv[cacheId] = ClientsCtrl.login(null, token.k).then(function (apiKey) {
|
||||
priv[cacheId] = Controllers.Clients.login(null, token.k).then(function (apiKey) {
|
||||
if (!apiKey) {
|
||||
return PromiseA.reject(new Error("Client no longer valid"));
|
||||
}
|
||||
|
@ -124,7 +239,60 @@ module.exports.inject = function (app) {
|
|||
return priv[cacheId];
|
||||
}
|
||||
|
||||
function getLoginId(req, token, priv) {
|
||||
function getAccountsByLogin(req, token, priv, Controllers, loginId, decrypt) {
|
||||
return getClient(req, req.oauth.token, priv).then(function (oauthClient) {
|
||||
if (decrypt) {
|
||||
loginId = scoper.unscope(loginId, oauthClient.secret);
|
||||
}
|
||||
|
||||
return Controllers.models.AccountsLogins.find({ loginId: loginId }).then(function (accounts) {
|
||||
return PromiseA.all(accounts.map(function (obj) {
|
||||
return Controllers.models.Accounts.get(obj.accountId)/*.then(function (account) {
|
||||
account.appScopedId = weakCipher(oauthClient.secret, account.id);
|
||||
return account;
|
||||
})*/;
|
||||
}));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getAccountsByArray(req, Controllers, arr) {
|
||||
return PromiseA.all(arr.map(function (accountId) {
|
||||
return Controllers.models.Accounts.get(accountId.id || accountId);
|
||||
}));
|
||||
}
|
||||
|
||||
function getAccounts(req, token, priv, Controllers) {
|
||||
if (!token) {
|
||||
token = req.oauth3.token;
|
||||
}
|
||||
|
||||
var err;
|
||||
|
||||
if (priv._accounts) {
|
||||
return PromiseA.resolve(priv._accounts);
|
||||
}
|
||||
|
||||
if ((req.oauth3.token.idx || req.oauth3.token.usr) && ('password' === req.oauth3.token.grt || 'login' === req.oauth3.token.as)) {
|
||||
priv._accounts = getAccountsByLogin(req, req.oauth3.token, priv, Controllers, (req.oauth3.token.idx || req.oauth3.token.usr), !!req.oauth3.token.idx);
|
||||
} else if (req.oauth3.token.axs && req.oauth3.token.axs.length || req.oauth3.token.acx) {
|
||||
req.oauth3._accounts = getAccountsByArray(req, Controllers, req.oauth3.token.axs && req.oauth3.token.axs.length && req.oauth3.token.axs || [req.oauth3.token.acx]);
|
||||
} else {
|
||||
err = new Error("neither login nor accounts were specified");
|
||||
err.code = "E_NO_AUTHZ";
|
||||
req.oauth3._accounts = PromiseA.reject(err);
|
||||
}
|
||||
|
||||
req.oauth3._accounts.then(function (accounts) {
|
||||
req.oauth3._accounts = accounts;
|
||||
|
||||
return accounts;
|
||||
});
|
||||
|
||||
return req.oauth3._accounts;
|
||||
}
|
||||
|
||||
function getLoginId(req, token, priv/*, Controllers*/) {
|
||||
if (!token) {
|
||||
token = req.oauth3.token;
|
||||
}
|
||||
|
@ -157,7 +325,7 @@ module.exports.inject = function (app) {
|
|||
return priv[cacheId];
|
||||
}
|
||||
|
||||
function getLogin(req, token, priv) {
|
||||
function getLogin(req, token, priv, Controllers) {
|
||||
if (!token) {
|
||||
token = req.oauth3.token;
|
||||
}
|
||||
|
@ -169,7 +337,8 @@ module.exports.inject = function (app) {
|
|||
}
|
||||
|
||||
priv[cacheId] = getLoginId(req, token, priv).then(function (loginId) {
|
||||
return LoginsCtrl.rawGet(loginId).then(function (login) {
|
||||
// DB.Logins.get(hashId)
|
||||
return Controllers.Logins.rawGet(loginId).then(function (login) {
|
||||
priv[cacheId] = login;
|
||||
|
||||
return login;
|
||||
|
@ -179,86 +348,60 @@ module.exports.inject = function (app) {
|
|||
return priv[cacheId];
|
||||
}
|
||||
|
||||
function getAccountsByLogin(req, token, priv, loginId, decrypt) {
|
||||
return getClient(req, req.oauth.token, priv).then(function (oauthClient) {
|
||||
if (decrypt) {
|
||||
loginId = scoper.unscope(loginId, oauthClient.secret);
|
||||
}
|
||||
function attachOauth3(req, res, next) {
|
||||
var privs = {};
|
||||
req.oauth3 = {};
|
||||
|
||||
return Db.AccountsLogins.find({ loginId: loginId }).then(function (accounts) {
|
||||
return PromiseA.all(accounts.map(function (obj) {
|
||||
return Db.Accounts.get(obj.accountId)/*.then(function (account) {
|
||||
account.appScopedId = weakCipher(oauthClient.secret, account.id);
|
||||
return account;
|
||||
})*/;
|
||||
}));
|
||||
getAppScopedControllers(req.experienceId).then(function (Controllers) {
|
||||
|
||||
return parseAccessToken(req).then(function (token) {
|
||||
if (!token) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
var jwt = require('jsonwebtoken');
|
||||
var data = jwt.decode(token);
|
||||
var err;
|
||||
|
||||
if (!data) {
|
||||
err = new Error('not a json web token');
|
||||
err.code = 'E_NOT_JWT';
|
||||
res.send({
|
||||
error: err.code
|
||||
, error_description: err.message
|
||||
, error_url: 'https://oauth3.org/docs/errors#' + (err.code || 'E_UNKNOWN_EXCEPTION')
|
||||
});
|
||||
// PromiseA.reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
req.oauth3.token = token;
|
||||
|
||||
req.oauth3.getLoginId = function (token) {
|
||||
getLoginId(req, token || req.oauth3.token, privs, Controllers);
|
||||
};
|
||||
|
||||
req.oauth3.getLogin = function (token) {
|
||||
getLogin(req, token || req.oauth3.token, privs, Controllers);
|
||||
};
|
||||
|
||||
// TODO modify prototypes?
|
||||
req.oauth3.getClient = function (token) {
|
||||
getClient(req, token || req.oauth3.token, privs, Controllers);
|
||||
};
|
||||
|
||||
// TODO req.oauth3.getAccountIds
|
||||
req.oauth3.getAccounts = function (token) {
|
||||
getAccounts(req, token || req.oauth3.token, privs, Controllers);
|
||||
};
|
||||
|
||||
next();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getAccountsByArray(req, arr) {
|
||||
return PromiseA.all(arr.map(function (accountId) {
|
||||
return Db.Accounts.get(accountId.id || accountId);
|
||||
}));
|
||||
}
|
||||
|
||||
function getAccounts(req, token, priv) {
|
||||
if (!token) {
|
||||
token = req.oauth3.token;
|
||||
}
|
||||
|
||||
var err;
|
||||
|
||||
if (priv._accounts) {
|
||||
return PromiseA.resolve(priv._accounts);
|
||||
}
|
||||
|
||||
if ((req.oauth3.token.idx || req.oauth3.token.usr) && ('password' === req.oauth3.token.grt || 'login' === req.oauth3.token.as)) {
|
||||
priv._accounts = getAccountsByLogin(req, req.oauth3.token, priv, (req.oauth3.token.idx || req.oauth3.token.usr), !!req.oauth3.token.idx);
|
||||
} else if (req.oauth3.token.axs && req.oauth3.token.axs.length || req.oauth3.token.acx) {
|
||||
req.oauth3._accounts = getAccountsByArray(req, req.oauth3.token.axs && req.oauth3.token.axs.length && req.oauth3.token.axs || [req.oauth3.token.acx]);
|
||||
} else {
|
||||
err = new Error("neither login nor accounts were specified");
|
||||
err.code = "E_NO_AUTHZ";
|
||||
req.oauth3._accounts = PromiseA.reject(err);
|
||||
}
|
||||
|
||||
req.oauth3._accounts.then(function (accounts) {
|
||||
req.oauth3._accounts = accounts;
|
||||
|
||||
return accounts;
|
||||
});
|
||||
|
||||
return req.oauth3._accounts;
|
||||
}
|
||||
|
||||
function promiseCredentials(req, res, next) {
|
||||
var privs = {};
|
||||
|
||||
// TODO modify prototypes?
|
||||
req.oauth3.getClient = function (token) {
|
||||
getClient(req, token || req.oauth3.token, privs);
|
||||
};
|
||||
|
||||
req.oauth3.getLoginId = function (token) {
|
||||
getLoginId(req, token || req.oauth3.token, privs);
|
||||
};
|
||||
|
||||
req.oauth3.getLogin = function (token) {
|
||||
getLogin(req, token || req.oauth3.token, privs);
|
||||
};
|
||||
|
||||
// TODO req.oauth3.getAccountIds
|
||||
req.oauth3.getAccounts = function (token) {
|
||||
getAccounts(req, token || req.oauth3.token, privs);
|
||||
};
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
app.use('/', cors);
|
||||
|
||||
app.use('/', getToken);
|
||||
|
||||
app.use('/', promiseCredentials);
|
||||
app.use('/', attachOauth3);
|
||||
};
|
||||
|
|
|
@ -103,7 +103,7 @@ function loadPages(pkgConf, packagedPage, req, res, next) {
|
|||
handlePromise(packagedPage._promise_page);
|
||||
}
|
||||
|
||||
function getApi(pkgConf, pkgDeps, packagedApi) {
|
||||
function getApi(conf, pkgConf, pkgDeps, packagedApi) {
|
||||
var PromiseA = require('bluebird');
|
||||
var path = require('path');
|
||||
var pkgpath = path.join(pkgConf.apipath, packagedApi.id/*, (packagedApi.api.version || '')*/);
|
||||
|
@ -155,7 +155,7 @@ function getApi(pkgConf, pkgDeps, packagedApi) {
|
|||
packagedApi._api = require('express-lazy')();
|
||||
packagedApi._api_app = myApp;
|
||||
|
||||
require('./oauth3-auth').inject(packagedApi._api, pkgConf, pkgDeps);
|
||||
require('./oauth3-auth').inject(conf, packagedApi._api, pkgConf, pkgDeps);
|
||||
|
||||
// DEBUG
|
||||
//
|
||||
|
@ -194,7 +194,7 @@ function getApi(pkgConf, pkgDeps, packagedApi) {
|
|||
});
|
||||
}
|
||||
|
||||
function loadApi(pkgConf, pkgDeps, packagedApi) {
|
||||
function loadApi(conf, pkgConf, pkgDeps, packagedApi) {
|
||||
function handlePromise(p) {
|
||||
return p.then(function (api) {
|
||||
packagedApi._api = api;
|
||||
|
@ -203,7 +203,7 @@ function loadApi(pkgConf, pkgDeps, packagedApi) {
|
|||
}
|
||||
|
||||
if (!packagedApi._promise_api) {
|
||||
packagedApi._promise_api = getApi(pkgConf, pkgDeps, packagedApi);
|
||||
packagedApi._promise_api = getApi(conf, pkgConf, pkgDeps, packagedApi);
|
||||
}
|
||||
|
||||
return handlePromise(packagedApi._promise_api);
|
||||
|
@ -274,13 +274,35 @@ function runApi(opts, router, req, res, next) {
|
|||
next();
|
||||
return;
|
||||
}
|
||||
// appId means hash(api.id + host + path) - also called "experience"
|
||||
Object.defineProperty(req, 'appId', {
|
||||
|
||||
// Reaching this point means that there are APIs for this pathname
|
||||
// it is important to identify this host + pathname (example.com/foo) as the app
|
||||
Object.defineProperty(req, 'experienceId', {
|
||||
enumerable: true
|
||||
, configurable: false
|
||||
, writable: false
|
||||
// TODO this identifier may need to be non-deterministic as to transfer if a domain name changes but is still the "same" app
|
||||
// (i.e. a company name change. maybe auto vs manual register - just like oauth3?)
|
||||
// NOTE: probably best to alias the name logically
|
||||
, value: (req.hostname + req.pathname).replace(/\/$/, '')
|
||||
});
|
||||
Object.defineProperty(req, 'escapedExperienceId', {
|
||||
enumerable: true
|
||||
, configurable: false
|
||||
, writable: false
|
||||
// TODO this identifier may need to be non-deterministic as to transfer if a domain name changes but is still the "same" app
|
||||
// (i.e. a company name change. maybe auto vs manual register - just like oauth3?)
|
||||
// NOTE: probably best to alias the name logically
|
||||
, value: req.experienceId.replace(/\//g, ':')
|
||||
});
|
||||
// packageId should mean hash(api.id + host + path) - also called "api"
|
||||
Object.defineProperty(req, 'packageId', {
|
||||
enumerable: true
|
||||
, configurable: false
|
||||
, writable: false
|
||||
// TODO this identifier may need to be non-deterministic as to transfer if a domain name changes but is still the "same" app
|
||||
// (i.e. a company name change. maybe auto vs manual register - just like oauth3?)
|
||||
// NOTE: probably best to alias the name logically
|
||||
, value: packagedApi.domain.id
|
||||
});
|
||||
Object.defineProperty(req, 'appConfig', {
|
||||
|
@ -312,7 +334,7 @@ function runApi(opts, router, req, res, next) {
|
|||
}
|
||||
|
||||
// console.log("[DEBUG pkgpath]", pkgConf.apipath, packagedApi.id);
|
||||
loadApi(pkgConf, pkgDeps, packagedApi).then(function (api) {
|
||||
loadApi(opts.conf, pkgConf, pkgDeps, packagedApi).then(function (api) {
|
||||
api(req, res, next);
|
||||
}, function (err) {
|
||||
console.error('[App Promise Error]');
|
||||
|
|
|
@ -225,6 +225,7 @@ module.exports.create = function (webserver, info, state) {
|
|||
config: pkgConf
|
||||
, deps: pkgDeps
|
||||
, services: Services
|
||||
, conf: info.conf
|
||||
}, req, res, next);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue