| 
									
										
										
										
											2017-07-13 18:23:42 -06:00
										 |  |  | 'use strict'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | var PromiseA = require('bluebird'); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-09-12 22:32:33 +00:00
										 |  |  | function generateRescope(req, Models, decoded, fullPpid, ppid) { | 
					
						
							|  |  |  |   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.
 | 
					
						
							|  |  |  |     console.log('[rescope] Attempting ', fullPpid); | 
					
						
							|  |  |  |     return Models.IssuerOauth3OrgGrants.find({ azpSub: fullPpid }).then(function (results) { | 
					
						
							|  |  |  |       if (results[0]) { | 
					
						
							|  |  |  |         console.log('[rescope] lukcy duck: got it on the 1st try'); | 
					
						
							|  |  |  |         return PromiseA.resolve(results); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       // XXX BUG XXX
 | 
					
						
							|  |  |  |       // should be able to distinguish between own ids and 3rd party via @whatever.com
 | 
					
						
							|  |  |  |       return Models.IssuerOauth3OrgGrants.find({ azpSub: ppid }); | 
					
						
							|  |  |  |     }).then(function (results) { | 
					
						
							|  |  |  |       var result = results[0]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       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;
 | 
					
						
							|  |  |  |         throw new Error("internal / external ID swapping not yet implemented. TODO: " | 
					
						
							|  |  |  |           + "No profile found with that credential. Would you like to create a new profile or link to an existing profile?"); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       // XXX BUG XXX need to pass own url in to use as issuer for own tokens
 | 
					
						
							|  |  |  |       req.oauth3.accountIdx = result.sub + '@' + decoded.iss; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       console.log('[rescope] result:'); | 
					
						
							|  |  |  |       console.log(results); | 
					
						
							|  |  |  |       console.log(req.oauth3.accountIdx); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       return PromiseA.resolve(req.oauth3.accountIdx); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-07-13 18:23:42 -06:00
										 |  |  | function extractAccessToken(req) { | 
					
						
							| 
									
										
										
										
											2017-08-11 17:00:18 -06:00
										 |  |  |   var token = null; | 
					
						
							| 
									
										
										
										
											2017-07-13 18:23:42 -06:00
										 |  |  |   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 decoded; | 
					
						
							| 
									
										
										
										
											2017-07-24 16:19:51 -06:00
										 |  |  | 
 | 
					
						
							|  |  |  |   if (!token) { | 
					
						
							|  |  |  |     return PromiseA.reject({ | 
					
						
							|  |  |  |       message: 'no token provided' | 
					
						
							|  |  |  |     , code: 'E_NO_TOKEN' | 
					
						
							|  |  |  |     , url: 'https://oauth3.org/docs/errors#E_NO_TOKEN' | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-07-13 18:23:42 -06:00
										 |  |  |   try { | 
					
						
							|  |  |  |     decoded = jwt.decode(token, {complete: true}); | 
					
						
							|  |  |  |   } catch (e) {} | 
					
						
							|  |  |  |   if (!decoded) { | 
					
						
							|  |  |  |     return PromiseA.reject({ | 
					
						
							|  |  |  |       message: 'provided token not a JSON Web Token' | 
					
						
							|  |  |  |     , code: 'E_NOT_JWT' | 
					
						
							|  |  |  |     , url: 'https://oauth3.org/docs/errors#E_NOT_JWT' | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   var sub = decoded.payload.sub || decoded.payload.ppid || decoded.payload.appScopedId; | 
					
						
							|  |  |  |   if (!sub) { | 
					
						
							|  |  |  |     return PromiseA.reject({ | 
					
						
							|  |  |  |       message: 'token missing sub' | 
					
						
							|  |  |  |     , code: 'E_MISSING_SUB' | 
					
						
							|  |  |  |     , url: 'https://oauth3.org/docs/errors#E_MISSING_SUB' | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   var kid = decoded.header.kid || decoded.payload.kid; | 
					
						
							|  |  |  |   if (!kid) { | 
					
						
							|  |  |  |     return PromiseA.reject({ | 
					
						
							|  |  |  |       message: 'token missing kid' | 
					
						
							|  |  |  |     , code: 'E_MISSING_KID' | 
					
						
							|  |  |  |     , url: 'https://oauth3.org/docs/errors#E_MISSING_KID' | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   if (!decoded.payload.iss) { | 
					
						
							|  |  |  |     return PromiseA.reject({ | 
					
						
							|  |  |  |       message: 'token missing iss' | 
					
						
							|  |  |  |     , code: 'E_MISSING_ISS' | 
					
						
							|  |  |  |     , url: 'https://oauth3.org/docs/errors#E_MISSING_ISS' | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   var OAUTH3 = require('oauth3.js'); | 
					
						
							|  |  |  |   OAUTH3._hooks = require('oauth3.js/oauth3.node.storage.js'); | 
					
						
							|  |  |  |   return OAUTH3.discover(decoded.payload.iss).then(function (directives) { | 
					
						
							|  |  |  |     var args = (directives || {}).retrieve_jwk; | 
					
						
							|  |  |  |     if (typeof args === 'string') { | 
					
						
							|  |  |  |       args = { url: args, method: 'GET' }; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     if (typeof (args || {}).url !== 'string') { | 
					
						
							|  |  |  |       return PromiseA.reject({ | 
					
						
							|  |  |  |         message: 'token issuer does not support retrieving JWKs' | 
					
						
							|  |  |  |       , code: 'E_INVALID_ISS' | 
					
						
							|  |  |  |       , url: 'https://oauth3.org/docs/errors#E_INVALID_ISS' | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     var params = { | 
					
						
							|  |  |  |       sub: sub | 
					
						
							|  |  |  |     , kid: kid | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  |     var url = args.url; | 
					
						
							|  |  |  |     var body; | 
					
						
							|  |  |  |     Object.keys(params).forEach(function (key) { | 
					
						
							|  |  |  |       if (url.indexOf(':'+key) !== -1) { | 
					
						
							|  |  |  |         url = url.replace(':'+key, params[key]); | 
					
						
							|  |  |  |         delete params[key]; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |     if (Object.keys(params).length > 0) { | 
					
						
							|  |  |  |       if ('GET' === (args.method || 'GET').toUpperCase()) { | 
					
						
							|  |  |  |         url += '?' + OAUTH3.query.stringify(params); | 
					
						
							|  |  |  |       } else { | 
					
						
							|  |  |  |         body = params; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return OAUTH3.request({ | 
					
						
							|  |  |  |       url: OAUTH3.url.resolve(directives.api, url) | 
					
						
							|  |  |  |     , method: args.method | 
					
						
							|  |  |  |     , data: body | 
					
						
							| 
									
										
										
										
											2017-08-11 17:00:18 -06:00
										 |  |  |     }).catch(function (err) { | 
					
						
							|  |  |  |       return PromiseA.reject({ | 
					
						
							|  |  |  |         message: 'failed to retrieve public key from token issuer' | 
					
						
							|  |  |  |       , code: 'E_NO_PUB_KEY' | 
					
						
							|  |  |  |       , url: 'https://oauth3.org/docs/errors#E_NO_PUB_KEY' | 
					
						
							|  |  |  |       , subErr: err.toString() | 
					
						
							|  |  |  |       }); | 
					
						
							| 
									
										
										
										
											2017-07-13 18:23:42 -06:00
										 |  |  |     }); | 
					
						
							|  |  |  |   }, function (err) { | 
					
						
							|  |  |  |     return PromiseA.reject({ | 
					
						
							|  |  |  |       message: 'token issuer is not a valid OAuth3 provider' | 
					
						
							|  |  |  |     , code: 'E_INVALID_ISS' | 
					
						
							|  |  |  |     , url: 'https://oauth3.org/docs/errors#E_INVALID_ISS' | 
					
						
							|  |  |  |     , subErr: err.toString() | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   }).then(function (res) { | 
					
						
							|  |  |  |     if (res.data.error) { | 
					
						
							|  |  |  |       return PromiseA.reject(res.data.error); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     var opts = {}; | 
					
						
							|  |  |  |     if (Array.isArray(res.data.alg)) { | 
					
						
							|  |  |  |       opts.algorithms = res.data.alg; | 
					
						
							|  |  |  |     } else if (typeof res.data.alg === 'string') { | 
					
						
							|  |  |  |       opts.algorithms = [res.data.alg]; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     try { | 
					
						
							|  |  |  |       return jwt.verify(token, require('jwk-to-pem')(res.data), opts); | 
					
						
							|  |  |  |     } catch (err) { | 
					
						
							|  |  |  |       return PromiseA.reject({ | 
					
						
							|  |  |  |         message: 'token verification failed' | 
					
						
							|  |  |  |       , code: 'E_INVALID_TOKEN' | 
					
						
							|  |  |  |       , url: 'https://oauth3.org/docs/errors#E_INVALID_TOKEN' | 
					
						
							|  |  |  |       , subErr: err.toString() | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-08-11 16:38:22 -06:00
										 |  |  | function deepFreeze(obj) { | 
					
						
							|  |  |  |   Object.keys(obj).forEach(function (key) { | 
					
						
							|  |  |  |     if (obj[key] && typeof obj[key] === 'object') { | 
					
						
							|  |  |  |       deepFreeze(obj[key]); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  |   Object.freeze(obj); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-09-12 22:32:33 +00:00
										 |  |  | function cookieOauth3(Models, req, res, next) { | 
					
						
							| 
									
										
										
										
											2017-08-30 17:47:31 +00:00
										 |  |  |   req.oauth3 = {}; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   var token = req.cookies.jwt; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   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; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-09-12 22:32:33 +00:00
										 |  |  |     req.oauth3.rescope = generateRescope(req, Models, decoded, fullPpid, ppid); | 
					
						
							| 
									
										
										
										
											2017-08-30 17:47:31 +00:00
										 |  |  |   }).then(function () { | 
					
						
							|  |  |  |     deepFreeze(req.oauth3); | 
					
						
							|  |  |  |     //Object.defineProperty(req, 'oauth3', {configurable: false, writable: false});
 | 
					
						
							|  |  |  |     next(); | 
					
						
							|  |  |  |   }, function (err) { | 
					
						
							|  |  |  |     if ('E_NO_TOKEN' === err.code) { | 
					
						
							|  |  |  |       next(); | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     console.error('[walnut] cookie lib/oauth3 error:'); | 
					
						
							|  |  |  |     console.error(err); | 
					
						
							|  |  |  |     res.send(err); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-09-12 22:32:33 +00:00
										 |  |  | function attachOauth3(Models, req, res, next) { | 
					
						
							| 
									
										
										
										
											2017-07-13 18:23:42 -06:00
										 |  |  |   req.oauth3 = {}; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   extractAccessToken(req).then(function (token) { | 
					
						
							| 
									
										
										
										
											2017-08-11 17:00:18 -06:00
										 |  |  |     req.oauth3.encodedToken = token; | 
					
						
							| 
									
										
										
										
											2017-07-24 16:19:51 -06:00
										 |  |  |     req.oauth3.verifyAsync = function (jwt) { | 
					
						
							|  |  |  |       return verifyToken(jwt || token); | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-07-13 18:23:42 -06:00
										 |  |  |     if (!token) { | 
					
						
							|  |  |  |       return null; | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2017-08-11 17:00:18 -06:00
										 |  |  |     return verifyToken(token); | 
					
						
							|  |  |  |   }).then(function  (decoded) { | 
					
						
							|  |  |  |     req.oauth3.token = decoded; | 
					
						
							| 
									
										
										
										
											2017-07-13 18:23:42 -06:00
										 |  |  |     if (!decoded) { | 
					
						
							| 
									
										
										
										
											2017-08-11 17:00:18 -06:00
										 |  |  |       return null; | 
					
						
							| 
									
										
										
										
											2017-08-10 11:09:39 -06:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2017-07-13 18:23:42 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-08-11 17:00:18 -06:00
										 |  |  |     var ppid = decoded.sub || decoded.ppid || decoded.appScopedId; | 
					
						
							| 
									
										
										
										
											2017-09-12 22:32:33 +00:00
										 |  |  |     var fullPpid = ppid+'@'+decoded.iss; | 
					
						
							| 
									
										
										
										
											2017-07-13 18:23:42 -06:00
										 |  |  |     req.oauth3.ppid = ppid; | 
					
						
							| 
									
										
										
										
											2017-08-11 17:00:18 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-09-12 22:32:33 +00:00
										 |  |  |     // 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, ''); | 
					
						
							| 
									
										
										
										
											2017-08-11 17:00:18 -06:00
										 |  |  |     req.oauth3.accountHash = hash; | 
					
						
							| 
									
										
										
										
											2017-07-13 18:23:42 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-09-12 22:32:33 +00:00
										 |  |  |     req.oauth3.rescope = generateRescope(req, Models, decoded, fullPpid, ppid); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     console.log('############### assigned req.oauth3:'); | 
					
						
							|  |  |  |     console.log(req.oauth3); | 
					
						
							| 
									
										
										
										
											2017-07-13 18:23:42 -06:00
										 |  |  |   }).then(function () { | 
					
						
							| 
									
										
										
										
											2017-09-12 22:32:33 +00:00
										 |  |  |     //deepFreeze(req.oauth3);
 | 
					
						
							| 
									
										
										
										
											2017-08-30 17:47:31 +00:00
										 |  |  |     //Object.defineProperty(req, 'oauth3', {configurable: false, writable: false});
 | 
					
						
							| 
									
										
										
										
											2017-07-13 18:23:42 -06:00
										 |  |  |     next(); | 
					
						
							|  |  |  |   }, function (err) { | 
					
						
							| 
									
										
										
										
											2017-08-30 17:47:31 +00:00
										 |  |  |     console.error('[walnut] JWT lib/oauth3 error:'); | 
					
						
							| 
									
										
										
										
											2017-08-16 19:47:51 +00:00
										 |  |  |     console.error(err); | 
					
						
							| 
									
										
										
										
											2017-07-13 18:23:42 -06:00
										 |  |  |     res.send(err); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | module.exports.attachOauth3 = attachOauth3; | 
					
						
							| 
									
										
										
										
											2017-08-30 17:47:31 +00:00
										 |  |  | module.exports.cookieOauth3 = cookieOauth3; | 
					
						
							| 
									
										
										
										
											2017-07-13 18:23:42 -06:00
										 |  |  | module.exports.verifyToken = verifyToken; |