From 75b9a0e2b3a660d5ad61ae8fe139c1ad8225c955 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 8 Dec 2015 00:51:48 +0000 Subject: [PATCH] (still) half-baked oauth3 layer --- lib/oauth3-auth.js | 369 +++++++++++++++++++++++++++++------------- lib/package-server.js | 36 ++++- lib/worker.js | 1 + 3 files changed, 286 insertions(+), 120 deletions(-) diff --git a/lib/oauth3-auth.js b/lib/oauth3-auth.js index 32cc979..024b28f 100644 --- a/lib/oauth3-auth.js +++ b/lib/oauth3-auth.js @@ -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); }; diff --git a/lib/package-server.js b/lib/package-server.js index 5763b48..60c534d 100644 --- a/lib/package-server.js +++ b/lib/package-server.js @@ -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]'); diff --git a/lib/worker.js b/lib/worker.js index 0dc29fd..ac9f303 100644 --- a/lib/worker.js +++ b/lib/worker.js @@ -225,6 +225,7 @@ module.exports.create = function (webserver, info, state) { config: pkgConf , deps: pkgDeps , services: Services + , conf: info.conf }, req, res, next); }