forked from coolaj86/walnut.js
		
	
		
			
				
	
	
		
			307 lines
		
	
	
		
			9.0 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			307 lines
		
	
	
		
			9.0 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 'use strict';
 | |
| 
 | |
| var PromiseA = require('bluebird');
 | |
| 
 | |
| 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);
 | |
|     });
 | |
|   };
 | |
| }
 | |
| 
 | |
| 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) {
 | |
|   var jwt = require('jsonwebtoken');
 | |
|   var decoded;
 | |
| 
 | |
|   if (!token) {
 | |
|     return PromiseA.reject({
 | |
|       message: 'no token provided'
 | |
|     , code: 'E_NO_TOKEN'
 | |
|     , url: 'https://oauth3.org/docs/errors#E_NO_TOKEN'
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   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
 | |
|     }).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()
 | |
|       });
 | |
|     });
 | |
|   }, 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()
 | |
|       });
 | |
|     }
 | |
|   });
 | |
| }
 | |
| 
 | |
| function deepFreeze(obj) {
 | |
|   Object.keys(obj).forEach(function (key) {
 | |
|     if (obj[key] && typeof obj[key] === 'object') {
 | |
|       deepFreeze(obj[key]);
 | |
|     }
 | |
|   });
 | |
|   Object.freeze(obj);
 | |
| }
 | |
| 
 | |
| function cookieOauth3(Models, req, res, next) {
 | |
|   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;
 | |
| 
 | |
|     req.oauth3.rescope = generateRescope(req, Models, decoded, fullPpid, ppid);
 | |
|   }).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);
 | |
|   });
 | |
| }
 | |
| 
 | |
| 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);
 | |
|     };
 | |
| 
 | |
|     if (!token) {
 | |
|       return null;
 | |
|     }
 | |
|     return verifyToken(token);
 | |
|   }).then(function  (decoded) {
 | |
|     req.oauth3.token = decoded;
 | |
|     if (!decoded) {
 | |
|       return null;
 | |
|     }
 | |
| 
 | |
|     var ppid = decoded.sub || decoded.ppid || decoded.appScopedId;
 | |
|     var fullPpid = ppid+'@'+decoded.iss;
 | |
|     req.oauth3.ppid = ppid;
 | |
| 
 | |
|     // 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;
 | |
| 
 | |
|     req.oauth3.rescope = generateRescope(req, Models, decoded, fullPpid, ppid);
 | |
| 
 | |
|     console.log('############### assigned req.oauth3:');
 | |
|     console.log(req.oauth3);
 | |
|   }).then(function () {
 | |
|     //deepFreeze(req.oauth3);
 | |
|     //Object.defineProperty(req, 'oauth3', {configurable: false, writable: false});
 | |
|     next();
 | |
|   }, function (err) {
 | |
|     console.error('[walnut] JWT lib/oauth3 error:');
 | |
|     console.error(err);
 | |
|     res.send(err);
 | |
|   });
 | |
| }
 | |
| 
 | |
| module.exports.attachOauth3 = attachOauth3;
 | |
| module.exports.cookieOauth3 = cookieOauth3;
 | |
| module.exports.verifyToken = verifyToken;
 |