Compare commits

..

7 Commits

Author SHA1 Message Date
AJ ONeal 82b0fcf00f adjust flow 2017-12-07 08:20:46 +00:00
AJ ONeal aa28f00a4b let validated 3rd party tokens pass 2017-12-06 07:46:25 +00:00
AJ ONeal fc07a8dd30 log cleanup 2017-12-06 06:02:17 +00:00
AJ ONeal 1e9618f5ec use proper token params 2017-12-05 22:36:46 +00:00
AJ ONeal 5f1191a6b9 Merge branch 'coolaj86' into installer-v2 2017-12-05 22:10:43 +00:00
AJ ONeal 66850535d3 delete expired token cookies 2017-12-05 22:18:03 +00:00
AJ ONeal 1eb98edd2b more docs 2017-11-28 21:25:52 +00:00
8 changed files with 233 additions and 166 deletions

View File

@ -19,7 +19,7 @@ Security Features
* disallows cookies, except for protected static assets * disallows cookies, except for protected static assets
* api.* subdomain for apis * api.* subdomain for apis
* assets.* subdomain for protected assets * assets.* subdomain for protected assets
* *must* sit behind a trusted https proxy (such as [Goldilocks](https://git.coolaj86.com/coolaj86/goldilocks.js)) * *must* sit behind a trusted https proxy (such as [Goldilocks](https://git.daplie.com/Daplie/goldilocks.js))
* HTTPS-only (checks for X-Forwarded-For) * HTTPS-only (checks for X-Forwarded-For)
* AES, RSA, and ECDSA encryption and signing * AES, RSA, and ECDSA encryption and signing
* Safe against CSRF, XSS, and SQL injection * Safe against CSRF, XSS, and SQL injection
@ -34,14 +34,14 @@ Application Features
* JSON-only expressjs APIs * JSON-only expressjs APIs
* Capability-based permissions system for (oauth3-discoverable) packages such as * Capability-based permissions system for (oauth3-discoverable) packages such as
* large file access (files@oauth3.org) * large file access (files@daplie.com)
* database access (data@oauth3.org) * database access (data@daplie.com)
* scheduling (for background tasks, alerts, alarms, calendars, reminders, etc) (events@oauth3.org) * scheduling (for background tasks, alerts, alarms, calendars, reminders, etc) (events@daplie.com)
* payments (credit card) (payments@oauth3.org) * payments (credit card) (payments@daplie.com)
* email (email@oauth3.org) * email (email@daplie.com)
* SMS (texting) (tel@oauth3.org) * SMS (texting) (tel@daplie.com)
* voice (calls and answering machine) (tel@oauth3.org) * voice (calls and answering machine) (tel@daplie.com)
* lamba-style functions (functions@oauth3.org) * lamba-style functions (functions@daplie.com)
* Per-app, per-site, and per-user configurations * Per-app, per-site, and per-user configurations
* Multi-Tentated Application Management * Multi-Tentated Application Management
* Built-in OAuth2 & OAuth3 support * Built-in OAuth2 & OAuth3 support
@ -54,7 +54,7 @@ Installation
We're still in a stage where the installation generally requires many manual steps. We're still in a stage where the installation generally requires many manual steps.
```bash ```bash
curl https://git.coolaj86.com/coolaj86/walnut.js/raw/v1.2/installer/get.sh | bash curl https://git.daplie.com/Daplie/walnut.js/raw/v1.2/installer/get.sh | bash
``` ```
See [INSTALL.md](/INSTALL.md) See [INSTALL.md](/INSTALL.md)
@ -104,6 +104,7 @@ Understanding Walnut
│ ├── rest │ ├── rest
│ └── services │ └── services
└── var └── var
├── <<pkgname>>/config.json
└── sites └── sites
``` ```
@ -131,7 +132,7 @@ Initialization
needs to know its primary domain needs to know its primary domain
``` ```
POST https://api.<domain.tld>/api/walnut@oauth3.org/init POST https://api.<domain.tld>/api/walnut@daplie.com/init
{ "domain": "<domain.tld>" } { "domain": "<domain.tld>" }
``` ```
@ -153,18 +154,18 @@ api.<domain.tld>
assets.<domain.tld> assets.<domain.tld>
``` ```
The domains can be setup through the OAuth3 Desktop App or with `oauth3-tools` The domains can be setup through the Daplie Desktop App or with `daplie-tools`
```bash ```bash
# set device address and attach primary domain # set device address and attach primary domain
oauth3 devices:attach -d foodevice -n example.com -a 127.0.0.1 daplie devices:attach -d foodevice -n example.com -a 127.0.0.1
# attach all other domains with same device/address # attach all other domains with same device/address
oauth3 devices:attach -d foodevice -n www.example.com daplie devices:attach -d foodevice -n www.example.com
oauth3 devices:attach -d foodevice -n api.example.com daplie devices:attach -d foodevice -n api.example.com
oauth3 devices:attach -d foodevice -n assets.example.com daplie devices:attach -d foodevice -n assets.example.com
oauth3 devices:attach -d foodevice -n cloud.example.com daplie devices:attach -d foodevice -n cloud.example.com
oauth3 devices:attach -d foodevice -n api.cloud.example.com daplie devices:attach -d foodevice -n api.cloud.example.com
``` ```
Example `/etc/goldilocks/goldilocks.yml`: Example `/etc/goldilocks/goldilocks.yml`:
@ -194,7 +195,7 @@ Resetting the Initialization
Once you run the app the initialization files will appear in these locations Once you run the app the initialization files will appear in these locations
``` ```
/srv/walnut/var/walnut+config@oauth3.org.sqlite3 /srv/walnut/var/walnut+config@daplie.com.sqlite3
/srv/walnut/config/<domain.tld>/config.json /srv/walnut/config/<domain.tld>/config.json
``` ```
@ -279,7 +280,7 @@ The packages:
The permissions: The permissions:
``` ```
/srv/walnut/packages/ /srv/walnut/etc/
└── client-api-grants └── client-api-grants
└── cloud.foobar.me └── cloud.foobar.me
''' '''
@ -290,7 +291,7 @@ The permissions:
``` ```
/srv/walnut/var/ /srv/walnut/var/
└── sites └── sites
└── example.com └── daplie.me
''' '''
seed@example.com # refers to /srv/walnut/packages/pages/seed@example.com seed@example.com # refers to /srv/walnut/packages/pages/seed@example.com
''' '''

11
dist/var/example.com/config.json vendored Normal file
View File

@ -0,0 +1,11 @@
{ "mailgun.org": {
"apiKey": "key-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
, "apiPublicKey": "pubkey-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
, "auth": {
"user": "mailer@example.com"
, "pass": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
, "api_key": "key-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
, "domain": "example.com"
}
}
}

View File

@ -7,7 +7,7 @@ my_ver=master
my_tmp=$(mktemp -d) my_tmp=$(mktemp -d)
mkdir -p $my_tmp/opt/$my_name/lib/node_modules/$my_name mkdir -p $my_tmp/opt/$my_name/lib/node_modules/$my_name
git clone https://git.coolaj86.com/coolaj86/walnut.js.git $my_tmp/opt/$my_name/core git clone https://git.daplie.com/Daplie/walnut.js.git $my_tmp/opt/$my_name/core
echo "Installing to $my_tmp (will be moved after install)" echo "Installing to $my_tmp (will be moved after install)"
pushd $my_tmp/opt/$my_name/core pushd $my_tmp/opt/$my_name/core

View File

@ -5,7 +5,7 @@
# # # #
############################### ###############################
# See https://git.coolaj86.com/coolaj86/snippets/blob/master/bash/http-get.sh # See https://git.daplie.com/Daplie/daplie-snippets/blob/master/bash/http-get.sh
_h_http_get="" _h_http_get=""
_h_http_opts="" _h_http_opts=""

View File

@ -6,7 +6,7 @@ set -u
### IMPORTANT ### ### IMPORTANT ###
### VERSION ### ### VERSION ###
my_name=walnut my_name=walnut
my_app_pkg_name=org.oauth3.walnut.web my_app_pkg_name=com.daplie.walnut.web
my_app_ver="v1.2" my_app_ver="v1.2"
my_azp_oauth3_ver="v1.2" my_azp_oauth3_ver="v1.2"
# is the old version still needed in launchpad? # is the old version still needed in launchpad?
@ -30,7 +30,7 @@ my_app_ver="v1.2"
my_launchpad_ver="v1.2" my_launchpad_ver="v1.2"
my_iss_oauth3_rest_ver="v1.2.0" my_iss_oauth3_rest_ver="v1.2.0"
my_iss_oauth3_pages_ver="v1.2.1" my_iss_oauth3_pages_ver="v1.2.1"
my_www_ppl_ver=v1.0.15 my_www_daplie_ver=v1.0.15
export NODE_VERSION="v8.9.0" export NODE_VERSION="v8.9.0"
################# #################
export NODE_PATH=$my_tmp/opt/$my_name/lib/node_modules export NODE_PATH=$my_tmp/opt/$my_name/lib/node_modules
@ -44,7 +44,7 @@ my_npm="$NPM_CONFIG_PREFIX/bin/npm"
# TODO un-hardcode core at al # TODO un-hardcode core at al
#my_app_dist=$my_tmp/opt/$my_name/lib/node_modules/$my_name/dist #my_app_dist=$my_tmp/opt/$my_name/lib/node_modules/$my_name/dist
my_app_dist=$my_tmp/opt/$my_name/core/dist my_app_dist=$my_tmp/opt/$my_name/core/dist
installer_base="https://git.coolaj86.com/coolaj86/goldilocks.js/raw/$my_app_ver" installer_base="https://git.daplie.com/Daplie/goldilocks.js/raw/$my_app_ver"
# Backwards compat # Backwards compat
# some scripts still use the old names # some scripts still use the old names
@ -102,8 +102,8 @@ pushd $my_tmp/opt/$my_name/core
$my_npm install $my_npm install
popd popd
git clone https://git.coolaj86.com/coolaj86/walnut_launchpad.html.git $my_tmp/opt/$my_name/core/lib/walnut@oauth3.org/setup git clone https://git.daplie.com/Daplie/walnut_launchpad.git $my_tmp/opt/$my_name/core/lib/walnut@daplie.com/setup
pushd $my_tmp/opt/$my_name/core/lib/walnut@oauth3.org/setup pushd $my_tmp/opt/$my_name/core/lib/walnut@daplie.com/setup
git pull git pull
git checkout $my_launchpad_ver git checkout $my_launchpad_ver
@ -130,9 +130,9 @@ pushd $my_tmp/opt/$my_name/packages
popd popd
popd popd
git clone https://git.coolaj86.com/coolaj86/walnut_rest_www_oauth3.org.js.git rest/www@oauth3.org git clone https://git.daplie.com/Daplie/walnut_rest_www_daplie.com.git rest/www@daplie.com
pushd rest/www@oauth3.org pushd rest/www@daplie.com
git checkout $my_www_ppl_ver git checkout $my_www_daplie_ver
$my_npm install $my_npm install
popd popd
popd popd

View File

@ -124,6 +124,7 @@ module.exports.create = function (xconfx, apiFactories, apiDeps) {
var tok = req.oauth3.token; var tok = req.oauth3.token;
var accountId = req.params.accountId || '__NO_ID_GIVEN__'; var accountId = req.params.accountId || '__NO_ID_GIVEN__';
var ppid; var ppid;
var iss = tok.iss;
if (tok.sub && tok.sub.split(/,/g).filter(function (ppid) { if (tok.sub && tok.sub.split(/,/g).filter(function (ppid) {
return ppid === accountId; return ppid === accountId;
@ -131,6 +132,7 @@ module.exports.create = function (xconfx, apiFactories, apiDeps) {
ppid = accountId; ppid = accountId;
} }
// Deprecated backwards compat. To be removed.
if (tok.axs && tok.axs.filter(function (acc) { if (tok.axs && tok.axs.filter(function (acc) {
return acc.id === accountId || acc.appScopedId === accountId; return acc.id === accountId || acc.appScopedId === accountId;
}).length) { }).length) {
@ -145,10 +147,9 @@ module.exports.create = function (xconfx, apiFactories, apiDeps) {
return PromiseA.reject(new Error("missing accountId '" + accountId + "' in access token")); return PromiseA.reject(new Error("missing accountId '" + accountId + "' in access token"));
} }
return req.oauth3.rescope(ppid).then(function (accountIdx) { return req.oauth3.rescope().then(function (accountIdx) {
req.oauth3.accountIdx = accountIdx; req.oauth3.accountIdx = accountIdx;
req.oauth3.ppid = ppid; req.oauth3.ppid = ppid;
req.oauth3.accountHash = crypto.createHash('sha1').update(accountIdx).digest('hex');
//console.log('[walnut@daplie.com] accountIdx:', accountIdx); //console.log('[walnut@daplie.com] accountIdx:', accountIdx);
//console.log('[walnut@daplie.com] ppid:', ppid); //console.log('[walnut@daplie.com] ppid:', ppid);
@ -160,19 +161,31 @@ module.exports.create = function (xconfx, apiFactories, apiDeps) {
} }
function accountRequired(req, res, next) { function accountRequired(req, res, next) {
console.log('[accountRequired] [enter]');
var myIss = req.experienceId;
var isPpid;
// if this already has auth, great // if this already has auth, great
if (req.oauth3.ppid && req.oauth3.accountIdx) { if (req.oauth3.ppid && req.oauth3.accountIdx) {
next(); // except that if it's a ppid, we have to internally exchange it for the real token
return; isPpid = (myIss === req.oauth3.iss && myIss !== req.oauth3.azp);
} if (!isPpid) {
console.log('[accountRequired] has token already');
// being public does not disallow authentication console.log(req.oauth3);
if (req.isPublic && !req.oauth3.encodedToken) { console.log('');
next(); next();
return; return;
}
} }
if (!req.oauth3.encodedToken) { if (!req.oauth3.encodedToken) {
// being public does not disallow authentication
if (req.isPublic) {
next();
return;
}
rejectableRequest( rejectableRequest(
req req
, res , res
@ -187,6 +200,7 @@ module.exports.create = function (xconfx, apiFactories, apiDeps) {
var tok = req.oauth3.token; var tok = req.oauth3.token;
var ppid; var ppid;
var err; var err;
var iss = tok.iss;
if (tok.sub) { if (tok.sub) {
if (tok.sub.split(/,/g).length > 1) { if (tok.sub.split(/,/g).length > 1) {
@ -195,25 +209,30 @@ module.exports.create = function (xconfx, apiFactories, apiDeps) {
} }
ppid = tok.sub; ppid = tok.sub;
} }
else if (tok.axs && tok.axs.length) {
if (tok.axs.length > 1) {
err = new Error("more than one 'axs' specified in token (also, update to using 'sub' instead)");
return PromiseA.reject(err);
}
ppid = tok.axs[0].appScopedId || tok.axs[0].id;
}
else if (tok.acx) {
ppid = tok.acx.appScopedId || tok.acx.id || tok.acx;
}
if (!ppid) { if (!ppid) {
return PromiseA.reject(new Error("could not determine accountId from access token")); return PromiseA.reject(new Error("could not determine accountId from access token"));
} }
return req.oauth3.rescope(ppid).then(function (accountIdx) { return req.oauth3.rescope().then(function (accountIdx) {
console.log('[accountRequired] req.oauth3');
console.log(accountIdx);
var sub = accountIdx.split('@')[0];
var iss = accountIdx.split('@')[1];
var id = sub + '@' + iss;
req.oauth3.profile = {
id: id
, sub: sub
, iss: iss
};
req.oauth3.id = id;
req.oauth3.sub = sub;
req.oauth3.iss = iss;
req.oauth3.accountIdx = accountIdx; req.oauth3.accountIdx = accountIdx;
req.oauth3.ppid = ppid; req.oauth3.ppid = ppid;
req.oauth3.accountHash = crypto.createHash('sha1').update(accountIdx).digest('hex');
next(); next();
}); });

View File

@ -2,15 +2,17 @@
var PromiseA = require('bluebird'); var PromiseA = require('bluebird');
function generateRescope(req, Models, decoded, fullPpid, ppid) { function generateRescope(req, Models, decoded) {
var fullPpid = decoded.sub+'@'+decoded.iss;
var ppid = decoded.sub;
return function (/*sub*/) { return function (/*sub*/) {
// TODO: this function is supposed to convert PPIDs of different parties to some account // TODO: this function is supposed to convert PPIDs of different parties to some account
// ID that allows application to keep track of permisions and what-not. // ID that allows application to keep track of permisions and what-not.
console.log('[rescope] Attempting ', fullPpid); console.log('[rescope] Attempting ', fullPpid);
return Models.IssuerOauth3OrgGrants.find({ azpSub: fullPpid }).then(function (results) { return Models.IssuerOauth3OrgGrants.find({ azpSub: fullPpid }).then(function (results) {
if (results[0]) { if (results[0]) {
console.log('[rescope] lukcy duck: got it on the 1st try'); console.log('[rescope] lucky duck: got it on the 1st try');
return PromiseA.resolve(results); return results;
} }
// XXX BUG XXX // XXX BUG XXX
@ -19,71 +21,46 @@ function generateRescope(req, Models, decoded, fullPpid, ppid) {
}).then(function (results) { }).then(function (results) {
var result = results[0]; var result = results[0];
if (!result || !result.sub || !decoded.iss) {
console.log('[rescope] Not a 2nd party token...');
return Models.IssuerOauth3OrgProfiles.get(fullPpid);
}
return result;
}).then(function (result) {
var err;
if (!result || !result.sub || !decoded.iss) { if (!result || !result.sub || !decoded.iss) {
// XXX BUG XXX TODO swap this external ppid for an internal (and ask user to link with existing profile) // XXX BUG XXX TODO swap this external ppid for an internal (and ask user to link with existing profile)
//req.oauth3.accountIdx = fullPpid; //req.oauth3.accountIdx = fullPpid;
throw new Error("internal / external ID swapping not yet implemented. TODO: " console.log('[DEBUG] decoded:');
+ "No profile found with that credential. Would you like to create a new profile or link to an existing profile?"); console.log(decoded);
console.log('[DEBUG] decoded.iss:', decoded.iss);
console.log('[DEBUG] fullPpid:', fullPpid);
console.log('[DEBUG] ppid:', ppid);
err = new Error(
"TODO: No profile found with that credential. Would you like to create a new profile or link to an existing profile?"
);
err.code = "E_NO_PROFILE@oauth3.org"
throw err;
//return req.oauth3.token.sub + '@' + req.oauth3.token.iss;
} }
// XXX BUG XXX need to pass own url in to use as issuer for own tokens // XXX BUG XXX need to pass own url in to use as issuer for own tokens
req.oauth3.accountIdx = result.sub + '@' + decoded.iss; req.oauth3.accountIdx = result.sub + '@' + (result.iss || decoded.iss);
console.log('[rescope] result:'); console.log('[rescope] result:');
console.log(results); console.log(result);
console.log(req.oauth3.accountIdx); console.log('[rescope] req.oauth3.accountIdx:', req.oauth3.accountIdx);
return PromiseA.resolve(req.oauth3.accountIdx); return req.oauth3.accountIdx;
}); });
}; };
} }
function extractAccessToken(req) { function verifyToken(token, opts) {
var token = null; opts = opts || { audiences: [], complete: false };
var parts;
var scheme;
var credentials;
if (req.headers && req.headers.authorization) {
// Works for all of Authorization: Bearer {{ token }}, Token {{ token }}, JWT {{ token }}
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 !== ['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'
// NOTE: the case of DDNS on routers requires a GET and access_token
// (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 verifyToken(token) {
var jwt = require('jsonwebtoken'); var jwt = require('jsonwebtoken');
var decoded; var decoded;
@ -98,6 +75,7 @@ function verifyToken(token) {
try { try {
decoded = jwt.decode(token, {complete: true}); decoded = jwt.decode(token, {complete: true});
} catch (e) {} } catch (e) {}
if (!decoded) { if (!decoded) {
return PromiseA.reject({ return PromiseA.reject({
message: 'provided token not a JSON Web Token' message: 'provided token not a JSON Web Token'
@ -130,6 +108,27 @@ function verifyToken(token) {
}); });
} }
var audMatch = decoded.payload.aud && ('*' === decoded.payload.aud || opts.audiences.some(function (aud) { return -1 !== decoded.payload.aud.split(',').indexOf(aud); }));
var azpMatch = decoded.payload.azp && ('*' === decoded.payload.azp || opts.audiences.some(function (aud) { return -1 !== decoded.payload.azp.split(',').indexOf(aud); }));
if (!audMatch) {
console.log("[verifyToken] 'aud' '" + decoded.payload.aud + "' does not match '" + opts.audiences.join(',') + "'");
}
// TODO needs an option to verify that the sender of the token was, in fact, the azp (i.e. the Origin and/or Referer Headers)
if (!azpMatch) {
console.log("[verifyToken] 'azp' '" + decoded.payload.azp + "' does not match '" + opts.audiences.join(',') + "'");
}
if (!audMatch && !azpMatch) {
err = new Error(
"Application '" + req.experienceId + "' refused token because '" + decoded.payload.aud + "' is not an accepted audience (aud)"
+ " and '" + decoded.payload.azp + "' is not an authorized party (azp)"
);
err.code = 'E_TOKEN_AUD';
err.url = 'https://oauth3.org/docs/errors#E_TOKEN_AUD'
return PromiseA.reject(err);
}
var OAUTH3 = require('oauth3.js'); var OAUTH3 = require('oauth3.js');
OAUTH3._hooks = require('oauth3.js/oauth3.node.storage.js'); OAUTH3._hooks = require('oauth3.js/oauth3.node.storage.js');
return OAUTH3.discover(decoded.payload.iss).then(function (directives) { return OAUTH3.discover(decoded.payload.iss).then(function (directives) {
@ -188,16 +187,27 @@ function verifyToken(token) {
if (res.data.error) { if (res.data.error) {
return PromiseA.reject(res.data.error); return PromiseA.reject(res.data.error);
} }
var opts = {}; var opts2 = {};
if (Array.isArray(res.data.alg)) { if (Array.isArray(res.data.alg)) {
opts.algorithms = res.data.alg; opts2.algorithms = res.data.alg;
} else if (typeof res.data.alg === 'string') { } else if (typeof res.data.alg === 'string') {
opts.algorithms = [res.data.alg]; opts2.algorithms = [res.data.alg];
} }
try { try {
return jwt.verify(token, require('jwk-to-pem')(res.data), opts); if (opts.complete) {
opts2.complete = true;
}
return jwt.verify(token, require('jwk-to-pem')(res.data), opts2);
} catch (err) { } catch (err) {
if ('TokenExpiredError' === err.name) {
return PromiseA.reject({
message: 'TokenExpiredError: jwt expired'
, code: 'E_TOKEN_EXPIRED'
, url: 'https://oauth3.org/docs/errors#E_TOKEN_EXPIRED'
});
}
return PromiseA.reject({ return PromiseA.reject({
message: 'token verification failed' message: 'token verification failed'
, code: 'E_INVALID_TOKEN' , code: 'E_INVALID_TOKEN'
@ -217,32 +227,47 @@ function deepFreeze(obj) {
Object.freeze(obj); Object.freeze(obj);
} }
function cookieOauth3(Models, req, res, next) { function fiddleOauth3(Models, req) {
req.oauth3 = {}; var token = req.oauth3.encodedToken;
var token = req.cookies.jwt; req.oauth3.verifyAsync = function (jwt, opts) {
return verifyToken(jwt || token, opts || { audiences: [ req.experienceId ] });
req.oauth3.encodedToken = token;
req.oauth3.verifyAsync = function (jwt) {
return verifyToken(jwt || token);
}; };
return verifyToken(token).then(function (decoded) { if (!token) {
return PromiseA.resolve(null);
}
return verifyToken(token, { complete: false, audiences: [ req.experienceId ] }).then(function (decoded) {
var err;
req.oauth3.token = decoded; req.oauth3.token = decoded;
if (!decoded) { if (!decoded) {
return null; return null;
} }
var ppid = decoded.sub || decoded.ppid || decoded.appScopedId; req.oauth3.ppid = decoded.sub;
req.oauth3.ppid = ppid;
req.oauth3.accountIdx = ppid+'@'+decoded.iss;
var hash = require('crypto').createHash('sha256').update(req.oauth3.accountIdx).digest('base64'); req.oauth3.id = decoded.sub + '@' + decoded.iss;
hash = hash.replace(/\+/g, '-').replace(/\//g, '_').replace(/\=+/g, ''); req.oauth3.sub = decoded.sub;
req.oauth3.accountHash = hash; req.oauth3.iss = decoded.iss;
req.oauth3.azp = decoded.azp;
req.oauth3.aud = decoded.aud;
req.oauth3.rescope = generateRescope(req, Models, decoded, fullPpid, ppid); req.oauth3.accountIdx = req.oauth3.id;
}).then(function () {
req.oauth3.rescope = generateRescope(req, Models, decoded);
});
}
function cookieOauth3(Models, req, res, next) {
req.oauth3 = {};
var cookieName = 'jwt';
var token = req.cookies[cookieName];
req.oauth3.encodedToken = token;
fiddleOauth3(Models, req).then(function () {
deepFreeze(req.oauth3); deepFreeze(req.oauth3);
//Object.defineProperty(req, 'oauth3', {configurable: false, writable: false}); //Object.defineProperty(req, 'oauth3', {configurable: false, writable: false});
next(); next();
@ -251,6 +276,11 @@ function cookieOauth3(Models, req, res, next) {
next(); next();
return; return;
} }
if ('E_TOKEN_EXPIRED' === err.code) {
res.clearCookie(cookieName);
next();
return;
}
console.error('[walnut] cookie lib/oauth3 error:'); console.error('[walnut] cookie lib/oauth3 error:');
console.error(err); console.error(err);
res.send(err); res.send(err);
@ -260,37 +290,49 @@ function cookieOauth3(Models, req, res, next) {
function attachOauth3(Models, req, res, next) { function attachOauth3(Models, req, res, next) {
req.oauth3 = {}; req.oauth3 = {};
extractAccessToken(req).then(function (token) { var token = null;
req.oauth3.encodedToken = token; var parts;
req.oauth3.verifyAsync = function (jwt) { var scheme;
return verifyToken(jwt || token); var credentials;
};
if (!token) { if (req.headers && req.headers.authorization) {
return null; // Works for all of Authorization: Bearer {{ token }}, Token {{ token }}, JWT {{ token }}
} parts = req.headers.authorization.split(' ');
return verifyToken(token);
}).then(function (decoded) { if (parts.length !== 2) {
req.oauth3.token = decoded; return PromiseA.reject(new Error("malformed Authorization header"));
if (!decoded) {
return null;
} }
var ppid = decoded.sub || decoded.ppid || decoded.appScopedId; scheme = parts[0];
var fullPpid = ppid+'@'+decoded.iss; credentials = parts[1];
req.oauth3.ppid = ppid;
// TODO we can anonymize the relationship between our user as the other service's user if (-1 !== ['token', 'bearer'].indexOf(scheme.toLowerCase())) {
// in our own database by hashing the remote service's ppid and using that as the lookup token = credentials;
var hash = require('crypto').createHash('sha256').update(fullPpid).digest('base64'); }
hash = hash.replace(/\+/g, '-').replace(/\//g, '_').replace(/\=+/g, ''); }
req.oauth3.accountHash = hash;
req.oauth3.rescope = generateRescope(req, Models, decoded, fullPpid, ppid); if (req.body && req.body.access_token) {
if (token) { PromiseA.reject(new Error("token exists in header and body")); }
token = req.body.access_token;
}
console.log('############### assigned req.oauth3:'); // TODO disallow query with req.method === 'GET'
console.log(req.oauth3); // NOTE: the case of DDNS on routers requires a GET and access_token
}).then(function () { // (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); }
*/
req.oauth3.encodedToken = token;
fiddleOauth3(Models, req).then(function () {
//deepFreeze(req.oauth3); //deepFreeze(req.oauth3);
//Object.defineProperty(req, 'oauth3', {configurable: false, writable: false}); //Object.defineProperty(req, 'oauth3', {configurable: false, writable: false});
next(); next();

View File

@ -8,7 +8,7 @@
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://git.coolaj86.com/coolaj86/walnut.js.git" "url": "https://github.com/Daplie/walnut.git"
}, },
"bin": { "bin": {
"walnut": "./bin/walnut.js" "walnut": "./bin/walnut.js"
@ -33,16 +33,16 @@
"private", "private",
"public" "public"
], ],
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com)", "author": "AJ ONeal <aj@daplie.com> (https://daplie.com)",
"license": "(MIT or Apache2)", "license": "(MIT or Apache2)",
"bugs": { "bugs": {
"url": "https://git.coolaj86.com/coolaj86/walnut.js/issues" "url": "https://github.com/Daplie/walnut/issues"
}, },
"homepage": "https://git.coolaj86.com/coolaj86/walnut.js", "homepage": "https://github.com/Daplie/walnut",
"dependencies": { "dependencies": {
"bluebird": "3.x", "bluebird": "3.x",
"body-parser": "1.x", "body-parser": "1.x",
"cluster-store": "^2.0.8", "cluster-store": "git+https://git.daplie.com/Daplie/cluster-store.git#v2",
"connect": "3.x", "connect": "3.x",
"connect-cors": "0.5.x", "connect-cors": "0.5.x",
"connect-recase": "^1.0.2", "connect-recase": "^1.0.2",
@ -57,7 +57,7 @@
"jwk-to-pem": "^1.2.6", "jwk-to-pem": "^1.2.6",
"mailchimp-api-v3": "^1.7.0", "mailchimp-api-v3": "^1.7.0",
"mandrill-api": "^1.0.45", "mandrill-api": "^1.0.45",
"masterquest-sqlite3": "^1.1.1", "masterquest-sqlite3": "git+https://git.daplie.com/node/masterquest-sqlite3.git",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"multiparty": "^4.1.3", "multiparty": "^4.1.3",
"nodemailer": "^1.4.0", "nodemailer": "^1.4.0",
@ -67,14 +67,8 @@
"request": "^2.81.0", "request": "^2.81.0",
"scmp": "^2.0.0", "scmp": "^2.0.0",
"serve-static": "1.x", "serve-static": "1.x",
"sqlite3-cluster": "^2.1.2", "sqlite3-cluster": "git+https://git.daplie.com/coolaj86/sqlite3-cluster.git#v2",
"stripe": "^4.22.0", "stripe": "^4.22.0",
"twilio": "1.x" "twilio": "1.x"
},
"gitDependencies": {
"cluster-store": "git+https://git.coolaj86.com/coolaj86/cluster-store.git#v2",
"masterquest-sqlite3": "git+https://git.coolaj86.com/coolaj86/masterquest-sqlite3.git",
"oauth3.js": "git+https://git.oauth3.org/OAuth3/oauth3.js.git#v1.2",
"sqlite3-cluster": "git+https://git.coolaj86.com/coolaj86/sqlite3-cluster.git#v2"
} }
} }