From 82b0fcf00f5a93ed1667e5f7fa1f4f1c99da8c81 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 7 Dec 2017 08:20:46 +0000 Subject: [PATCH] adjust flow --- lib/apis.js | 63 ++++++++++----- lib/oauth3.js | 216 ++++++++++++++++++++++++++------------------------ 2 files changed, 154 insertions(+), 125 deletions(-) diff --git a/lib/apis.js b/lib/apis.js index 76aba6e..1f3f585 100644 --- a/lib/apis.js +++ b/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(); }); diff --git a/lib/oauth3.js b/lib/oauth3.js index ff0cd27..13f818b 100644 --- a/lib/oauth3.js +++ b/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();