walnut.js/lib/oauth3-auth.js

408 lines
12 KiB
JavaScript

'use strict';
var PromiseA = require('bluebird');
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: [
'X-Requested-With'
, 'X-HTTP-Method-Override'
, 'Content-Type'
, 'Accept'
, 'Authorization'
], methods: [ "GET", "POST", "PATCH", "PUT", "DELETE" ] });
// Allows CORS access to API with ?access_token=
// TODO Access-Control-Max-Age: 600
// TODO How can we help apps handle this? token?
// TODO allow apps to configure trustedDomains, auth, etc
//function weakDecipher(secret, val) { return require('./weak-crypt').weakDecipher(val, secret); }
//
// Generic Session / Login / Account Routes
//
function parseAccessToken(req, opts) {
var token;
var parts;
var scheme;
var credentials;
if (req.headers && req.headers.authorization) {
parts = req.headers.authorization.split(' ');
if (parts.length !== 2) {
return PromiseA.reject(new Error("malformed Authorization header"));
}
scheme = parts[0];
credentials = parts[1];
if (-1 !== (opts && opts.schemes || ['token', 'bearer']).indexOf(scheme.toLowerCase())) {
token = credentials;
}
}
if (req.body && req.body.access_token) {
if (token) { PromiseA.reject(new Error("token exists in header and body")); }
token = req.body.access_token;
}
// TODO disallow query with req.method === 'GET'
// (cookies should be used for protected static assets)
if (req.query && req.query.access_token) {
if (token) { PromiseA.reject(new Error("token already exists in either header or body and also in query")); }
token = req.query.access_token;
}
/*
err = new Error(challenge());
err.code = 'E_BEARER_REALM';
if (!token) { return PromiseA.reject(err); }
*/
return PromiseA.resolve(token);
}
function getClient(req, token, priv, Controllers) {
if (!token) {
token = req.oauth3.token;
}
var cacheId = '_' + token.k + 'Client';
if (priv[cacheId]) {
return PromiseA.resolve(priv[cacheId]);
}
// TODO could get client directly by token.app (id of client)
priv[cacheId] = Controllers.Clients.login(null, token.k).then(function (apiKey) {
if (!apiKey) {
return PromiseA.reject(new Error("Client no longer valid"));
}
priv[cacheId + 'Key'] = apiKey;
priv[cacheId] = apiKey.oauthClient;
return apiKey.oauthClient;
});
return priv[cacheId];
}
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;
}
var cacheId = '_' + token.idx + 'LoginId';
if (priv[cacheId]) {
return PromiseA.resolve(priv[cacheId]);
}
// TODO
// this ends up defeating part of the purpose of JWT (few database calls)
// perhaps the oauthClient secret should be sent, encrypted with a master key,
// with the request? Or just mash the oauthClient secret with the loginId
// and encrypt with the master key?
priv._loginId = getClient(req, token, priv).then(function (oauthClient) {
var loginId;
if (token.idx) {
loginId = scoper.unscope(token.idx, oauthClient.secret);
} else {
loginId = token.usr;
}
priv[cacheId] = loginId;
return loginId;
});
return priv[cacheId];
}
function getLogin(req, token, priv, Controllers) {
if (!token) {
token = req.oauth3.token;
}
var cacheId = '_' + token.idx + 'Login';
if (priv[cacheId]) {
return PromiseA.resolve(priv[cacheId]);
}
priv[cacheId] = getLoginId(req, token, priv).then(function (loginId) {
// DB.Logins.get(hashId)
return Controllers.Logins.rawGet(loginId).then(function (login) {
priv[cacheId] = login;
return login;
});
});
return priv[cacheId];
}
function attachOauth3(req, res, next) {
var privs = {};
req.oauth3 = {};
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();
});
});
}
app.use('/', cors);
app.use('/', attachOauth3);
};