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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user