adjust flow
This commit is contained in:
parent
aa28f00a4b
commit
82b0fcf00f
63
lib/apis.js
63
lib/apis.js
|
@ -124,6 +124,7 @@ module.exports.create = function (xconfx, apiFactories, apiDeps) {
|
|||
var tok = req.oauth3.token;
|
||||
var accountId = req.params.accountId || '__NO_ID_GIVEN__';
|
||||
var ppid;
|
||||
var iss = tok.iss;
|
||||
|
||||
if (tok.sub && tok.sub.split(/,/g).filter(function (ppid) {
|
||||
return ppid === accountId;
|
||||
|
@ -131,6 +132,7 @@ module.exports.create = function (xconfx, apiFactories, apiDeps) {
|
|||
ppid = accountId;
|
||||
}
|
||||
|
||||
// Deprecated backwards compat. To be removed.
|
||||
if (tok.axs && tok.axs.filter(function (acc) {
|
||||
return acc.id === accountId || acc.appScopedId === accountId;
|
||||
}).length) {
|
||||
|
@ -145,10 +147,9 @@ module.exports.create = function (xconfx, apiFactories, apiDeps) {
|
|||
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.ppid = ppid;
|
||||
req.oauth3.accountHash = crypto.createHash('sha1').update(accountIdx).digest('hex');
|
||||
//console.log('[walnut@daplie.com] accountIdx:', accountIdx);
|
||||
//console.log('[walnut@daplie.com] ppid:', ppid);
|
||||
|
||||
|
@ -160,19 +161,31 @@ module.exports.create = function (xconfx, apiFactories, apiDeps) {
|
|||
}
|
||||
|
||||
function accountRequired(req, res, next) {
|
||||
console.log('[accountRequired] [enter]');
|
||||
|
||||
var myIss = req.experienceId;
|
||||
var isPpid;
|
||||
|
||||
// if this already has auth, great
|
||||
if (req.oauth3.ppid && req.oauth3.accountIdx) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
// being public does not disallow authentication
|
||||
if (req.isPublic && !req.oauth3.encodedToken) {
|
||||
next();
|
||||
return;
|
||||
// except that if it's a ppid, we have to internally exchange it for the real token
|
||||
isPpid = (myIss === req.oauth3.iss && myIss !== req.oauth3.azp);
|
||||
if (!isPpid) {
|
||||
console.log('[accountRequired] has token already');
|
||||
console.log(req.oauth3);
|
||||
console.log('');
|
||||
next();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!req.oauth3.encodedToken) {
|
||||
// being public does not disallow authentication
|
||||
if (req.isPublic) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
rejectableRequest(
|
||||
req
|
||||
, res
|
||||
|
@ -187,6 +200,7 @@ module.exports.create = function (xconfx, apiFactories, apiDeps) {
|
|||
var tok = req.oauth3.token;
|
||||
var ppid;
|
||||
var err;
|
||||
var iss = tok.iss;
|
||||
|
||||
if (tok.sub) {
|
||||
if (tok.sub.split(/,/g).length > 1) {
|
||||
|
@ -195,25 +209,30 @@ module.exports.create = function (xconfx, apiFactories, apiDeps) {
|
|||
}
|
||||
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) {
|
||||
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.ppid = ppid;
|
||||
req.oauth3.accountHash = crypto.createHash('sha1').update(accountIdx).digest('hex');
|
||||
|
||||
next();
|
||||
});
|
||||
|
|
216
lib/oauth3.js
216
lib/oauth3.js
|
@ -2,7 +2,9 @@
|
|||
|
||||
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*/) {
|
||||
// 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.
|
||||
|
@ -26,6 +28,8 @@ function generateRescope(req, Models, decoded, fullPpid, ppid) {
|
|||
|
||||
return result;
|
||||
}).then(function (result) {
|
||||
var err;
|
||||
|
||||
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)
|
||||
//req.oauth3.accountIdx = fullPpid;
|
||||
|
@ -35,13 +39,12 @@ function generateRescope(req, Models, decoded, fullPpid, ppid) {
|
|||
console.log('[DEBUG] fullPpid:', fullPpid);
|
||||
console.log('[DEBUG] ppid:', ppid);
|
||||
|
||||
if (!req.oauth3.token.sub || !req.oauth3.token.iss) {
|
||||
throw new Error(
|
||||
"TODO: No profile found with that credential. Would you like to create a new profile or link to an existing profile?"
|
||||
);
|
||||
}
|
||||
|
||||
return req.oauth3.token.sub + '@' + req.oauth3.token.iss;
|
||||
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
|
||||
|
@ -56,52 +59,8 @@ function generateRescope(req, Models, decoded, fullPpid, ppid) {
|
|||
};
|
||||
}
|
||||
|
||||
function extractAccessToken(req) {
|
||||
var token = null;
|
||||
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) {
|
||||
function verifyToken(token, opts) {
|
||||
opts = opts || { audiences: [], complete: false };
|
||||
var jwt = require('jsonwebtoken');
|
||||
var decoded;
|
||||
|
||||
|
@ -116,6 +75,7 @@ function verifyToken(token) {
|
|||
try {
|
||||
decoded = jwt.decode(token, {complete: true});
|
||||
} catch (e) {}
|
||||
|
||||
if (!decoded) {
|
||||
return PromiseA.reject({
|
||||
message: 'provided token not a JSON Web Token'
|
||||
|
@ -148,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');
|
||||
OAUTH3._hooks = require('oauth3.js/oauth3.node.storage.js');
|
||||
return OAUTH3.discover(decoded.payload.iss).then(function (directives) {
|
||||
|
@ -206,15 +187,18 @@ function verifyToken(token) {
|
|||
if (res.data.error) {
|
||||
return PromiseA.reject(res.data.error);
|
||||
}
|
||||
var opts = {};
|
||||
var opts2 = {};
|
||||
if (Array.isArray(res.data.alg)) {
|
||||
opts.algorithms = res.data.alg;
|
||||
opts2.algorithms = res.data.alg;
|
||||
} else if (typeof res.data.alg === 'string') {
|
||||
opts.algorithms = [res.data.alg];
|
||||
opts2.algorithms = [res.data.alg];
|
||||
}
|
||||
|
||||
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) {
|
||||
if ('TokenExpiredError' === err.name) {
|
||||
return PromiseA.reject({
|
||||
|
@ -243,6 +227,39 @@ function deepFreeze(obj) {
|
|||
Object.freeze(obj);
|
||||
}
|
||||
|
||||
function fiddleOauth3(Models, req) {
|
||||
var token = req.oauth3.encodedToken;
|
||||
|
||||
req.oauth3.verifyAsync = function (jwt, opts) {
|
||||
return verifyToken(jwt || token, opts || { audiences: [ req.experienceId ] });
|
||||
};
|
||||
|
||||
if (!token) {
|
||||
return PromiseA.resolve(null);
|
||||
}
|
||||
|
||||
return verifyToken(token, { complete: false, audiences: [ req.experienceId ] }).then(function (decoded) {
|
||||
var err;
|
||||
req.oauth3.token = decoded;
|
||||
|
||||
if (!decoded) {
|
||||
return null;
|
||||
}
|
||||
|
||||
req.oauth3.ppid = decoded.sub;
|
||||
|
||||
req.oauth3.id = decoded.sub + '@' + decoded.iss;
|
||||
req.oauth3.sub = decoded.sub;
|
||||
req.oauth3.iss = decoded.iss;
|
||||
req.oauth3.azp = decoded.azp;
|
||||
req.oauth3.aud = decoded.aud;
|
||||
|
||||
req.oauth3.accountIdx = req.oauth3.id;
|
||||
|
||||
req.oauth3.rescope = generateRescope(req, Models, decoded);
|
||||
});
|
||||
}
|
||||
|
||||
function cookieOauth3(Models, req, res, next) {
|
||||
req.oauth3 = {};
|
||||
|
||||
|
@ -250,26 +267,7 @@ function cookieOauth3(Models, req, res, next) {
|
|||
var token = req.cookies[cookieName];
|
||||
|
||||
req.oauth3.encodedToken = token;
|
||||
req.oauth3.verifyAsync = function (jwt) {
|
||||
return verifyToken(jwt || token);
|
||||
};
|
||||
|
||||
return verifyToken(token).then(function (decoded) {
|
||||
req.oauth3.token = decoded;
|
||||
if (!decoded) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var ppid = decoded.sub || decoded.ppid || decoded.appScopedId;
|
||||
req.oauth3.ppid = ppid;
|
||||
req.oauth3.accountIdx = ppid+'@'+decoded.iss;
|
||||
|
||||
var hash = require('crypto').createHash('sha256').update(req.oauth3.accountIdx).digest('base64');
|
||||
hash = hash.replace(/\+/g, '-').replace(/\//g, '_').replace(/\=+/g, '');
|
||||
req.oauth3.accountHash = hash;
|
||||
|
||||
req.oauth3.rescope = generateRescope(req, Models, decoded, fullPpid, ppid);
|
||||
}).then(function () {
|
||||
fiddleOauth3(Models, req).then(function () {
|
||||
deepFreeze(req.oauth3);
|
||||
//Object.defineProperty(req, 'oauth3', {configurable: false, writable: false});
|
||||
next();
|
||||
|
@ -292,37 +290,49 @@ function cookieOauth3(Models, req, res, next) {
|
|||
function attachOauth3(Models, req, res, next) {
|
||||
req.oauth3 = {};
|
||||
|
||||
extractAccessToken(req).then(function (token) {
|
||||
req.oauth3.encodedToken = token;
|
||||
req.oauth3.verifyAsync = function (jwt) {
|
||||
return verifyToken(jwt || token);
|
||||
};
|
||||
var token = null;
|
||||
var parts;
|
||||
var scheme;
|
||||
var credentials;
|
||||
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
return verifyToken(token);
|
||||
}).then(function (decoded) {
|
||||
req.oauth3.token = decoded;
|
||||
if (!decoded) {
|
||||
return null;
|
||||
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"));
|
||||
}
|
||||
|
||||
var ppid = decoded.sub || decoded.ppid || decoded.appScopedId;
|
||||
var fullPpid = ppid+'@'+decoded.iss;
|
||||
req.oauth3.ppid = ppid;
|
||||
scheme = parts[0];
|
||||
credentials = parts[1];
|
||||
|
||||
// TODO we can anonymize the relationship between our user as the other service's user
|
||||
// in our own database by hashing the remote service's ppid and using that as the lookup
|
||||
var hash = require('crypto').createHash('sha256').update(fullPpid).digest('base64');
|
||||
hash = hash.replace(/\+/g, '-').replace(/\//g, '_').replace(/\=+/g, '');
|
||||
req.oauth3.accountHash = hash;
|
||||
if (-1 !== ['token', 'bearer'].indexOf(scheme.toLowerCase())) {
|
||||
token = credentials;
|
||||
}
|
||||
}
|
||||
|
||||
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:');
|
||||
console.log(req.oauth3);
|
||||
}).then(function () {
|
||||
// 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); }
|
||||
*/
|
||||
|
||||
req.oauth3.encodedToken = token;
|
||||
fiddleOauth3(Models, req).then(function () {
|
||||
//deepFreeze(req.oauth3);
|
||||
//Object.defineProperty(req, 'oauth3', {configurable: false, writable: false});
|
||||
next();
|
||||
|
|
Loading…
Reference in New Issue